aboutsummaryrefslogtreecommitdiff
path: root/building_guide/roulette.md
blob: e3794e2e310d3900f05dad68ae1bb17274344ec9 (plain)
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
163
164
# 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`. The betting snapshot leads with a blank line
  so the tracker is separated from the prose or menu that precedes it. 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
  betting menu (on join and after settlement) 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. Labels are centered on the middle row; a label too
  wide to leave a blank cell against its dividers wraps onto the rows above
  and below it (e.g. "Tiers du" / "Cylindre"), and only the glyphs are
  written so the centering padding never paints over the dividers. 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.
- **Menu ordering**: the roulette betting menu prints its balance and
  heading, then the tracker and board, and only then the wager reminder
  (`bet <amount> on <target>`, `bet / rebet`, `table / sitout / stop`) so the
  instructions follow the table they describe. Other games keep their menu
  above the snapshot.
- **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.

Settlements are rendered by `settlementText` in `internal/casino/table_ux.go`:
a colored win/loss/push headline gives the round's net once (the old
"Net ... | ... returned (stake included)" split is gone), then each named bet
— a blackjack hand, insurance, or the table's spot — is listed with its own
net. Wins are green, losses red, pushes white, and a natural keeps the jackpot
gradient. The engine emits inline color tags (`{casino_result_win}WIN{/}`),
which `Game.casinoProse` expands per recipient against the session palette, so
the engine still never embeds ANSI. Casino prose defaults to white
(`casino_action`), leaving color for emphasized outcomes.

## 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.