aboutsummaryrefslogtreecommitdiff
path: root/internal/game/baseline_race_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-15 15:35:52 -0400
committerhistoria <[not public]>2026-09-15 15:35:52 -0400
commit0b0b25222b3131ada79ff78c28eb8bf07d977deb (patch)
tree2ceaef5203595044f66a8f00064cd8e017f8f48d /internal/game/baseline_race_test.go
parent396c3facb8287b9a34076cae8f3d7b300d366d99 (diff)
downloadthehouseoficarus-0b0b25222b3131ada79ff78c28eb8bf07d977deb.tar.gz
feat: single-owner game state, transactional lifecycle, durable money on server stop
Diffstat (limited to 'internal/game/baseline_race_test.go')
-rw-r--r--internal/game/baseline_race_test.go132
1 files changed, 132 insertions, 0 deletions
diff --git a/internal/game/baseline_race_test.go b/internal/game/baseline_race_test.go
new file mode 100644
index 0000000..de5b7b4
--- /dev/null
+++ b/internal/game/baseline_race_test.go
@@ -0,0 +1,132 @@
+package game
+
+// Owner-boundary concurrency regressions. These exercise the competing
+// runtime entry points the way production does (connection input, ticks,
+// disconnects, admin mutations, owner-routed wallet operations) and must
+// pass under -race with the single-owner design in place.
+
+import (
+ "fmt"
+ "sync"
+ "testing"
+ "thehouseoficarus/internal/net"
+)
+
+// TestBaselineRaceSessionInputTickDisconnect exercises the three competing
+// runtime entry points against each other: connection-goroutine input, the
+// ticker goroutine, and hub disconnect callbacks.
+func TestBaselineRaceSessionInputTickDisconnect(t *testing.T) {
+ h := newHarness(t)
+ h.seedAccount("alice", "pw", false)
+ h.seedAccount("bob", "pw", false)
+ h.seedCharacter("alice", "alice")
+ h.seedCharacter("bob", "bob")
+
+ aSess, _ := h.connect("alice")
+ bSess, _ := h.connect("bob")
+
+ aLines := h.goInput(aSess, 1)
+ bLines := h.goInput(bSess, 1)
+
+ var wg sync.WaitGroup
+ wg.Add(2)
+ go func() {
+ defer wg.Done()
+ for i := 0; i < 200; i++ {
+ aLines <- "look"
+ }
+ close(aLines)
+ }()
+ go func() {
+ defer wg.Done()
+ for i := 0; i < 200; i++ {
+ bLines <- "say hi"
+ }
+ close(bLines)
+ }()
+
+ // Disconnect bob from a third goroutine while everything else runs.
+ go func() {
+ for i := 0; i < 50; i++ {
+ h.hub.PlayersInRoom(1)
+ if i == 25 {
+ h.hub.Remove(bSess)
+ }
+ }
+ }()
+
+ wg.Wait()
+}
+
+// TestBaselineRaceAdminRewriteWhilePlaying hammers Game.RewriteRoomIDs (the
+// admin entry point) while players keep moving and ticking.
+func TestBaselineRaceAdminRewriteWhilePlaying(t *testing.T) {
+ h := newHarness(t)
+ h.seedAccount("carol", "pw", false)
+ h.seedCharacter("carol", "carol")
+ sess, _ := h.connect("carol")
+ lines := h.goInput(sess, 1)
+
+ var wg sync.WaitGroup
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for i := 0; i < 200; i++ {
+ lines <- fmt.Sprintf("go north")
+ }
+ close(lines)
+ }()
+
+ // Concurrent admin room-ID rewrites targeting the player's live state.
+ for i := 0; i < 20; i++ {
+ h.g.RewriteRoomIDs(map[int]int{2: 2 + i + 1, 2 + i: 2 + i + 1})
+ }
+
+ wg.Wait()
+}
+
+// TestBaselineRaceCasinoWallet drives concurrent casino activity through
+// the owner boundary (as production does): wallet debits/credits from
+// multiple players' session tasks plus session input, all on one owner.
+func TestBaselineRaceCasinoWallet(t *testing.T) {
+ h := newHarness(t)
+ for _, n := range []string{"dave", "erin", "frank"} {
+ h.seedAccount(n, "pw", false)
+ p := h.seedCharacter(n, n)
+ p.Credits = 100000
+ }
+ sessions := make([]*net.Session, 0, 3)
+ for _, n := range []string{"dave", "erin", "frank"} {
+ s, _ := h.connect(n)
+ sessions = append(sessions, s)
+ }
+
+ var wg sync.WaitGroup
+ for _, s := range sessions {
+ wallet := casinoWallet{g: h.g, p: s.Player}
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for i := 0; i < 100; i++ {
+ h.g.RunOnOwner(func() {
+ wallet.Wager(1)
+ wallet.PayCredits(1)
+ })
+ }
+ }()
+
+ // Session input competes with the wallet operations.
+ lines := h.goInput(s, 1)
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for i := 0; i < 100; i++ {
+ lines <- "look"
+ }
+ close(lines)
+ }()
+ }
+
+ wg.Wait()
+ h.barrier()
+}