package game import ( "fmt" "strings" "testing" "unicode/utf8" "thehouseoficarus/internal/casino" "thehouseoficarus/internal/color" ) func TestCashHuntCommandsAndReceipt(t *testing.T) { for _, choice := range []string{"100", "pick"} { t.Run(choice, func(t *testing.T) { g, s, conn := casinoCrapsGame(t, "name: Vault\ncasino_tables:\n - id: cash_hunt\n game: cash_hunt\n") s.Player.Options = map[string]any{"color": "none", "casino_hints": "off"} g.executePlay(s, []string{"cash", "hunt"}, "play cash hunt") table, ok := g.Casino.Participation(s.Player.Name) if !ok || table == nil || table.Config.Game != casino.GameCashHunt { t.Fatal("Cash Hunt alias did not seat player") } g.executeCommand(s, "difficulty", []string{"hard"}, "difficulty hard") conn.data = nil g.executeBet(s, []string{"1"}, "bet 1") text := string(conn.data) preview, hidden := strings.Index(text, "PRIZE PREVIEW"), strings.Index(text, "hidden and mixed") if preview < 0 || hidden <= preview || !strings.Contains(text, " 100 ") || len(s.Player.PendingWagers) != 1 { t.Fatalf("preview/shuffle sequence missing: %s", text) } if table.Busy(s.Player.Name) { t.Fatal("choosing must not suppress the input prompt") } conn.data = nil g.doLookTarget(s, "cash hunt") if strings.Contains(string(conn.data), "PRIZE PREVIEW") || !strings.Contains(string(conn.data), "hidden and mixed") { t.Fatal("look leaked sealed positions") } conn.data = nil if choice == "pick" { g.executeCommand(s, "pick", []string{"100"}, "pick 100") } else { // Exercise the real input path: a bare square number must survive // classification instead of being rejected as an unknown verb. g.handleGameCommand(s, choice) } text = string(conn.data) if strings.Contains(text, "Unknown command") { t.Fatalf("bare square number rejected: %s", text) } if len(s.Player.PendingWagers) != 0 || strings.Count(text, "Returned:") != 1 || !strings.Contains(text, ">100<") { t.Fatalf("selection/receipt failed: %s", text) } conn.data = nil g.executeCommand(s, "100", nil, "100") if strings.Contains(string(conn.data), "Returned:") { t.Fatal("duplicate selection paid again") } g.executeCommand(s, "paytable", nil, "paytable") if !strings.Contains(string(conn.data), "50000x") { t.Fatal("paytable missing jackpots") } }) } } func TestCashHuntBareNumberReachesGame(t *testing.T) { // A player typing any square number, including 1 and 100, must select it. g, s, conn := casinoCrapsGame(t, "name: Vault\ncasino_tables:\n - id: cash_hunt\n game: cash_hunt\n") s.Player.Options = map[string]any{"color": "none", "casino_hints": "off"} g.executePlay(s, []string{"cash", "hunt"}, "play cash hunt") for _, square := range []string{"1", "9", "10", "99", "100"} { g.executeBet(s, []string{"1"}, "bet 1") conn.data = nil g.handleGameCommand(s, square) if !strings.Contains(string(conn.data), fmt.Sprintf(">%s<", square)) { t.Fatalf("square %s was not selected: %s", square, conn.data) } } } func TestCashHuntSettleReachesGame(t *testing.T) { // settle is not a shared-registry verb; a seated player must reach the game. g, s, conn := casinoCrapsGame(t, "name: Vault\ncasino_tables:\n - id: cash_hunt\n game: cash_hunt\n") s.Player.Options = map[string]any{"color": "none", "casino_hints": "off"} g.executePlay(s, []string{"cash", "hunt"}, "play cash hunt") conn.data = nil g.handleGameCommand(s, "settle") if strings.Contains(string(conn.data), "Unknown command") { t.Fatalf("settle was rejected before reaching Cash Hunt: %s", conn.data) } } func TestCashHuntPlayAliasesWithWager(t *testing.T) { for _, name := range []string{"cash hunt", "cash_hunt", "cashhunt", "cash-hunt"} { g, s, conn := casinoCrapsGame(t, "name: Vault\ncasino_tables:\n - id: cash_hunt\n game: cash_hunt\n") args := append(strings.Fields(name), "1") g.executePlay(s, args, "play "+name+" 1") if len(s.Player.PendingWagers) != 1 || !strings.Contains(stripANSI(string(conn.data)), "PRIZE PREVIEW") { t.Fatalf("inline wager failed for %q: %s", name, conn.data) } } } func TestCashHuntBoardRendering(t *testing.T) { prizes := []int{0, 1, 2, 5, 10, 20, 100, 200, 500, 1000, 10000, 50000} for _, mode := range []string{"none", "ansi", "xterm256"} { for _, unicode := range []bool{false, true} { for _, phase := range []string{"preview", "result"} { s := &casino.CashHuntSnapshot{Phase: phase, Risk: "hard"} for i := range s.Squares { s.Squares[i] = prizes[i%len(prizes)] } if phase == "result" { s.Selected = 100 } rendered := renderCashHunt(s, mode, unicode) if mode == "none" && strings.Contains(rendered, "\x1b") { t.Fatal("plain board has ANSI escapes") } lines := strings.Split(stripANSI(rendered), "\n") if len(lines) != 22 { t.Fatalf("want two headers and 20 tile lines, got %d", len(lines)) } for _, line := range lines[2:] { if utf8.RuneCountInString(line) != 70 { t.Fatalf("tile row is not 70 columns: %q", line) } } board := strings.Join(lines[2:], "\n") if strings.ContainsAny(board, "[]") { t.Fatalf("square numbers must not use brackets: %q", board) } for row := 0; row < 10; row++ { numberLine := lines[2+2*row] for col := 0; col < 10; col++ { square := row*10 + col + 1 label := fmt.Sprintf("%d", square) if square == s.Selected { label = fmt.Sprintf(">%d<", square) } got := numberLine[col*cashHuntTileWidth : (col+1)*cashHuntTileWidth] if want := cashHuntCenter(label, cashHuntTileWidth); got != want { t.Fatalf("square %d label: got %q want %q", square, got, want) } } } if !strings.Contains(board, "50000x") { t.Fatal("largest prize clipped") } if !unicode { for _, c := range stripANSI(rendered) { if c > 127 { t.Fatal("ASCII mode printed Unicode") } } } if mode == "xterm256" { // Dark rainbow columns replace the old black/blue checker. if strings.Contains(rendered, "\x1b[48;5;16m") || strings.Contains(rendered, "\x1b[48;5;17m") { t.Fatal("black/blue tile background survived") } for _, shade := range cashHuntTileColors { if !strings.Contains(rendered, color.BgCode("xterm256", shade)) { t.Fatalf("missing dark rainbow shade %d", shade) } } } } hidden := renderCashHunt(&casino.CashHuntSnapshot{Phase: "hidden"}, mode, unicode) for _, line := range strings.Split(stripANSI(hidden), "\n") { if utf8.RuneCountInString(line) != 70 { t.Fatalf("shuffle banner width changed: %q", line) } } } } }