package game import ( "strings" "testing" "thehouseoficarus/internal/casino" "thehouseoficarus/internal/color" ) func TestBigSixRendering(t *testing.T) { for _, mode := range []string{"none", "ansi16", "xterm256"} { for _, unicode := range []bool{false, true} { s := &casino.BigSixSnapshot{Phase: "betting", Multiplier: "1", BettingTicks: 25, MinBet: 1, MaxBet: 10000} board := renderBigSixSnapshot(s, mode, unicode) if mode != "none" && !strings.Contains(board, "\x1b[2m") { t.Fatal("payouts must be dim") } if mode == "none" && strings.Contains(board, "\x1b") { t.Fatal("ANSI leaked into plain display") } for _, text := range []string{"$1", "$2", "$5", "$10", "$20", "SWORD", "SHIELD", "DOUBLE", "pays 47:1", "Bets:"} { if !strings.Contains(board, text) { t.Fatalf("missing %q: %s", text, board) } } for _, gone := range []string{"B I G", "RTP", "Place any number", "Odds are profit", "double & respin"} { if strings.Contains(board, gone) { t.Fatalf("stale felt text %q: %s", gone, board) } } if unicode && !strings.ContainsAny(board, "─│┌╭") { t.Fatalf("Unicode felt is still ASCII: %s", board) } if !unicode && strings.ContainsAny(board, "─│┌╭") { t.Fatalf("ASCII felt leaked Unicode: %s", board) } for _, phase := range []string{"playing", "result"} { s.Phase = phase s.Pockets = []string{"1", "2", "5", "10", "DOUBLE", "20", "SWORD", "SHIELD", "1"} if phase == "playing" { s.Pockets = append(s.Pockets, "1", "2") } text := renderBigSixSnapshot(s, mode, unicode) if strings.Count(text, "\n") < 5 { t.Fatal("not a multi-line wheel", text) } wall := "|" if unicode { wall = "│" } // The bill outline must continue through the number row. var bill string if phase == "result" { bill = wall + " $1 " + wall } else { bill = wall + " $1 " + wall } if !strings.Contains(text, bill) { t.Fatalf("number row is missing the bill walls (%q): %s", bill, text) } // Every rail and separator uses the same dim-yellow border ink. if mode != "none" { yellow := color.Render(mode, color.ColorSpec{Fg: 3, Bg: -1, Dim: true}, wall) if !strings.Contains(text, yellow) { t.Fatalf("wheel separators are not the yellow border color: %s", text) } } for _, line := range strings.Split(text, "\n") { if color.VisibleLen(line) > 80 { t.Fatalf("frame too wide (%d): %s", color.VisibleLen(line), line) } } pointer := "\\/" if unicode { pointer = "▼" } if !strings.Contains(text, pointer) { t.Fatal("no pointer") } if strings.ContainsAny(text, "◆") { t.Fatalf("second pointer glyph still present: %s", text) } if strings.Contains(text, "* * *") || strings.Contains(text, "[ STOP ]") { t.Fatalf("stale wheel result art: %s", text) } if phase == "result" { lines := strings.Split(strings.TrimRight(text, " \n"), "\n") if !strings.Contains(lines[len(lines)-1], "WINNER") { t.Fatalf("WINNER must print below the entire wheel: %s", text) } } } for _, line := range strings.Split(board, "\n") { if color.VisibleLen(line) > 80 { t.Fatalf("felt too wide (%d): %s", color.VisibleLen(line), line) } if strings.Contains(line, "pays 23:1") && mode != "none" { // The box walls stay bold; only the payout text is dim. if !strings.Contains(line, "\x1b[1m") || !strings.Contains(line, "\x1b[2m") { t.Fatalf("pays row walls/text have the wrong weight: %q", line) } } } } } } func TestBigSixUnicodeArtwork(t *testing.T) { // Check the rendered cells, not just the source art: independent row // centering must not misalign the blade or disconnect shield corners. for _, tt := range []struct { label string want []string }{ {"SWORD", []string{ "│ ▲ │", "│ ║ │", "│ ║ │", "│ ╘══╬══╛ │", "│ ╩ │", }}, {"SHIELD", []string{ "│ ╭───────╮ │", "│ │ ◇ │ │", "│ ╰╮ ╭╯ │", "│ ╰╮ ╭╯ │", "│ ╰───╯ │", }}, } { t.Run(tt.label, func(t *testing.T) { panel := bigSixPanel(casino.BigSixSpot{Label: tt.label, Pays: 47}, 24, "none", true) if got, want := strings.Join(panel[2:7], "\n"), strings.Join(tt.want, "\n"); got != want { t.Fatalf("table artwork:\n%s\nwant:\n%s", got, want) } }) } s := &casino.BigSixSnapshot{Phase: "result", Pockets: []string{"SWORD", "SHIELD"}} lines := strings.Split(renderBigSixSnapshot(s, "none", true), "\n") want := strings.Join([]string{ "│ ▲ │╭─────╮│", "│ ╘═╬═╛ │╰╮ ◇ ╭╯│", "│ ╩ │ ╰───╯ │", "│ SWORD │SHIELD │", }, "\n") if got := strings.Join(lines[2:6], "\n"); got != want { t.Fatalf("wheel artwork:\n%s\nwant:\n%s", got, want) } } func TestBigSixSpinAbbreviation(t *testing.T) { // "sp" is the blackjack split shorthand. Split is not a Big 6 command, so // the abbreviation must instead match the only relevant verb: spin. g, sess, _ := houseEdgeGame(t, "name: Wheel\ncasino_tables:\n - id: big6\n game: big6\n") g.queue = NewCommandQueue() g.executePlay(sess, []string{"big6"}, "play big6") g.executeBet(sess, []string{"2", "on", "1"}, "bet 2 on 1") if got, ok := g.casinoAbbreviation(sess, "sp"); !ok || got != "spin" { t.Fatalf("sp at Big 6 resolved to %q (ok=%v), want spin", got, ok) } // Prefixes of other games' verbs (split, roll, hit, double) must not // resolve at this table. for _, input := range []string{"spl", "spli", "ro", "hi", "dd"} { if got, ok := g.casinoAbbreviation(sess, input); ok { t.Fatalf("Big 6 %q unexpectedly abbreviated to %q", input, got) } } g.handleGameCommand(sess, "sp") if qc, ok := g.queue.PeekActive("alice"); !ok || qc.Command != "spin" { t.Fatalf("sp queued %+v (ok=%v), want spin", qc, ok) } } func TestBigSixCommandsAndVisibility(t *testing.T) { g, sess, conn := houseEdgeGame(t, "name: Wheel\ncasino_tables:\n - id: big6\n game: big6\n") g.executePlay(sess, []string{"big6"}, "play big6") table, playing := g.Casino.Participation(sess.Player.Name) if !playing || table == nil { t.Fatal("join failed", string(conn.data)) } for _, target := range []string{"1", "$20", "sword", "shield"} { g.executeBet(sess, []string{"2", "on", target}, "bet 2 on "+target) } if sess.Player.Credits != 92 || table.State() != "waiting" { t.Fatal("bet/spin conflated", sess.Player.Credits, table.State(), string(conn.data)) } conn.data = nil g.executeList(sess, nil, "list") if !strings.Contains(string(conn.data), "SWORD") || !strings.Contains(string(conn.data), "47:1") { t.Fatal("list missing targets", string(conn.data)) } conn.data = nil g.executeTableStatus(sess, nil, "table") if !strings.Contains(string(conn.data), "Bets:") { t.Fatal("table not routed", string(conn.data)) } conn.data = nil g.executeRules(sess, nil, "rules") if !strings.Contains(string(conn.data), "48/49") { t.Fatal("help not routed", string(conn.data)) } g.executeSpin(sess, nil, "spin") if table.State() != "playing" || sess.Player.Credits != 92 { t.Fatal("spin rebet/debit", string(conn.data)) } g.executeSpin(sess, nil, "spin") if sess.Player.Credits != 92 { t.Fatal("second spin debited") } beforePrompt := len(conn.data) g.writePrompt(sess) if len(conn.data) != beforePrompt { t.Fatal("prompt interrupted wheel") } if !table.Busy(sess.Player.Name) { t.Fatal("animation prompt handling") } if !g.seesCasinoDetails(sess, casino.Event{RoomID: 1, TableID: "gone", Snapshot: &casino.BigSixSnapshot{Players: []string{sess.Player.Name}}}) { t.Fatal("departing seat cannot see final frame") } }