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") } }