# 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). Three views share one renderer: - **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 rounded panel listing each call bet's chips and wheel-order number series, plus the Neighbors syntax. - **Spinning**: a stylised 17x9 wheel (wider than tall so it reads as a circle in a terminal cell grid). Only the rim is colored, as solid red and black pockets alternating around the circumference; 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 its two physical wheel neighbours 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 odds reminder (`Numbers 35:1 ...`) is not part of the ambient board. It is shown only by the `table` command (and `table rules`). 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, Zero Spiel) 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, Zero Spiel 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.