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
|
package game
import (
"strings"
"testing"
"thehouseoficarus/internal/casino"
"thehouseoficarus/internal/color"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
)
func TestCasinoCommandsWrapBeforeSessionWithoutLosingIndent(t *testing.T) {
commands := []casino.MenuCommand{
{Command: "bet <amount> on <target>", Desc: "Place bets until the dice roll; table rules lists targets"},
{Command: "roll / autoroll / stop", Desc: "Shoot / repeat on your turn / return to manual play"},
{Command: "unbet <target> / unbet all", Desc: "Remove one bet / every optional bet"},
{Command: "table", Desc: "View the felt"},
{Command: "insurance", Desc: "Buy insurance; a deliberately long description which requires a hanging continuation line"},
{Command: "decline", Desc: "Continue without insurance (also the timeout default)"},
{Command: "bet <amount> on player/banker/tie/player_pair/banker_pair", Desc: "One wager per round; 'on' is optional"},
{Command: "option casino_display compact", Desc: "Compact action hints (detailed for full command menus)"},
{Command: "spin", Desc: "Spin using your current wager"},
{Command: "autospin", Desc: "Start automatic betting"},
{Command: "unicode", Desc: strings.Repeat("é", 100)},
}
for _, wrap := range []int{0, 40, 60, 80, 120} {
p := player.New("a")
p.Options = map[string]any{"wrap_width": wrap}
conn := &casinoTestConn{}
sess := &net.Session{Player: p, Conn: conn}
g := &Game{}
text := g.casinoMenuCommands(sess, commands)
sess.WriteLine(text)
limit := 80
if wrap > 0 {
limit = min(limit, wrap)
}
for _, line := range strings.Split(strings.ReplaceAll(plainRoulette(string(conn.data)), "\r\n", "\n"), "\n") {
if line == "" {
continue
}
if color.VisibleLen(line) > limit {
t.Fatalf("wrap %d overlong %d: %q", wrap, color.VisibleLen(line), line)
}
if !strings.HasPrefix(line, " ") {
t.Fatalf("wrap %d lost indent: %q", wrap, line)
}
}
expected := strings.ReplaceAll(plainRoulette(text), "\r\n", "\n")
actual := strings.TrimRight(strings.ReplaceAll(plainRoulette(string(conn.data)), "\r\n", "\n"), "\n")
if expected != actual {
t.Fatalf("session reflowed prewrapped menu at %d", wrap)
}
}
}
func TestCasinoCommandsHangingIndentAndShortDescription(t *testing.T) {
sess := &net.Session{Player: player.New("a")}
g := &Game{}
commands := []casino.MenuCommand{
{Command: "bet <amount> on <target>", Desc: "Place a wager; targets: table rules"},
{Command: "table", Desc: "View the felt"},
}
text := plainRoulette(g.casinoMenuCommands(sess, commands))
lines := strings.Split(text, "\n")
if len(lines) != 2 {
t.Fatal("short commands should not wrap", text)
}
col := strings.Index(lines[0], "Place")
if col != strings.Index(lines[1], "View") {
t.Fatal("descriptions not aligned", text)
}
commands[0].Desc = strings.Repeat("long description ", 10)
text = plainRoulette(g.casinoMenuCommands(sess, commands))
lines = strings.Split(text, "\n")
for _, line := range lines[1 : len(lines)-1] {
if !strings.HasPrefix(line, strings.Repeat(" ", col)) {
t.Fatal("missing hanging indent", text)
}
}
}
func TestCasinoCompactHintsUseOnlyAvailableCommands(t *testing.T) {
p := player.New("a")
p.Options = map[string]any{"casino_display": "compact"}
p.Credits = 10
sess := &net.Session{Player: p}
g := &Game{}
menu := &casino.MenuView{Heading: "Your turn", Commands: []casino.MenuCommand{
{Command: "hit", Desc: "Take a card"}, {Command: "stand", Desc: "Hold"}, {Command: "double", Cost: 20},
}}
text := plainRoulette(g.casinoPrivateMessage(sess, "casino_menu", menu, ""))
if strings.Contains(text, "double") || !strings.Contains(text, " h:hit") || !strings.Contains(text, " st:stand") {
t.Fatal(text)
}
menu.ShowBalance = true
menu.Repeat = true
menu.Commands = []casino.MenuCommand{{Command: "table", Desc: "View the felt"}}
text = plainRoulette(g.casinoPrivateMessage(sess, "casino_menu", menu, ""))
if strings.Contains(text, "bet") || strings.Contains(text, "sitout") {
t.Fatal("compact mode invented actions", text)
}
}
func TestSimplifiedFeltSpacingAndNoIdlePrompt(t *testing.T) {
text := renderCrapsSnapshot(&casino.CrapsSnapshot{Variant: "simplified"}, "none", true)
for _, want := range []string{"DICE TOTAL", "NET PROFIT", "3 or 4", "1:1 (even money)", "10 or 11", "LOSE STAKE", "ONE ROLL", "stake back"} {
if !strings.Contains(text, want) {
t.Fatalf("missing %q: %s", want, text)
}
}
if strings.Contains(text, "WINS PAY 1:1") || strings.Contains(text, "COME-OUT") {
t.Fatal("stale simplified rules", text)
}
if strings.Contains(text, "Place a bet, then roll when ready.") {
t.Fatal(text)
}
rows := strings.Split(text, "\n")
for i, row := range rows {
if strings.Contains(row, "PASS ONLY") {
if i+1 >= len(rows) || strings.Trim(rows[i+1], "│ ") != "" || !strings.HasPrefix(rows[i+2], "└") {
t.Fatal("expected exactly one blank footer row", text)
}
return
}
}
t.Fatal("missing pass label")
}
|