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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
|
package game
import (
"strings"
"testing"
"thehouseoficarus/internal/casino"
)
func coinFlipRoom() string {
return "name: Two Moons Flip\ncasino_tables:\n - id: coin_flip\n game: coin_flip\n min_bet: 1\n max_bet: 100\n"
}
func TestCoinFlipPlayAndFlip(t *testing.T) {
g, s, conn := casinoCrapsGame(t, coinFlipRoom())
s.Player.Options = map[string]any{"color": "none", "casino_hints": "on"}
g.executePlay(s, []string{"coin", "flip"}, "play coin flip")
table, ok := g.Casino.Participation(s.Player.Name)
if !ok || table == nil || table.Config.Game != casino.GameCoinFlip {
t.Fatal("Coin Flip alias did not seat the player")
}
if table.StakeMultiple() != 100 {
t.Fatalf("stake multiple %d, want 100", table.StakeMultiple())
}
// Sitting down must not print a coin or a multiplier.
if text := string(conn.data); strings.Contains(text, "▐") || strings.Contains(text, "BLUE") {
t.Fatalf("join printed a coin: %s", text)
}
// The player only holds 100 credits, so a stake of 1 reserves 100.
conn.data = nil
g.executeBet(s, []string{"1"}, "bet 1")
if len(s.Player.PendingWagers) != 1 || s.Player.PendingWagers[0].Credits != 100 {
t.Fatalf("bet did not reserve 100x: %+v", s.Player.PendingWagers)
}
if !strings.Contains(string(conn.data), "Stake 1") {
t.Fatalf("bet did not print the stake line: %s", conn.data)
}
// The first spin frame is held back one tick.
conn.data = nil
g.CasinoTick()
if !strings.Contains(string(conn.data), "▼") {
t.Fatalf("first tick did not start the red spin: %s", conn.data)
}
// Advance until the round lands (the spin + flip plan is at most ~40 frames).
for i := 0; i < 200 && len(s.Player.PendingWagers) > 0; i++ {
conn.data = nil
g.CasinoTick()
}
if len(s.Player.PendingWagers) != 0 {
t.Fatalf("round never settled: %+v", s.Player.PendingWagers)
}
if !strings.Contains(stripANSI(string(conn.data)), "Returned:") {
t.Fatalf("settlement receipt missing: %s", conn.data)
}
}
func TestCoinFlipStakeLimit(t *testing.T) {
g, s, _ := casinoCrapsGame(t, coinFlipRoom())
s.Player.Options = map[string]any{"color": "none"}
g.executePlay(s, []string{"coin flip"}, "play coin flip")
// 100 credits cannot cover a stake of 2 (200 reserved).
g.executeBet(s, []string{"2"}, "bet 2")
if len(s.Player.PendingWagers) != 0 {
t.Fatalf("accepted a stake the reserve cannot cover: %+v", s.Player.PendingWagers)
}
}
func TestCoinFlipBetMaxUsesReserve(t *testing.T) {
g, s, _ := casinoCrapsGame(t, coinFlipRoom())
s.Player.Options = map[string]any{"color": "none"}
g.executePlay(s, []string{"coin flip"}, "play coin flip")
// 100 credits with a 100x reserve means the largest stake is 1.
g.executeBet(s, []string{"max"}, "bet max")
if len(s.Player.PendingWagers) != 1 || s.Player.PendingWagers[0].Credits != 100 {
t.Fatalf("bet max did not reserve 100x the affordable stake: %+v", s.Player.PendingWagers)
}
}
func TestCoinFlipSettleReachesGame(t *testing.T) {
g, s, conn := casinoCrapsGame(t, coinFlipRoom())
s.Player.Options = map[string]any{"color": "none"}
g.executePlay(s, []string{"coin flip"}, "play coin flip")
conn.data = nil
g.handleGameCommand(s, "settle")
if strings.Contains(string(conn.data), "Unknown command") {
t.Fatalf("settle was rejected before reaching Coin Flip: %s", conn.data)
}
}
func TestCoinFlipWheelInk(t *testing.T) {
if ink, bold := coinFlipWheelInk(30, casino.CoinFlipSideRed); ink != -1 || bold {
t.Fatalf("30x = %d,%v; want plain", ink, bold)
}
if ink, bold := coinFlipWheelInk(60, casino.CoinFlipSideRed); ink != coinFlipRedInk || bold {
t.Fatalf("60x = %d,%v; want red not bold", ink, bold)
}
if ink, bold := coinFlipWheelInk(80, casino.CoinFlipSideBlue); ink != coinFlipBlueInk || !bold {
t.Fatalf("80x = %d,%v; want blue bold", ink, bold)
}
if ink, bold := coinFlipWheelInk(-90, casino.CoinFlipSideRed); ink != coinFlipRedInk || !bold {
t.Fatalf("-90x = %d,%v; want red bold", ink, bold)
}
}
func TestCoinFlipRendering(t *testing.T) {
for _, mode := range []string{"none", "ansi", "xterm256"} {
for _, unicode := range []bool{false, true} {
spin := &casino.CoinFlipSnapshot{
Phase: casino.CoinFlipPhaseRedSpin,
SpinSide: casino.CoinFlipSideRed,
SpinValues: []int{-10, -20, -30, -40, -50, -60, -70},
}
rendered := renderCoinFlip(spin, mode, unicode)
if mode == "none" && strings.Contains(rendered, "\x1b") {
t.Fatalf("plain spin has ANSI escapes: %q", rendered)
}
if !strings.Contains(rendered, "▼") && unicode {
t.Fatalf("spin strip missing its pointer: %q", rendered)
}
set := &casino.CoinFlipSnapshot{
Phase: casino.CoinFlipPhaseRedSpin,
SpinSide: casino.CoinFlipSideRed,
SpinValues: []int{-10, -20, -30, -40, -50, -60, -70},
Set: true,
}
if !strings.Contains(renderCoinFlip(set, mode, unicode), "SET") {
t.Fatalf("landed spin missing SET: %q", renderCoinFlip(set, mode, unicode))
}
results := &casino.CoinFlipSnapshot{Phase: casino.CoinFlipPhaseResults, Red: -61, Blue: 85}
rendered = renderCoinFlip(results, mode, unicode)
if plain := stripANSI(rendered); !strings.Contains(plain, "-61x") || !strings.Contains(plain, "+85x") {
t.Fatalf("results panel missing a face: %q", plain)
}
coin := &casino.CoinFlipSnapshot{Phase: casino.CoinFlipPhaseFlipping, Side: casino.CoinFlipSideBlue, Multiplier: 42}
rendered = renderCoinFlip(coin, mode, unicode)
if plain := stripANSI(rendered); !strings.Contains(plain, "+42x") {
t.Fatalf("coin missing its multiplier: %q", plain)
}
if strings.Contains(rendered, "COIN FLIP") || strings.Contains(rendered, "stake") {
t.Fatalf("flip printed chrome instead of just the coin: %q", rendered)
}
settled := &casino.CoinFlipSnapshot{Phase: casino.CoinFlipPhaseResult, Side: casino.CoinFlipSideRed, Multiplier: -50, Result: true}
rendered = renderCoinFlip(settled, mode, unicode)
if plain := stripANSI(rendered); !strings.Contains(plain, "-50x") {
t.Fatalf("settled coin missing its multiplier: %q", plain)
}
if mode == "xterm256" && !strings.Contains(rendered, "48;5;196") {
t.Fatalf("settled red coin has no red fill: %q", rendered)
}
if mode == "ansi" && !strings.Contains(rendered, "\x1b[41m") {
t.Fatalf("settled red coin has no red fill: %q", rendered)
}
}
}
if got := renderCoinFlip(&casino.CoinFlipSnapshot{Phase: casino.CoinFlipPhaseIdle}, "none", true); got != "" {
t.Fatalf("idle coin rendered %q", got)
}
}
|