aboutsummaryrefslogtreecommitdiff
path: root/dev_guide/roulette.md
diff options
context:
space:
mode:
Diffstat (limited to 'dev_guide/roulette.md')
-rw-r--r--dev_guide/roulette.md164
1 files changed, 164 insertions, 0 deletions
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 <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.