aboutsummaryrefslogtreecommitdiff
path: root/building_guide/roulette.md
blob: e257b3163e697e73d5d1db9cbcf41ddf88fddc26 (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
# 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.