# 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 on `, `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.