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
|
# Roulette and casino extension readiness
The casino has a usable extension seam: `tableGame`, generic `tableCore`,
registered constructors/config validation in `registry.go`, structured snapshots,
shared participation/control routing, and durable per-stake wallet handles.
Roulette uses those seams rather than adding another command dispatcher.
Example table (one game of each type per room remains the manager's rule):
```yaml
casino_tables:
- id: roulette_single
game: roulette
min_bet: 1
max_bet: 10000
betting_ticks: 15
roulette:
double_zero: false
spin_ticks: 4
```
`double_zero: true` selects the American 38-pocket wheel; otherwise the
European 37-pocket wheel is used. Spin ticks default to 3; betting ticks use
the shared default. Ticks follow the server's configured tick length.
The wheel order is authentic and the outcome is selected uniformly only when
spinning finishes.
## Presentation
Art is rendered per recipient in `internal/game/render_roulette.go`, so color
mode and Unicode preference stay per player (the engine snapshot never embeds
ANSI). The views and betting tracker share one snapshot renderer:
- **Tracker**: a five-line, 74-column strip above the betting board, rendered
by `render_roulette_tracker.go`. It shows the latest eight hits (newest first),
three hot/cold pockets with hit counts, and red/black/zero percentages.
All stats use a table-local rolling window of 50 completed spins. Zeros are
included in the denominator; ties use numeric order (0, 00, 1, ...).
History is copied into snapshots, never sampled before settlement, and
survives round resets but not table replacement or server restart.
History is copied into snapshots, never sampled before settlement, and
survives round resets but not table replacement or server restart.
- **Snapshot cadence**: the board and tracker are drawn once per round by the
menu event (on join and after settlement, the "Next spin" menu) and by an
explicit `table`/`table rules`. Placing a wager only confirms the wager; it
does not re-send the board snapshot.
- **Betting board**: numbered grid with zeros in the left lane, red/black
pockets, and outside bets. The whole five-row zero lane (three numbered rows
plus the two separators between them) carries the green background, and its
outer corners are rounded in Unicode mode. A racetrack table draws
`renderRouletteRacetrack` above the board: a numbered oval in physical
clockwise wheel order, with 26 at the right tip. Interior dividers meet
each call bet's pockets, left to right: Tiers du Cylindre, Orphelins,
Voisins du Zero, Zero. Connected box-drawing steps separate Tiers from
Orphelins; Zero has a curved boundary within Voisins. Neighbors is not
drawn on the board, but remains in the payout reference and help.
- **Spinning**: a stylised 17x9 wheel (wider than tall so it reads as a circle
in a terminal cell grid). Only the rim is colored: solid red pockets alternate
with a very dark blue that stands in for black (`rouletteWheelBlack`), because
pure black vanishes on dark terminals. Pockets are two cells wide except the
top and bottom centres, which stay one cell. One pocket is green (plus its
diametrically opposite pocket on American wheels). A simple plus marks the
croupier's spindle. No pocket numbers are drawn; with color off the pockets
fall back to R/B/G so the wheel still reads.
- **Result**: the winning pocket plus two physical wheel neighbours on each
side (five cards) at playing-card size. The pocket number sits on the top row,
a rule separates it from the ball (one row above centre), and the color name
is spelled out on the bottom row only when color is off. There is no
surrounding prose. The smallest (five-row) cards drop the rule so the ball
can sit directly under the number. The wheel and result graphics each get a
blank line before and after so they do not crowd the prose.
The colored payout tables in `internal/game/render_casino_help.go` use the
shared ANSI-aware `ui.Table` renderer, respecting ASCII/Unicode and wrap width.
They show wheel-specific win chances and net profit odds, including call-bet
exceptions. They appear only with `table` and `table rules`, not ambient art.
`table help` opens the current game's guide; `help casino` indexes the brief
casino guides in `data/help/`. `help racetrack` explains chip placements and
rounding, including `zero`/`jeu zero` versus the straight-number target `0`.
Each table's one-line
overview is kept short (`Label | Limits 1-10000 | n/m seated | state`) so it
fits beside a look minimap; `writeLookSideBySide` also wraps any content line
that would otherwise run into the map column.
Initial scope is **one wager per player per spin**. A wager covers straight-up
numbers, outside bets (colors, parity, high/low, 1to12/13to24/25to36, rows),
and the combination bets: splits, streets, corners, six-lines, trios, and the
basket. Combination targets are a compact number list (`split 1 2`,
`street 1-2-3`, `corner 1 2 4 5`, `line 1 2 3 4 5 6`, `trio 0 00 2`,
`basket`) joined from the tokens after `on`; the leading name is optional
because the geometry of the covered set identifies the bet. Non-adjacent or
wrong-shaped sets are rejected rather than silently accepted. Multiple
simultaneous wagers remain future work: `Participant.Wager` still holds a
single stake.
Single-zero tables may set `roulette.racetrack: true`. That adds the four
French call bets (Voisins du Zéro, Tiers du Cylindre, Orphelins, Jeu Zero)
and the Neighbors bet (a chosen number plus four on each side in wheel order).
The racetrack is a single-zero layout only: the validator rejects
`racetrack: true` together with `double_zero: true`. A call bet is **one
wager** whose amount is the total stake spread evenly over the bet's chips
(Voisins 9, Tiers 6, Orphelins 5, Jeu Zero 4, Neighbors 9). Because the
components are ordinary inside bets, each returns `36/covered` when its
number hits, so a winning pocket can pay a different multiple of the stake
than another (for example 0 on Voisins pays 24/9, while 4 pays 18/9). The
engine therefore resolves a wager to exact per-pocket `num/den` shares
(`roulette_racetrack.go`) instead of a single multiplier; `bet` validates
every possible return before committing and `settle` floors
`stake*num/den`. The rack panel is drawn by `renderRouletteRacetrack` above
the board and lists each bet's wheel-order series.
All returns are checked before debiting a wager. Both winning and losing stakes
settle through `PayCredits(return, stakeID)`, including a zero return for losses.
Failures leave the stake pending and do not claim that payment succeeded.
Shutdown and eviction refund outstanding stakes and reset the round.
Disconnect refunds are safe because no partial outcome is sampled or exposed.
## Assessment and boundaries
This is a sound foundation for more testing-stage table games, not a blanket
production-readiness certification. New games must test payout math, overflow,
phase boundaries, disconnects, queued departure, refunds, persistence failure,
and spectator output. Shared card-game loss settlement still uses the void
`ResolveStakesByIDs` API: it preserves failed stakes for recovery but cannot
report that failure to its caller. Roulette avoids that limitation with the
existing error-returning payment API. Uniform per-stake error-returning
settlement would be a worthwhile follow-up for older games.
Multiple concurrent wagers need their own explicit ledger/limit model instead
of reusing the single `Participant.Wager`. Multiple same-game tables in one room
would also require changing current addressing/validation assumptions.
## Testing shortcut
`data/objects/casino_test_portal.yaml` is placed in starting room 1001.
`use casino` gives 1000 chips and teleports to vestibule 6100. It is intentionally
repeatable and uses ordinary trigger/drop/teleport machinery. Keep an inventory
slot free (ordinary trigger drops fall to the ground when inventory is full).
Remove the room's `casino_test_portal` reference before production: this grants
unlimited test funds and bypasses travel/progression. No account roles are needed.
Single-zero room 6182 and double-zero room 6183 are north of ground-floor
landing 6102. `help roulette` describes player syntax and supported bets.
|