12ga Roulette — How it works
A short, honest walkthrough of the game, the math, and the fairness model. The aim was to remove the degenerate "spam self-shot and skip" exploit from the earlier prototype and replace it with a clean cash-out curve where every action is fair-EV and the multiplier ceiling is genuinely high.
§The round
A round is a single sealed cylinder of shells. The server commits to the layout up front (by publishing a SHA-256 hash of its secret seed), you choose your client seed, and you play actions one shell at a time. When the round ends, the server reveals the full seed and your browser can replay the whole shuffle locally to confirm it was honest.
- Volatility picks the (total shells, live count) range. Higher volatility = more shells = bigger ceiling.
- Client seed is yours. Same seed + nonce + server seed always produces the same chamber.
- Nonce increments each round so seeds can be reused across rounds.
| Volatility | Total shells | Live shells | Max blanks in a row |
|---|---|---|---|
| Easy | 3–5 | 1 | 2–4 |
| Medium | 5–8 | 1–2 | 3–7 |
| High | 8–12 | 2–4 | 4–10 |
§Three actions
1. Stack (self-shot)
You bet the next shell is blank.
- Blank: your multiplier grows by
total_remaining / blanks_remaining. The chamber advances. You keep playing. - Live: bust. Round ends with payout 0.
2. Press (fire at opponent)
You bet the next shell is live.
- Live: cash out at
multiplier × (total_remaining / live_remaining) × RTP. Round ends. - Blank: bust. Round ends with payout 0.
3. Cash out
End the round immediately, take multiplier × RTP as payout. Available at any time.
§Worked example: a 12-shell / 2-live chamber
Starting state: 12 shells remaining, 2 live, 10 blanks. Bet = 1. Multiplier = 1.00. RTP = 0.97.
| Action # | Total left | Blanks left | Stack factor | Multiplier after |
|---|---|---|---|---|
| 1 (stack blank) | 12 → 11 | 10 → 9 | ×1.200 | 1.20 |
| 2 (stack blank) | 11 → 10 | 9 → 8 | ×1.222 | 1.47 |
| 3 (stack blank) | 10 → 9 | 8 → 7 | ×1.250 | 1.83 |
| 4 (stack blank) | 9 → 8 | 7 → 6 | ×1.286 | 2.36 |
| 5 (stack blank) | 8 → 7 | 6 → 5 | ×1.333 | 3.14 |
| 6 (stack blank) | 7 → 6 | 5 → 4 | ×1.400 | 4.40 |
| 7 (stack blank) | 6 → 5 | 4 → 3 | ×1.500 | 6.60 |
| 8 (stack blank) | 5 → 4 | 3 → 2 | ×1.667 | 11.00 |
| 9 (stack blank) | 4 → 3 | 2 → 1 | ×2.000 | 22.00 |
| 10 (stack blank) | 3 → 2 | 1 → 0 | ×3.000 | 66.00 |
At this point 2 shells remain, both live, and the multiplier is 66.00. Stacking is no longer possible (zero blanks). Press wins guaranteed: payout = 66.00 × (2 / 2) × 0.97 = ×64.02. Or cash out for ×64.02 directly.
What's the probability of reaching that point? It's 10 / 12 × 9 / 11 × … × 1 / 3 = 1/66. Multiply by the payout 64.02 and you get 0.970 — exactly the RTP. Fair across the whole sequence.
§Provable fairness
Commit / reveal
- Server generates a secret 32-byte seed.
- Server publishes
SHA-256(server_seed)at the start of the round. This is the commit — it can't be changed once you see it. - You play actions; chamber order is determined but hidden.
- Round ends. Server reveals the full
server_seed. - Your browser hashes the revealed seed and confirms it equals the commit. If not, the server cheated.
Deterministic shuffle (HMAC-SHA256 over Fisher–Yates)
For each random draw, the server (and your verifier) computes:
digest = HMAC-SHA256(
key = server_seed,
msg = client_seed + ":" + nonce + ":" + label
)
chunk = first 4 bytes of digest as big-endian uint32
result = low + (chunk % (high - low + 1))
The labels used by a round are:
"total"— pick the total shell count from the volatility range."live"— pick how many of those shells are live."shuffle-0","shuffle-1", … — one per Fisher–Yates swap. Each draws an indexj ∈ [0, i]and swapsa[i]witha[j].
The in-browser verifier
The demo's "Verify in browser" button uses the Web Crypto API (crypto.subtle.sign + crypto.subtle.digest) to:
- Recompute
SHA-256(revealed server seed)and compare to the published commit. - Replay every HMAC-SHA256 step, label by label, with the exact same inputs.
- Reproduce the initial
[1,1,…,0,0,…]ordering and run Fisher–Yates locally. - Compare the resulting shells, byte by byte, to what the server reported.
The trace panel shows every digest, every chunk % modulus = j, and every swap so you can sanity-check the math. Everything runs in your browser; no server calls during verification.
§What this is for
This is a proof-of-concept for a future Stake Engine slot port. The on-site version uses Flask and free-play credits only — there is no real money loop here, just the fairness primitives and the gameplay shape. The reusable pieces (HMAC RNG, Fisher–Yates over a typed shell array, fair-odds multiplier math) live in repos_priority/stakeengine/12ga_roulette/.