aboutsummaryrefslogtreecommitdiff
path: root/internal/game/big_six_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/big_six_test.go')
-rw-r--r--internal/game/big_six_test.go222
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")
+ }
+}