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.
VolatilityTotal shellsLive shellsMax blanks in a row
Easy3–512–4
Medium5–81–23–7
High8–122–44–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.

Why these three are the same expected value Stack pays exactly the fair-odds inverse of the blank probability, and press pays exactly the fair-odds inverse of the live probability. Cash out is the no-variance baseline. All three have expected return = bet × RTP. The choice between them is pure variance preference — there is no exploit. (Old prototype: skip was free and timing-driven, so a perfect-information player could grind. Removed.)

§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 leftBlanks leftStack factorMultiplier after
1 (stack blank)12 → 1110 → 9×1.2001.20
2 (stack blank)11 → 109 → 8×1.2221.47
3 (stack blank)10 → 98 → 7×1.2501.83
4 (stack blank)9 → 87 → 6×1.2862.36
5 (stack blank)8 → 76 → 5×1.3333.14
6 (stack blank)7 → 65 → 4×1.4004.40
7 (stack blank)6 → 54 → 3×1.5006.60
8 (stack blank)5 → 43 → 2×1.66711.00
9 (stack blank)4 → 32 → 1×2.00022.00
10 (stack blank)3 → 21 → 0×3.00066.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

  1. Server generates a secret 32-byte seed.
  2. 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.
  3. You play actions; chamber order is determined but hidden.
  4. Round ends. Server reveals the full server_seed.
  5. 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 index j ∈ [0, i] and swaps a[i] with a[j].

The in-browser verifier

The demo's "Verify in browser" button uses the Web Crypto API (crypto.subtle.sign + crypto.subtle.digest) to:

  1. Recompute SHA-256(revealed server seed) and compare to the published commit.
  2. Replay every HMAC-SHA256 step, label by label, with the exact same inputs.
  3. Reproduce the initial [1,1,…,0,0,…] ordering and run Fisher–Yates locally.
  4. 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/.