package game import ( "strings" "testing" "thehouseoficarus/internal/casino" "thehouseoficarus/internal/color" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) func TestCasinoCommandsWrapBeforeSessionWithoutLosingIndent(t *testing.T) { commands := []casino.MenuCommand{ {Command: "bet on ", Desc: "Place bets until the dice roll; table rules lists targets"}, {Command: "roll / autoroll / stop", Desc: "Shoot / repeat on your turn / return to manual play"}, {Command: "unbet / unbet all", Desc: "Remove one bet / every optional bet"}, {Command: "table", Desc: "View the felt"}, {Command: "insurance", Desc: "Buy insurance; a deliberately long description which requires a hanging continuation line"}, {Command: "decline", Desc: "Continue without insurance (also the timeout default)"}, {Command: "bet on player/banker/tie/player_pair/banker_pair", Desc: "One wager per round; 'on' is optional"}, {Command: "option casino_display compact", Desc: "Compact action hints (detailed for full command menus)"}, {Command: "spin", Desc: "Spin using your current wager"}, {Command: "autospin", Desc: "Start automatic betting"}, {Command: "unicode", Desc: strings.Repeat("é", 100)}, } for _, wrap := range []int{0, 40, 60, 80, 120} { p := player.New("a") p.Options = map[string]any{"wrap_width": wrap} conn := &casinoTestConn{} sess := &net.Session{Player: p, Conn: conn} g := &Game{} text := g.casinoMenuCommands(sess, commands) sess.WriteLine(text) limit := 80 if wrap > 0 { limit = min(limit, wrap) } for _, line := range strings.Split(strings.ReplaceAll(plainRoulette(string(conn.data)), "\r\n", "\n"), "\n") { if line == "" { continue } if color.VisibleLen(line) > limit { t.Fatalf("wrap %d overlong %d: %q", wrap, color.VisibleLen(line), line) } if !strings.HasPrefix(line, " ") { t.Fatalf("wrap %d lost indent: %q", wrap, line) } } expected := strings.ReplaceAll(plainRoulette(text), "\r\n", "\n") actual := strings.TrimRight(strings.ReplaceAll(plainRoulette(string(conn.data)), "\r\n", "\n"), "\n") if expected != actual { t.Fatalf("session reflowed prewrapped menu at %d", wrap) } } } func TestCasinoCommandsHangingIndentAndShortDescription(t *testing.T) { sess := &net.Session{Player: player.New("a")} g := &Game{} commands := []casino.MenuCommand{ {Command: "bet on ", Desc: "Place a wager; targets: table rules"}, {Command: "table", Desc: "View the felt"}, } text := plainRoulette(g.casinoMenuCommands(sess, commands)) lines := strings.Split(text, "\n") if len(lines) != 2 { t.Fatal("short commands should not wrap", text) } col := strings.Index(lines[0], "Place") if col != strings.Index(lines[1], "View") { t.Fatal("descriptions not aligned", text) } commands[0].Desc = strings.Repeat("long description ", 10) text = plainRoulette(g.casinoMenuCommands(sess, commands)) lines = strings.Split(text, "\n") for _, line := range lines[1 : len(lines)-1] { if !strings.HasPrefix(line, strings.Repeat(" ", col)) { t.Fatal("missing hanging indent", text) } } } func TestCasinoCompactHintsUseOnlyAvailableCommands(t *testing.T) { p := player.New("a") p.Options = map[string]any{"casino_display": "compact"} p.Credits = 10 sess := &net.Session{Player: p} g := &Game{} menu := &casino.MenuView{Heading: "Your turn", Commands: []casino.MenuCommand{ {Command: "hit", Desc: "Take a card"}, {Command: "stand", Desc: "Hold"}, {Command: "double", Cost: 20}, }} text := plainRoulette(g.casinoPrivateMessage(sess, "casino_menu", menu, "")) if strings.Contains(text, "double") || !strings.Contains(text, " h:hit") || !strings.Contains(text, " st:stand") { t.Fatal(text) } menu.ShowBalance = true menu.Repeat = true menu.Commands = []casino.MenuCommand{{Command: "table", Desc: "View the felt"}} text = plainRoulette(g.casinoPrivateMessage(sess, "casino_menu", menu, "")) if strings.Contains(text, "bet") || strings.Contains(text, "sitout") { t.Fatal("compact mode invented actions", text) } } func TestSimplifiedFeltSpacingAndNoIdlePrompt(t *testing.T) { text := renderCrapsSnapshot(&casino.CrapsSnapshot{Variant: "simplified"}, "none", true) for _, want := range []string{"SIMPLIFIED CRAPS", "DICE TOTAL", "PAYOUT", "3 or 4", "1:1", "10 or 11", "LOSE STAKE"} { if !strings.Contains(text, want) { t.Fatalf("missing %q: %s", want, text) } } for _, stale := range []string{"even money", "NET PROFIT", "stake back", "PASS ONLY", "ONE ROLL", "COME-OUT"} { if strings.Contains(text, stale) { t.Fatalf("stale simplified rules %q", stale) } } if strings.Contains(text, "Place a bet, then roll when ready.") { t.Fatal(text) } rows := strings.Split(strings.TrimSuffix(text, "\n"), "\n") for i := 0; i < len(rows)-1; i++ { if strings.TrimSpace(rows[i]) == "" && strings.TrimSpace(rows[i+1]) == "" { t.Fatalf("consecutive blank lines at %d: %q", i, text) } } // The paytable ends at its own bottom border: no footer block remains. if strings.Count(text, "─") == 0 || strings.Contains(text, "NO POINT, ODDS") { t.Fatalf("stale footer: %q", text) } }