aboutsummaryrefslogtreecommitdiff
path: root/internal/game/render_roulette_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-29 18:58:28 -0400
committerhistoria <[not public]>2026-09-29 18:58:28 -0400
commit624626b9a10ae47cf7d5f6e91765570ed759bd74 (patch)
tree9812f5d254b7d9cfe1d2fcb6da53362e974541b0 /internal/game/render_roulette_test.go
parentff9002b96e35ad0808945e7c35791460c9ca8776 (diff)
downloadthehouseoficarus-624626b9a10ae47cf7d5f6e91765570ed759bd74.tar.gz
feat: better roulette racetrack graphics and payout table
Diffstat (limited to 'internal/game/render_roulette_test.go')
-rw-r--r--internal/game/render_roulette_test.go143
1 files changed, 138 insertions, 5 deletions
diff --git a/internal/game/render_roulette_test.go b/internal/game/render_roulette_test.go
index d992abf..8d9c141 100644
--- a/internal/game/render_roulette_test.go
+++ b/internal/game/render_roulette_test.go
@@ -68,7 +68,7 @@ func TestRouletteArtModes(t *testing.T) {
layout := plainRoulette(layoutArt)
rows := strings.Split(layout, "\n")
if double {
- if !strings.Contains(string([]rune(rows[1])[:7]), "0") || !strings.Contains(string([]rune(rows[5])[:7]), "00") {
+ if !strings.Contains(string([]rune(rows[1])[:7]), "00") || !strings.Contains(string([]rune(rows[5])[:7]), "0") {
t.Fatal("double zero not left of grid", layout)
}
} else if !strings.Contains(string([]rune(rows[3])[:7]), "0") {
@@ -227,14 +227,14 @@ func TestRouletteZeroPanel(t *testing.T) {
}
// TestRouletteRacetrackPanel checks that an enabled racetrack adds the call-bet
-// panel above the board, names all four call bets and the neighbours bet, and
+// panel above the board, names the four call-bet areas (not Neighbors), and
// keeps the same width and ASCII/Unicode guarantees as the board.
func TestRouletteRacetrackPanel(t *testing.T) {
for _, mode := range []string{"none", "ansi", "xterm256"} {
for _, unicode := range []bool{false, true} {
art := renderRouletteLayout(false, true, mode, unicode)
plain := plainRoulette(art)
- for _, want := range []string{"Voisins du Zero", "Tiers du Cylindre", "Orphelins", "Zero Spiel", "Neighbors (9)"} {
+ for _, want := range []string{"Tiers du Cylindre", "Orphelins", "Voisins du Zero", "Zero"} {
if !strings.Contains(plain, want) {
t.Fatalf("mode=%s unicode=%v: missing %q:\n%s", mode, unicode, want, plain)
}
@@ -255,11 +255,11 @@ func TestRouletteRacetrackPanel(t *testing.T) {
}
// The panel comes from the snapshot flag, and a plain board omits it.
snap := &casino.RouletteSnapshot{Phase: "betting", Wheel: casino.WheelOrder(false), Racetrack: true}
- if !strings.Contains(plainRoulette(renderRouletteSnapshot(snap, "none", true, 7)), "Voisins du Zero") {
+ if !strings.Contains(plainRoulette(renderRouletteSnapshot(snap, "none", true, 7)), "Voisins") {
t.Fatal("racetrack snapshot did not draw the panel")
}
snap.Racetrack = false
- if strings.Contains(plainRoulette(renderRouletteSnapshot(snap, "none", true, 7)), "Voisins du Zero") {
+ if strings.Contains(plainRoulette(renderRouletteSnapshot(snap, "none", true, 7)), "Voisins") {
t.Fatal("plain snapshot drew the racetrack panel")
}
}
@@ -404,3 +404,136 @@ func TestRouletteSettlementShowsBalanceOnce(t *testing.T) {
}
}
}
+
+// Read the perimeter clockwise, not in terminal row order. Every displayed
+// pocket must match the actual European wheel, including the wraparound.
+func TestRouletteRacetrackPhysicalOrder(t *testing.T) {
+ for _, unicode := range []bool{false, true} {
+ art := renderRouletteRacetrack("none", unicode)
+ rows := strings.Split(art, "\n")
+ top, bottom := racetrackNumbers(rows[1]), racetrackNumbers(rows[9])
+ if len(top) != 16 || len(bottom) != 15 {
+ t.Fatalf("missing track pockets:\n%s", art)
+ }
+ order := append([]string(nil), top...)
+ for _, i := range []int{3, 5, 7} {
+ side := racetrackNumbers(rows[i])
+ if len(side) != 2 {
+ t.Fatalf("missing curved-end pockets:\n%s", art)
+ }
+ order = append(order, side[1])
+ }
+ for i := len(bottom) - 1; i >= 0; i-- {
+ order = append(order, bottom[i])
+ }
+ for _, i := range []int{7, 5, 3} {
+ order = append(order, racetrackNumbers(rows[i])[0])
+ }
+ wheel := casino.WheelOrder(false)
+ if len(order) != len(wheel) {
+ t.Fatalf("got %d pockets", len(order))
+ }
+ for i, p := range order {
+ if p != wheel[(19+i)%len(wheel)] {
+ t.Errorf("pocket %d: %s", i, p)
+ }
+ }
+ if got := racetrackNumbers(rows[5]); got[1] != "26" {
+ t.Fatalf("right tip is %s, want 26", got[1])
+ }
+ }
+}
+
+func racetrackNumbers(row string) []string {
+ var out []string
+ for _, match := range regexp.MustCompile(`([0-9]+)[RBG]`).FindAllStringSubmatch(row, -1) {
+ out = append(out, match[1])
+ }
+ return out
+}
+
+// The sections must touch their actual bet coverage, not merely contain
+// labels. Zero is a nested part of Voisins, not a fourth disjoint sector.
+func TestRouletteRacetrackSections(t *testing.T) {
+ for _, unicode := range []bool{false, true} {
+ rows := strings.Split(renderRouletteRacetrack("none", unicode), "\n")
+ segment := func(y, left, right int) string { return string([]rune(rows[y])[left:right]) }
+ nums := func(y, left, right int) []string { return racetrackNumbers(segment(y, left, right)) }
+ sections := map[string][]string{
+ "tiers": append(nums(1, 4, 20), nums(9, 4, 24)...),
+ "orphelins": append(nums(1, 20, 40), nums(9, 24, 40)...),
+ "voisins": append(nums(1, 40, 60), nums(9, 40, 60)...),
+ "jeuzero": append(nums(1, 60, 68), nums(9, 60, 68)...),
+ }
+ for _, y := range []int{3, 5, 7} {
+ sections["tiers"] = append(sections["tiers"], nums(y, 0, 4)...)
+ sections["jeuzero"] = append(sections["jeuzero"], nums(y, 68, 73)...)
+ }
+ sections["voisins"] = append(sections["voisins"], sections["jeuzero"]...)
+ for _, bet := range casino.RacetrackBets() {
+ got := sections[bet.Syntax]
+ if len(got) != len(bet.Series) {
+ t.Fatalf("%s: coverage %v, want %v", bet.Syntax, got, bet.Series)
+ }
+ seen := map[string]bool{}
+ for _, n := range got {
+ seen[n] = true
+ }
+ for _, n := range bet.Series {
+ if !seen[n] {
+ t.Errorf("%s does not touch %s", bet.Syntax, n)
+ }
+ }
+ }
+ for _, label := range []struct {
+ left, right int
+ text string
+ }{
+ {4, 22, "Tiers du Cylindre"}, {22, 40, "Orphelins"}, {40, 58, "Voisins du Zero"}, {58, 68, "Jeu Zero"},
+ } {
+ if strings.TrimSpace(segment(5, label.left+1, label.right)) != label.text {
+ t.Errorf("misplaced %s label", label.text)
+ }
+ }
+ // Every step joins orthogonally; no font-dependent diagonal gaps.
+ b := rouletteBorders(unicode)
+ for _, step := range []struct {
+ x, y int
+ text string
+ }{
+ {20, 3, b.v}, {20, 4, b.bl + b.h + b.tr}, {22, 5, b.v},
+ {22, 6, b.bl + b.h + b.tr}, {24, 7, b.v},
+ } {
+ if got := segment(step.y, step.x, step.x+len([]rune(step.text))); got != step.text {
+ t.Errorf("disconnected step at %d,%d: %q", step.x, step.y, got)
+ }
+ }
+ // The Zero boundary bows into Voisins then returns to the same column.
+ for _, p := range [][2]int{{60, 2}, {59, 3}, {58, 4}, {58, 5}, {58, 6}, {59, 7}, {60, 8}} {
+ if []rune(rows[p[1]])[p[0]] == ' ' {
+ t.Errorf("gap in Zero curve at %v", p)
+ }
+ }
+ }
+ for _, mode := range []string{"none", "ansi", "xterm256"} {
+ for _, unicode := range []bool{false, true} {
+ art := renderRouletteRacetrack(mode, unicode)
+ if strings.Contains(strings.ToLower(art), "neighbor") {
+ t.Fatal("Neighbors belongs in help, not on the racetrack")
+ }
+ rows := strings.Split(art, "\n")
+ for y, row := range rows {
+ wantWidth := 73
+ if y == 0 || y == 10 {
+ wantWidth = 69
+ }
+ if y == 1 || y == 9 {
+ wantWidth = 72
+ }
+ if color.VisibleLen(row) != wantWidth {
+ t.Errorf("mode=%s unicode=%v: row %d width=%d, want %d", mode, unicode, y, color.VisibleLen(row), wantWidth)
+ }
+ }
+ }
+ }
+}