From 1082f3336085c15992ca83d5553c0ba52208d70a Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Thu, 1 Oct 2026 14:38:08 -0400 Subject: feat: pai gow poker added to the casino --- dev_guide/roulette.md | 164 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 dev_guide/roulette.md (limited to 'dev_guide/roulette.md') diff --git a/dev_guide/roulette.md b/dev_guide/roulette.md new file mode 100644 index 0000000..e3794e2 --- /dev/null +++ b/dev_guide/roulette.md @@ -0,0 +1,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 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. -- cgit v1.2.3