package game import ( "strings" "testing" "thehouseoficarus/internal/casino" "thehouseoficarus/internal/color" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) type casinoTestConn struct { data []byte } func (c *casinoTestConn) ReadMessage() (string, error) { return "", nil } func (c *casinoTestConn) Write(b []byte) (int, error) { c.data = append(c.data, b...) return len(b), nil } func (c *casinoTestConn) Close() error { return nil } func (c *casinoTestConn) SetEcho(bool) error { return nil } func TestExecuteBetRejectsInvalidMachineArgs(t *testing.T) { g := &Game{Casino: casino.NewManager(1)} cfg := casino.MachineConfig{ID: "slots", Game: casino.GameSlots} g.Casino.JoinMachine(1, cfg, "alice") conn := &casinoTestConn{} sess := &net.Session{Player: player.New("alice"), Conn: conn} g.loggedInChars = map[string]*net.Session{"alice": sess} for _, args := range [][]string{{"banana"}, {"-5"}, {"10", "extra"}} { conn.data = nil g.executeBet(sess, args, "bet "+strings.Join(args, " ")) if !strings.Contains(string(conn.data), "Use bet, bet , or bet max.") { t.Fatalf("bet %v: expected usage error, got %q", args, string(conn.data)) } } // A bet amount at a machine sets the wager and immediately spins. conn.data = nil g.executeBet(sess, []string{"10"}, "bet 10") if !strings.Contains(string(conn.data), "You don't have enough chips and credits") { t.Fatalf("bet 10 should attempt an auto-spin and fail at the empty wallet, got %q", string(conn.data)) } } func TestExecuteBetParsesBaccaratSpots(t *testing.T) { g := &Game{Casino: casino.NewManager(1)} cfg := casino.TableConfig{ID: "baccarat", Game: casino.GameBaccarat} g.Casino.Join(1, cfg, "alice") conn := &casinoTestConn{} sess := &net.Session{Player: player.New("alice"), Conn: conn} g.loggedInChars = map[string]*net.Session{"alice": sess} // Valid spot syntaxes reach the table and fail only at the (empty) wallet. for _, args := range [][]string{{"tie", "100"}, {"100", "tie"}, {"100", "on", "banker"}, {"banker", "100"}} { conn.data = nil g.executeBet(sess, args, "bet "+strings.Join(args, " ")) if !strings.Contains(string(conn.data), "You don't have enough chips and credits") { t.Fatalf("bet %v: expected wallet failure, got %q", args, string(conn.data)) } } // Invalid spots are rejected by the table. conn.data = nil g.executeBet(sess, []string{"red", "100"}, "bet red 100") if !strings.Contains(string(conn.data), "must be on player, banker, or tie") { t.Fatalf("bet red 100: expected spot rejection, got %q", string(conn.data)) } // Two amounts or two spots are usage errors. for _, args := range [][]string{{"10", "20"}, {"player", "banker"}} { conn.data = nil g.executeBet(sess, args, "bet "+strings.Join(args, " ")) if !strings.Contains(string(conn.data), "Use bet,") { t.Fatalf("bet %v: expected usage error, got %q", args, string(conn.data)) } } // A bare first bet asks for a target (via the rebet path). conn.data = nil g.executeBet(sess, nil, "bet") if !strings.Contains(string(conn.data), "Bet on what?") { t.Fatalf("bare bet: expected target guidance, got %q", string(conn.data)) } } func TestCasinoMenuRendersAlignedColoredCommands(t *testing.T) { g := &Game{Casino: casino.NewManager(1)} conn := &casinoTestConn{} sess := &net.Session{Player: player.New("alice"), Conn: conn} g.loggedInChars = map[string]*net.Session{"alice": sess} menu := &casino.MenuView{Kind: casino.MenuSlotCommands, Commands: []casino.MenuCommand{ {Command: "bet ", Desc: "Set your wager"}, {Command: "spin", Desc: "Spin using your current wager"}, }} msg := g.casinoPrivateMessage(sess, "casino_menu", menu, "Your slot machine bet is set to 10.") if !strings.Contains(msg, "You have 0 chips and 0 credits.") { t.Fatalf("missing balance line: %q", msg) } // Commands are painted with the "command" color (0A bold). if !strings.Contains(msg, "\033[1m\033[38;5;10mbet \033[0m") { t.Fatalf("command not colored: %q", msg) } // Shorter commands are right-padded to align the descriptions. if !strings.Contains(msg, "\033[1m\033[38;5;10mspin \033[0m Spin using your current wager") { t.Fatalf("commands not aligned: %q", msg) } } func TestRenderBaccaratSnapshot(t *testing.T) { snapshot := &casino.BaccaratSnapshot{ Player: []casino.CardView{{Rank: "5", Suit: "clubs"}, {Rank: "4", Suit: "hearts"}}, Banker: []casino.CardView{{Rank: "2", Suit: "spades"}, {Rank: "3", Suit: "diamonds"}}, PlayerScore: "9", BankerScore: "5", Bets: []casino.BaccaratBetView{{Name: "alice", Spot: "player", Amount: 100}}, } plain := renderBaccaratSnapshot(snapshot, "none", false, 5, "dark", nil) if !strings.Contains(plain, "Player (9):") || !strings.Contains(plain, "Banker (5):") || !strings.Contains(plain, "alice bets 100 on player.") { t.Fatalf("unexpected baccarat render: %q", plain) } output := renderBaccaratSnapshot(snapshot, "xterm256", true, 7, "dark", nil) if !strings.Contains(output, "\033[38;5;") { t.Fatalf("expected colored baccarat art: %q", output) } for _, line := range strings.Split(output, "\n") { if color.VisibleLen(line) > 80 { t.Fatalf("baccarat line exceeds console width: %d", color.VisibleLen(line)) } } } func TestRenderSlotSnapshotColorsWordSymbols(t *testing.T) { snapshot := &casino.SlotSnapshot{StoppedReels: 1} snapshot.Grid[0][0] = "scatter" if !strings.Contains(renderSlotSnapshot(snapshot, "none", false), "scatter") { t.Fatal("expected word-based slot symbols") } output := renderSlotSnapshot(snapshot, "xterm256", true) if !strings.Contains(output, "\033[38;5;") { t.Fatalf("expected colored slot symbols, got %q", output) } for _, line := range strings.Split(output, "\n") { if color.VisibleLen(line) != 51 { t.Fatalf("line visible width = %d, want 51: %q", color.VisibleLen(line), line) } } if !strings.Contains(output, "┌") || !strings.Contains(output, "└") { t.Fatalf("slot output lacks the Unicode frame: %q", output) } } func TestRenderBlackjackSnapshotUsesCompactASCIIArt(t *testing.T) { snapshot := &casino.BlackjackSnapshot{ Dealer: []casino.CardView{{Rank: "?", Hidden: true}, {Rank: "K", Suit: "hearts"}}, Players: []casino.BlackjackPlayerView{{Name: "alice", Score: "20", Cards: []casino.CardView{{Rank: "5", Suit: "clubs"}, {Rank: "Q", Suit: "spades"}}}}, } plain := renderBlackjackSnapshot(snapshot, "none", false, 5, "dark", nil) if !strings.Contains(plain, "+---+") || !strings.Contains(plain, "Dealer:") || !strings.Contains(plain, "alice (20):") { t.Fatalf("unexpected blackjack art: %q", plain) } output := renderBlackjackSnapshot(snapshot, "xterm256", false, 5, "dark", nil) if !strings.Contains(output, "\033[38;5;") { t.Fatalf("expected colored blackjack art: %q", output) } if strings.ContainsAny(output, "│─┌┐└┘") { t.Fatalf("blackjack art contains Unicode borders: %q", output) } for _, line := range strings.Split(output, "\n") { if color.VisibleLen(line) > 80 { t.Fatalf("blackjack line exceeds console width: %d", color.VisibleLen(line)) } } unicodeOutput := renderBlackjackSnapshot(snapshot, "none", true, 7, "dark", nil) if !strings.Contains(unicodeOutput, "│5 │") || !strings.Contains(unicodeOutput, "│ 5│") || !strings.Contains(unicodeOutput, "│ ♣ │") { t.Fatalf("unexpected Unicode card alignment: %q", unicodeOutput) } coloredUnicode := renderBlackjackSnapshot(snapshot, "xterm256", true, 7, "dark", nil) if !strings.Contains(coloredUnicode, "\033[38;5;15m") || !strings.Contains(coloredUnicode, "\033[38;5;27m") { t.Fatalf("expected white borders and blue card art: %q", coloredUnicode) } light := renderBlackjackSnapshot(snapshot, "xterm256", true, 7, "light", nil) if !strings.Contains(light, "\033[48;5;15m") { t.Fatalf("expected white card background in light style: %q", light) } lightLines := strings.Split(light, "\n") if strings.Contains(lightLines[1], "\033[48;5;15m") || strings.Contains(lightLines[7], "\033[48;5;15m") { t.Fatalf("card edges should not have a background: %q", light) } lightPlain := renderBlackjackSnapshot(snapshot, "none", true, 7, "light", nil) if !strings.Contains(lightPlain, "▀▀▀▀▀▀▀") || !strings.Contains(lightPlain, "▄▄▄▄▄▄▄") { t.Fatalf("expected half-height light card edges: %q", lightPlain) } }