package game import ( "strings" "testing" "thehouseoficarus/internal/casino" "thehouseoficarus/internal/color" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) func TestRouletteTrackerFormats(t *testing.T) { for _, mode := range []string{"none", "ansi", "xterm256"} { for _, unicode := range []bool{false, true} { for _, history := range [][]string{nil, {"0"}, {"1", "2", "00", "1", "36", "00", "36", "36", "2", "0"}} { s := &casino.RouletteSnapshot{DoubleZero: true, History: history} art := renderRouletteTracker(s, mode, unicode) rows := strings.Split(art, "\n") if len(rows) != 5 { t.Fatalf("tracker has %d rows", len(rows)) } for _, row := range rows { if color.VisibleLen(row) != 74 { t.Errorf("tracker row width %d: %q", color.VisibleLen(row), plainRoulette(row)) } if !unicode { for _, r := range plainRoulette(row) { if r > 127 { t.Errorf("non-ASCII rune %q", r) } } } } if mode == "none" && strings.Contains(art, "\x1b") { t.Fatal("color disabled but ANSI present") } if mode != "none" { for _, bg := range []int{0, 1, 2} { if !strings.Contains(art, color.BgCode(mode, bg)) { t.Errorf("missing color %d", bg) } } } plain := plainRoulette(art) if strings.Contains(plain, "History, not odds") { t.Fatal("tracker should not carry the removed caption") } if strings.Contains(plain, "newest first") { t.Fatal("tracker should not label the recent order") } if len(history) == 0 { for _, want := range []string{"last 0 spins", "awaiting the first spin", "Hot: --", "Cold: --", "Red --", "Black --"} { if !strings.Contains(plain, want) { t.Errorf("missing %q", want) } } } } } } } func TestRouletteTrackerStatistics(t *testing.T) { s := &casino.RouletteSnapshot{DoubleZero: true, History: []string{"1", "2", "00", "1", "36", "00", "36", "36", "2", "0"}} plain := plainRoulette(renderRouletteTracker(s, "none", true)) for _, want := range []string{"last 10 spins", "Red 50.0%", "Black 20.0%", "Zero 30.0%", "Hot: 36R (3) 00G (2) 1R (2)", "Cold: 3R (0) 4B (0) 5R (0)"} { if !strings.Contains(strings.Join(strings.Fields(plain), " "), want) { t.Errorf("missing %q:\n%s", want, plain) } } // Only the newest eight, newest on the left, and 00 stays distinct from 0. recent := strings.Split(plain, "\n")[1] hits := racetrackNumbers(recent) if got := strings.Join(hits, ","); got != "0,2,36,36,00,36,1,00" { t.Errorf("recent hits: %s", got) } // Unhit pockets aren't called hot. Cold tie order includes 00 only on // American wheels; it is not silently merged with zero. s.History = []string{"0"} if line := strings.Split(renderRouletteTracker(s, "none", false), "\n")[2]; !strings.Contains(strings.Join(strings.Fields(line), " "), "Hot: 0G (1) | Cold: 00G (0)") { t.Errorf("zero ranking: %s", line) } s.DoubleZero = false if strings.Contains(renderRouletteTracker(s, "none", false), "00G") { t.Fatal("European tracker contains 00") } } func TestRouletteTrackerAboveBettingBoardsOnly(t *testing.T) { for _, racetrack := range []bool{false, true} { s := &casino.RouletteSnapshot{Racetrack: racetrack, Phase: "betting", History: []string{"17"}, Wheel: casino.WheelOrder(false)} art := renderRouletteSnapshot(s, "none", true, 7) tracker := renderRouletteTracker(s, "none", true) if !strings.HasPrefix(art, "\n"+tracker+"\n") { t.Fatal("tracker not above board") } for _, phase := range []string{"playing", "result"} { s.Phase = phase s.Pockets = [5]string{"25", "17", "34", "6", "27"} if strings.Contains(renderRouletteSnapshot(s, "none", true, 7), "History:") { t.Fatalf("tracker appeared during %s", phase) } } } } func TestRouletteTableShowsLiveHistory(t *testing.T) { g := &Game{Casino: casino.NewManager(7)} p := player.New("alice") p.RoomID, p.Credits = 1, 100 p.Options = map[string]any{"color": "none", "unicode": true} sess := &net.Session{Player: p} cfg := casino.TableConfig{ID: "r", Game: casino.GameRoulette, BettingTicks: 1, Roulette: casino.RouletteConfig{SpinTicks: 1}} g.Casino.Join(1, cfg, p.Name) g.Casino.Bet(1, "r", p.Name, "red", 1, casinoWallet{g: g, p: p, saveCharacter: func(*player.Player) error { return nil }}) g.Casino.Tick() g.Casino.Tick() // The table command renders the live status snapshot, which carries the // tracker's completed-spin history. var snap *casino.RouletteSnapshot for _, event := range g.Casino.Table(1, "r").Status(p.Name) { if view, ok := event.Snapshot.(*casino.RouletteSnapshot); ok && view.Phase != "result" { snap = view break } } if snap == nil { t.Fatal("no live roulette snapshot") } art := g.renderCasinoSnapshot(sess, snap) if !strings.Contains(art, "History: last 1 spin") || strings.Contains(art, "awaiting the first spin") { t.Fatalf("table did not show live history:\n%s", art) } }