aboutsummaryrefslogtreecommitdiff
path: root/internal/game/render_roulette_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/render_roulette_test.go')
-rw-r--r--internal/game/render_roulette_test.go217
1 files changed, 217 insertions, 0 deletions
diff --git a/internal/game/render_roulette_test.go b/internal/game/render_roulette_test.go
new file mode 100644
index 0000000..1dde5ad
--- /dev/null
+++ b/internal/game/render_roulette_test.go
@@ -0,0 +1,217 @@
+package game
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+ "testing"
+
+ "thehouseoficarus/internal/casino"
+ "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
+)
+
+var rouletteSGR = regexp.MustCompile(`\x1b\[[0-9;]*m`)
+
+func plainRoulette(s string) string { return rouletteSGR.ReplaceAllString(s, "") }
+
+func TestRouletteArtModes(t *testing.T) {
+ for _, mode := range []string{"none", "ansi", "xterm256"} {
+ for _, unicode := range []bool{false, true} {
+ for _, double := range []bool{false, true} {
+ t.Run(fmt.Sprintf("%s/unicode=%v/double=%v", mode, unicode, double), func(t *testing.T) {
+ wheelOrder := casino.WheelOrder(double)
+ s := &casino.RouletteSnapshot{DoubleZero: double, Wheel: wheelOrder}
+ for _, phase := range []string{"betting", "playing", "result"} {
+ s.Phase = phase
+ s.Pockets = [3]string{"26", "0", "32"}
+ art := renderRouletteSnapshot(s, mode, unicode, 7)
+ plain := plainRoulette(art)
+ if mode == "xterm256" && unicode && double {
+ t.Logf("%s preview (colors stripped):\n%s", phase, plain)
+ }
+ if strings.Contains(plain, "\x1b") {
+ t.Fatal("malformed ANSI")
+ }
+ if mode == "none" && art != plain {
+ t.Fatal("color disabled but ANSI present")
+ }
+ for _, r := range plain {
+ if !unicode && r > 127 {
+ t.Fatalf("non-ASCII rune %q in ASCII mode", r)
+ }
+ }
+ for _, line := range strings.Split(art, "\n") {
+ if color.VisibleLen(line) > 78 {
+ t.Fatalf("line exceeds terminal width: %q", plainRoulette(line))
+ }
+ }
+ if mode != "none" {
+ for _, bg := range []int{0, 1, 2} {
+ if !strings.Contains(art, color.BgCode(mode, bg)) {
+ t.Fatalf("missing pocket background %d", bg)
+ }
+ }
+ if !strings.Contains(art, color.FgCode(mode, 15)) || !strings.Contains(art, color.StyleCode("bold")) {
+ t.Fatal("missing bold white text")
+ }
+ if !strings.HasSuffix(art, color.Reset) {
+ t.Fatal("art leaves terminal colored")
+ }
+ }
+ }
+
+ layoutArt := renderRouletteLayout(double, mode, unicode)
+ layout := plainRoulette(layoutArt)
+ rows := strings.Split(layout, "\n")
+ if double {
+ if !strings.Contains(string([]rune(rows[2])[:7]), "0") || !strings.Contains(string([]rune(rows[6])[:7]), "00") {
+ t.Fatal("double zero not left of grid", layout)
+ }
+ } else if !strings.Contains(string([]rune(rows[4])[:7]), "0") {
+ t.Fatal("zero not left of grid", layout)
+ }
+ for _, target := range []string{"column1", "column2", "column3", "dozen1", "dozen2", "dozen3", "red", "black", "odd", "even", "low", "high"} {
+ if !strings.Contains(layout, target) {
+ t.Fatal("missing bet", target)
+ }
+ }
+ // The odds reminder belongs to "table", never the board.
+ for _, reminder := range []string{"Numbers 35:1", "Low: 1-18", "bet 10 on"} {
+ if strings.Contains(layout, reminder) {
+ t.Fatal("reminder text leaked onto the betting board")
+ }
+ }
+ if mode != "none" {
+ wantGreen := 1
+ if double {
+ wantGreen = 2
+ }
+ if got := strings.Count(layoutArt, color.BgCode(mode, 2)); got != wantGreen {
+ t.Fatalf("green cells on board = %d, want %d", got, wantGreen)
+ }
+ }
+
+ wheelArt := renderRouletteWheel(wheelOrder, mode, unicode)
+ plainWheel := plainRoulette(wheelArt)
+ lines := strings.Split(plainWheel, "\n")
+ if len(lines) != 23 {
+ t.Fatalf("wheel height = %d, want 23", len(lines))
+ }
+ wantDoubleZeroLabel := 0
+ if double {
+ wantDoubleZeroLabel = 1
+ }
+ if got := strings.Count(plainWheel, "00"); got != wantDoubleZeroLabel {
+ t.Fatalf("double-zero labels = %d, want %d", got, wantDoubleZeroLabel)
+ }
+ if mode != "none" {
+ wantGreen := 1
+ if double {
+ wantGreen = 2
+ }
+ greens := strings.Count(wheelArt, color.BgCode(mode, 2))
+ if greens != wantGreen {
+ t.Fatalf("green pockets = %d, want %d", greens, wantGreen)
+ }
+ total := greens + strings.Count(wheelArt, color.BgCode(mode, 0)) + strings.Count(wheelArt, color.BgCode(mode, 1))
+ if total != len(wheelOrder) {
+ t.Fatalf("drawn pockets = %d, want %d", total, len(wheelOrder))
+ }
+ }
+
+ result := plainRoulette(renderRouletteResult([3]string{"1", "00", "27"}, mode, unicode, 7))
+ for _, banned := range []string{"PREV", "BALL", "NEXT", "WINNER"} {
+ if strings.Contains(result, banned) {
+ t.Fatalf("result prose %q should be removed:\n%s", banned, result)
+ }
+ }
+ })
+ }
+ }
+ }
+}
+
+func TestRouletteResultSizesAndNeighbors(t *testing.T) {
+ for _, unicode := range []bool{false, true} {
+ for _, size := range []int{1, 5, 7, 9} {
+ art := plainRoulette(renderRouletteResult([3]string{"1", "00", "27"}, "xterm256", unicode, size))
+ lines := strings.Split(art, "\n")
+ height := max(5, cardRenderSize(unicode, size))
+ width := height + 2
+ if len(lines) != height {
+ t.Fatalf("size=%d, height=%d want %d", size, len(lines), height)
+ }
+ for _, line := range lines {
+ if color.VisibleLen(line) != 3*width+2 {
+ t.Fatalf("unequal pocket widths: %q", line)
+ }
+ }
+ center := []rune(lines[height/2])
+ if !strings.Contains(string(center[:width]), "1") || !strings.Contains(string(center[width+1:2*width+1]), "00") || !strings.Contains(string(center[2*width+2:]), "27") {
+ t.Fatal("wrong pocket order", art)
+ }
+ ball := "o"
+ if unicode {
+ ball = "●"
+ }
+ if strings.Count(art, ball) != 1 {
+ t.Fatal("missing or duplicate ball", art)
+ }
+ }
+ }
+}
+
+func TestRouletteEventColorsDoNotWrapArt(t *testing.T) {
+ for _, public := range []bool{false, true} {
+ for _, mode := range []string{"none", "ansi", "xterm256"} {
+ g := &Game{Casino: casino.NewManager(1), Hub: net.NewHub()}
+ p := player.New("alice")
+ p.RoomID = 1
+ p.Options = map[string]any{"color": mode, "unicode": true}
+ conn := &casinoTestConn{}
+ sess := &net.Session{Player: p, Conn: conn}
+ g.loggedInChars = map[string]*net.Session{"alice": sess}
+ g.Hub.EnterRoom(sess, 1)
+ // Use an emphatic blue background to detect whole-message color wrapping.
+ sess.Account = &player.Account{Colors: map[string]string{"casino_action": "0F bg:04 bold"}}
+ g.Casino.Join(1, casino.TableConfig{ID: "r", Game: casino.GameRoulette}, p.Name)
+ e := casino.Event{RoomID: 1, TableID: "r", Message: "Spinning now", Color: "casino_action", Public: public, Snapshot: &casino.RouletteSnapshot{Phase: "playing", Players: []string{p.Name}}}
+ if !public {
+ e.PrivateTo = p.Name
+ }
+ g.emitCasinoEvents([]casino.Event{e})
+ output := strings.ReplaceAll(string(conn.data), "\r\n", "\n")
+ expected := g.colorize(sess, "casino_action", e.Message) + "\n" + g.renderCasinoSnapshot(sess, e.Snapshot) + "\n"
+ if output != expected {
+ t.Fatalf("public=%v mode=%s: prose color leaked into art or snapshot missing\n%q\nwant\n%q", public, mode, output, expected)
+ }
+ }
+ }
+}
+
+func TestRouletteSettlementShowsBalanceOnce(t *testing.T) {
+ for _, action := range []string{"", "sitout", "leave_after"} {
+ g := &Game{Casino: casino.NewManager(7), Combat: combat.NewTracker(), Deps: Deps{World: world.New(t.TempDir())}}
+ p := player.New("alice")
+ p.Credits = 1000
+ p.Options = map[string]any{"color": "none"}
+ conn := &casinoTestConn{}
+ sess := &net.Session{Player: p, Conn: conn}
+ g.loggedInChars = map[string]*net.Session{"alice": sess}
+ cfg := casino.TableConfig{ID: "r", Game: casino.GameRoulette, BettingTicks: 1, Roulette: casino.RouletteConfig{SpinTicks: 1}}
+ g.Casino.Join(1, cfg, p.Name)
+ g.Casino.Bet(1, "r", p.Name, "dozen2", 10, casinoWallet{g: g, p: p, saveCharacter: func(*player.Player) error { return nil }})
+ if action != "" {
+ g.Casino.Action(1, "r", p.Name, action)
+ }
+ g.Casino.Tick()
+ g.emitCasinoEvents(g.Casino.Tick())
+ if got := strings.Count(string(conn.data), "Available "); got != 1 {
+ t.Fatalf("action=%q: balance printed %d times\n%s", action, got, conn.data)
+ }
+ }
+}