aboutsummaryrefslogtreecommitdiff
path: root/internal/game/casino_ux_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-24 20:06:41 -0400
committerhistoria <[not public]>2026-09-24 20:07:31 -0400
commit13db3e50cb6e29eedbf25416e5284bf08b0adefb (patch)
treed5e9b99030b25cc7fc102288b694c133f8832f84 /internal/game/casino_ux_test.go
parentd114c747a7672c7e4daab3a5b5f1b2f066ebc912 (diff)
downloadthehouseoficarus-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.go153
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)))
+ }
+}