1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
|
package game
import (
"strings"
"testing"
"thehouseoficarus/internal/casino"
"thehouseoficarus/internal/color"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
)
func TestRouletteTrackerFormats(t *testing.T) {
for _, mode := range []string{"none", "ansi", "xterm256"} {
for _, unicode := range []bool{false, true} {
for _, history := range [][]string{nil, {"0"}, {"1", "2", "00", "1", "36", "00", "36", "36", "2", "0"}} {
s := &casino.RouletteSnapshot{DoubleZero: true, History: history}
art := renderRouletteTracker(s, mode, unicode)
rows := strings.Split(art, "\n")
if len(rows) != 5 {
t.Fatalf("tracker has %d rows", len(rows))
}
for _, row := range rows {
if color.VisibleLen(row) != 74 {
t.Errorf("tracker row width %d: %q", color.VisibleLen(row), plainRoulette(row))
}
if !unicode {
for _, r := range plainRoulette(row) {
if r > 127 {
t.Errorf("non-ASCII rune %q", r)
}
}
}
}
if mode == "none" && strings.Contains(art, "\x1b") {
t.Fatal("color disabled but ANSI present")
}
if mode != "none" {
for _, bg := range []int{0, 1, 2} {
if !strings.Contains(art, color.BgCode(mode, bg)) {
t.Errorf("missing color %d", bg)
}
}
}
plain := plainRoulette(art)
if strings.Contains(plain, "History, not odds") {
t.Fatal("tracker should not carry the removed caption")
}
if strings.Contains(plain, "newest first") {
t.Fatal("tracker should not label the recent order")
}
if len(history) == 0 {
for _, want := range []string{"last 0 spins", "awaiting the first spin", "Hot: --", "Cold: --", "Red --", "Black --"} {
if !strings.Contains(plain, want) {
t.Errorf("missing %q", want)
}
}
}
}
}
}
}
func TestRouletteTrackerStatistics(t *testing.T) {
s := &casino.RouletteSnapshot{DoubleZero: true, History: []string{"1", "2", "00", "1", "36", "00", "36", "36", "2", "0"}}
plain := plainRoulette(renderRouletteTracker(s, "none", true))
for _, want := range []string{"last 10 spins", "Red 50.0%", "Black 20.0%", "Zero 30.0%", "Hot: 36R (3) 00G (2) 1R (2)", "Cold: 3R (0) 4B (0) 5R (0)"} {
if !strings.Contains(strings.Join(strings.Fields(plain), " "), want) {
t.Errorf("missing %q:\n%s", want, plain)
}
}
// Only the newest eight, newest on the left, and 00 stays distinct from 0.
recent := strings.Split(plain, "\n")[1]
hits := racetrackNumbers(recent)
if got := strings.Join(hits, ","); got != "0,2,36,36,00,36,1,00" {
t.Errorf("recent hits: %s", got)
}
// Unhit pockets aren't called hot. Cold tie order includes 00 only on
// American wheels; it is not silently merged with zero.
s.History = []string{"0"}
if line := strings.Split(renderRouletteTracker(s, "none", false), "\n")[2]; !strings.Contains(strings.Join(strings.Fields(line), " "), "Hot: 0G (1) | Cold: 00G (0)") {
t.Errorf("zero ranking: %s", line)
}
s.DoubleZero = false
if strings.Contains(renderRouletteTracker(s, "none", false), "00G") {
t.Fatal("European tracker contains 00")
}
}
func TestRouletteTrackerAboveBettingBoardsOnly(t *testing.T) {
for _, racetrack := range []bool{false, true} {
s := &casino.RouletteSnapshot{Racetrack: racetrack, Phase: "betting", History: []string{"17"}, Wheel: casino.WheelOrder(false)}
art := renderRouletteSnapshot(s, "none", true, 7)
tracker := renderRouletteTracker(s, "none", true)
if !strings.HasPrefix(art, "\n"+tracker+"\n") {
t.Fatal("tracker not above board")
}
for _, phase := range []string{"playing", "result"} {
s.Phase = phase
s.Pockets = [5]string{"25", "17", "34", "6", "27"}
if strings.Contains(renderRouletteSnapshot(s, "none", true, 7), "History:") {
t.Fatalf("tracker appeared during %s", phase)
}
}
}
}
func TestRouletteTableShowsLiveHistory(t *testing.T) {
g := &Game{Casino: casino.NewManager(7)}
p := player.New("alice")
p.RoomID, p.Credits = 1, 100
p.Options = map[string]any{"color": "none", "unicode": true}
sess := &net.Session{Player: p}
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, "red", 1, casinoWallet{g: g, p: p, saveCharacter: func(*player.Player) error { return nil }})
g.Casino.Tick()
g.Casino.Tick()
// The table command renders the live status snapshot, which carries the
// tracker's completed-spin history.
var snap *casino.RouletteSnapshot
for _, event := range g.Casino.Table(1, "r").Status(p.Name) {
if view, ok := event.Snapshot.(*casino.RouletteSnapshot); ok && view.Phase != "result" {
snap = view
break
}
}
if snap == nil {
t.Fatal("no live roulette snapshot")
}
art := g.renderCasinoSnapshot(sess, snap)
if !strings.Contains(art, "History: last 1 spin") || strings.Contains(art, "awaiting the first spin") {
t.Fatalf("table did not show live history:\n%s", art)
}
}
|