diff options
| author | historia <[not public]> | 2026-09-15 17:04:48 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-15 17:04:48 -0400 |
| commit | 69fb90f6876d7f7dbe7be0d515c94849e5431caf (patch) | |
| tree | 15fb0f5897c537315d4283bb8265d28a13aae313 /internal/game/quit_casino_test.go | |
| parent | d89504c3df74706b387b8fa829dd291e31ca01c7 (diff) | |
| download | thehouseoficarus-69fb90f6876d7f7dbe7be0d515c94849e5431caf.tar.gz | |
cmd_quit gracefully departs casino games; cmd_stop cancels a pending quit
Diffstat (limited to 'internal/game/quit_casino_test.go')
| -rw-r--r-- | internal/game/quit_casino_test.go | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/internal/game/quit_casino_test.go b/internal/game/quit_casino_test.go new file mode 100644 index 0000000..a62e9cf --- /dev/null +++ b/internal/game/quit_casino_test.go @@ -0,0 +1,185 @@ +package game + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "thehouseoficarus/internal/casino" + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +// quitTestGame builds the minimal live state quit and stop need: a casino +// manager, a combat tracker, a tick engine for the rest countdown, and a +// durable wallet (economy journal + account store) so wagers can commit. +func quitTestGame(t *testing.T) (*Game, *net.Session) { + t.Helper() + dataDir := t.TempDir() + // Character saves (wallet persistence) need the player directory. + if err := os.MkdirAll(filepath.Join(dataDir, "players", "characters"), 0o755); err != nil { + t.Fatal(err) + } + sess := &net.Session{Player: player.New("alice"), Conn: &casinoTestConn{}} + g := &Game{ + Deps: Deps{AccountStore: player.NewAccountStore(dataDir), Ticks: engine.New()}, + Casino: casino.NewManager(1), + Combat: combat.NewTracker(), + Economy: newEconomyJournal(t.TempDir()), + safespot: NewSafespotManager(), + loggedInChars: map[string]*net.Session{"alice": sess}, + restTimers: make(map[string]uint64), + } + sess.Player.Credits = 1000 + return g, sess +} + +func (g *Game) quitResting(name string) bool { + _, ok := g.restTimers[name] + return ok +} + +// A wager committed to the round must block a deliberate quit: the player +// stays seated with the stake intact and the quit countdown never starts. +func TestQuitBlockedMidRoundKeepsWager(t *testing.T) { + g, sess := quitTestGame(t) + cfg := casino.TableConfig{ID: "blackjack", Game: casino.GameBlackjack, MinBet: 1, MaxBet: 100} + g.Casino.Join(1, cfg, "alice") + g.Casino.Join(1, cfg, "bob") // second seat opens the betting window + conn := sess.Conn.(*casinoTestConn) + if events := g.Casino.Bet(1, "blackjack", "alice", "", 10, casinoWallet{g: g, p: sess.Player}); len(events) == 0 { + t.Fatal("bet produced no events") + } + + conn.data = nil + g.doQuit(sess) + + if !strings.Contains(string(conn.data), "middle of a blackjack round") { + t.Fatalf("expected mid-round refusal, got %q", string(conn.data)) + } + if _, playing := g.Casino.Participation("alice"); !playing { + t.Fatal("quit mid-round removed the player from the table") + } + if g.quitResting("alice") { + t.Fatal("blocked quit started the rest countdown") + } + // The stake is still committed to the round, not refunded. + if events := g.Casino.Leave(1, "blackjack", "alice"); len(events) == 0 || !strings.Contains(events[0].Message, "middle of a blackjack round") { + t.Fatalf("wager was not still committed after the blocked quit: %+v", events) + } +} + +// Baccarat blocks a mid-round quit the same way. +func TestQuitBlockedMidRoundBaccarat(t *testing.T) { + g, sess := quitTestGame(t) + cfg := casino.TableConfig{ID: "baccarat", Game: casino.GameBaccarat, MinBet: 1, MaxBet: 100} + g.Casino.Join(1, cfg, "alice") + g.Casino.Join(1, cfg, "bob") // second seat opens the betting window + conn := sess.Conn.(*casinoTestConn) + g.Casino.Bet(1, "baccarat", "alice", "player", 10, casinoWallet{g: g, p: sess.Player}) + + conn.data = nil + g.doQuit(sess) + + if !strings.Contains(string(conn.data), "middle of a baccarat round") { + t.Fatalf("expected mid-round refusal, got %q", string(conn.data)) + } + if _, playing := g.Casino.Participation("alice"); !playing { + t.Fatal("quit mid-round removed the player from the baccarat table") + } + if g.quitResting("alice") { + t.Fatal("blocked quit started the rest countdown") + } +} + +// With no wager committed, quit leaves the table gracefully (the same +// stand-up the table emits for "stop") and starts the rest countdown. +func TestQuitLeavesTableBetweenRounds(t *testing.T) { + g, sess := quitTestGame(t) + cfg := casino.TableConfig{ID: "blackjack", Game: casino.GameBlackjack, MinBet: 1, MaxBet: 100} + g.Casino.Join(1, cfg, "alice") + conn := sess.Conn.(*casinoTestConn) + + g.doQuit(sess) + + if !strings.Contains(string(conn.data), "You stand up.") { + t.Fatalf("expected a graceful stand-up, got %q", string(conn.data)) + } + if _, playing := g.Casino.Participation("alice"); playing { + t.Fatal("quit did not free the table seat") + } + if !g.quitResting("alice") { + t.Fatal("quit did not start the rest countdown") + } +} + +// Quit at a slot machine stops any auto-bet (or leaves the machine) and +// starts the rest countdown; it never force-disconnects the player. +func TestQuitLeavesMachine(t *testing.T) { + g, sess := quitTestGame(t) + cfg := casino.MachineConfig{ID: "slots", Game: casino.GameSlots, MinBet: 1, MaxBet: 100} + g.Casino.JoinMachine(1, cfg, "alice") + conn := sess.Conn.(*casinoTestConn) + + g.doQuit(sess) + + if _, playing := g.Casino.Participation("alice"); playing { + t.Fatal("quit did not free the machine") + } + if !g.quitResting("alice") { + t.Fatal("quit did not start the rest countdown") + } + if strings.Contains(string(conn.data), "WARNING") { + t.Fatalf("quit at a machine must not warn or forfeit: %q", string(conn.data)) + } +} + +// "stop" is an instant command, so it must cancel a pending quit countdown +// explicitly. +func TestStopCancelsQuit(t *testing.T) { + g, sess := quitTestGame(t) + g.restTimers["alice"] = 42 + + g.executeStop(sess, nil, "stop") + + if g.quitResting("alice") { + t.Fatal("stop did not cancel the pending quit") + } +} + +// Stop and quit share one departure path: both refuse mid-round and both +// leave gracefully between rounds. +func TestStopMatchesQuitCasinoPolicy(t *testing.T) { + cfg := casino.TableConfig{ID: "blackjack", Game: casino.GameBlackjack, MinBet: 1, MaxBet: 100} + + // Mid-round: stop refuses just like quit. + g, sess := quitTestGame(t) + g.Casino.Join(1, cfg, "alice") + g.Casino.Join(1, cfg, "bob") + g.Casino.Bet(1, "blackjack", "alice", "", 10, casinoWallet{g: g, p: sess.Player}) + g.restTimers["alice"] = 42 + conn := sess.Conn.(*casinoTestConn) + conn.data = nil + g.executeStop(sess, nil, "stop") + if !strings.Contains(string(conn.data), "middle of a blackjack round") { + t.Fatalf("expected mid-round refusal, got %q", string(conn.data)) + } + if _, playing := g.Casino.Participation("alice"); !playing { + t.Fatal("stop mid-round removed the player from the table") + } + + // Between rounds: stop stands the player up. + g2, sess2 := quitTestGame(t) + g2.Casino.Join(1, cfg, "alice") + conn2 := sess2.Conn.(*casinoTestConn) + g2.executeStop(sess2, nil, "stop") + if !strings.Contains(string(conn2.data), "You stand up.") { + t.Fatalf("expected a graceful stand-up, got %q", string(conn2.data)) + } + if _, playing := g2.Casino.Participation("alice"); playing { + t.Fatal("stop did not free the table seat") + } +} |
