diff options
| author | historia <[not public]> | 2026-09-24 20:06:41 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-24 20:07:31 -0400 |
| commit | 13db3e50cb6e29eedbf25416e5284bf08b0adefb (patch) | |
| tree | d5e9b99030b25cc7fc102288b694c133f8832f84 /internal/game/casino_ux_test.go | |
| parent | d114c747a7672c7e4daab3a5b5f1b2f066ebc912 (diff) | |
| download | thehouseoficarus-13db3e50cb6e29eedbf25416e5284bf08b0adefb.tar.gz | |
feat: casino table ux: aliases, rebet hints, deadline notifications, spectator mode
Diffstat (limited to 'internal/game/casino_ux_test.go')
| -rw-r--r-- | internal/game/casino_ux_test.go | 153 |
1 files changed, 153 insertions, 0 deletions
diff --git a/internal/game/casino_ux_test.go b/internal/game/casino_ux_test.go new file mode 100644 index 0000000..046d7d3 --- /dev/null +++ b/internal/game/casino_ux_test.go @@ -0,0 +1,153 @@ +package game + +import ( + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + "thehouseoficarus/internal/casino" + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/engine" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" +) + +func stripANSI(s string) string { + return regexp.MustCompile(`\x1b\[[0-9;]*m`).ReplaceAllString(s, "") +} + +func uxCasinoGame(t *testing.T) (*Game, *net.Session, *casinoTestConn) { + t.Helper() + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "rooms"), 0755); err != nil { + t.Fatal(err) + } + room := "name: Casino\ncasino_tables:\n - id: bj1\n game: blackjack\n min_bet: 1\n max_bet: 100\n blackjack:\n action_ticks: 25\n - id: bac1\n game: baccarat\n min_bet: 5\n max_bet: 100\n" + if err := os.WriteFile(filepath.Join(dir, "rooms", "1.yaml"), []byte(room), 0644); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(filepath.Join(dir, "players", "characters"), 0o755); err != nil { + t.Fatal(err) + } + p := player.New("alice") + sess := &net.Session{Player: p, Conn: &casinoTestConn{}} + g := &Game{ + Deps: Deps{World: world.New(dir), AccountStore: player.NewAccountStore(dir), Ticks: engine.New()}, + Casino: casino.NewManager(1), + Combat: combat.NewTracker(), + safespot: NewSafespotManager(), + restTimers: make(map[string]uint64), + loggedInChars: map[string]*net.Session{"alice": sess}, + } + p.Credits = 100 + p.RoomID = 1 + return g, sess, sess.Conn.(*casinoTestConn) +} + +func TestPlayAliasesJoinGames(t *testing.T) { + for input, game := range map[string]string{"bj": "blackjack", "black": "blackjack", "bac": "baccarat", "baccarat": "baccarat"} { + g, sess, conn := uxCasinoGame(t) + g.executePlay(sess, strings.Fields(input), "play "+input) + table, ok := g.Casino.Participation("alice") + if !ok || table == nil || string(table.Config.Game) != game { + t.Fatalf("play %s: participation=%v game=%q", input, ok, game) + } + if !strings.Contains(stripANSI(string(conn.data)), "Next hand") { + t.Fatalf("play %s: missing betting menu, got %q", input, stripANSI(string(conn.data))) + } + } +} + +func TestPlayAmbiguousAndUnknownListChoices(t *testing.T) { + g, sess, conn := uxCasinoGame(t) + _ = g + for _, input := range []string{"", "b", "zzz"} { + conn.data = nil + args := []string{} + if input != "" { + args = strings.Fields("play " + input)[1:] + } + g.executePlay(sess, args, "play "+input) + text := stripANSI(string(conn.data)) + if !strings.Contains(text, "play blackjack") || !strings.Contains(text, "play baccarat") { + t.Fatalf("play %q: expected choice list, got %q", input, text) + } + } +} + +func TestPlayWithAmountJoinsAndBets(t *testing.T) { + g, sess, conn := uxCasinoGame(t) + g.executePlay(sess, []string{"bj", "25"}, "play bj 25") + if !strings.Contains(stripANSI(string(conn.data)), "You bet 25 credits.") { + t.Fatalf("missing wager confirmation: %q", stripANSI(string(conn.data))) + } +} + +func TestTableStatusShowsStateAndRules(t *testing.T) { + g, sess, conn := uxCasinoGame(t) + g.executePlay(sess, []string{"bj"}, "play bj") + g.executeTableStatus(sess, nil, "table") + text := stripANSI(string(conn.data)) + if !strings.Contains(text, "Limits") || !strings.Contains(text, "seated") { + t.Fatalf("status missing overview: %q", text) + } + conn.data = nil + g.executeTableStatus(sess, []string{"rules"}, "table rules") + if !strings.Contains(stripANSI(string(conn.data)), "payout") { + t.Fatalf("table rules missing: %q", stripANSI(string(conn.data))) + } +} + +func TestWatchAndSpectatorDetailFlow(t *testing.T) { + g, sess, conn := uxCasinoGame(t) + g.executePlay(sess, []string{"bj", "10"}, "play bj 10") + // Alice, seated, sees the deal; watch off is still acknowledged. + if _, seated := g.Casino.Participation("alice"); !seated { + t.Fatal("alice not seated") + } + g.executeWatch(sess, []string{"off"}, "watch off") + if !strings.Contains(stripANSI(string(conn.data)), "stop watching") { + t.Fatalf("watch off not confirmed: %q", stripANSI(string(conn.data))) + } +} + +func TestBlackjackShortcutsReachTheTable(t *testing.T) { + g, sess, conn := uxCasinoGame(t) + cfg := casino.TableConfig{ID: "blackjack", Game: casino.GameBlackjack, MinBet: 1, MaxBet: 100, Blackjack: casino.BlackjackConfig{ActionTicks: 3}} + g.Casino.Join(1, cfg, "alice") + // Rounds deal instantly with a lone bettor; dealer aces and naturals vary + // by seed, so repeat short rounds until a hit actually lands. + hit := false + for round := 0; round < 12 && !hit; round++ { + g.executeBet(sess, nil, "rebet") + g.executeTableAction(sess, "decline") + for attempt := 0; attempt < 3 && !hit; attempt++ { + g.executeTableAction(sess, "h") + hit = strings.Contains(stripANSI(string(conn.data)), "You hit") + } + g.executeTableAction(sess, "st") + } + if _, seated := g.Casino.Participation("alice"); !seated { + t.Fatal("shortcut actions should never remove the seat") + } + if !hit { + t.Fatalf("shortcuts did not hit: %q", stripANSI(string(conn.data))) + } +} + +func TestRebetRepeatsWager(t *testing.T) { + g, sess, conn := uxCasinoGame(t) + cfg := casino.TableConfig{ID: "bac", Game: casino.GameBaccarat, MinBet: 5, MaxBet: 100} + g.Casino.Join(1, cfg, "alice") + g.Casino.Bet(1, "bac", "alice", "banker", 20, casinoWallet{g: g, p: sess.Player}) + conn.data = nil + // The first round deals and settles instantly with a single bettor, so a + // fresh betting window opens; rebet should repeat 20 on banker. + g.executeBet(sess, nil, "rebet") + if !strings.Contains(stripANSI(string(conn.data)), "on banker") { + t.Fatalf("rebet did not target banker: %q", stripANSI(string(conn.data))) + } +} |
