diff options
Diffstat (limited to 'internal/game/big_six_test.go')
| -rw-r--r-- | internal/game/big_six_test.go | 222 |
1 files changed, 222 insertions, 0 deletions
diff --git a/internal/game/big_six_test.go b/internal/game/big_six_test.go new file mode 100644 index 0000000..6a83cdf --- /dev/null +++ b/internal/game/big_six_test.go @@ -0,0 +1,222 @@ +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. + bill := wall + " $1 " + 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") + } +} |
