Second Chance

Research

Proving a roulette wheel is fair

There is a bookmaker’s in Oakhaven called Last Tenner Bets, and it has a roulette table. The ball on it is a real rigid body rolling round a real bowl. Nothing picks the number. The rotor is turning, the ball is thrown, it loses speed, drops off the track, rattles across eight deflectors and the frets, and whichever pocket it settles in is the number that pays.

There is no “choose the result and animate towards it” path anywhere in the build. That was decided before any geometry existed, and it buys something no amount of polish can fake — a ball that occasionally hops the rim and lands on the carpet is obviously physical. It also creates a problem a random number generator does not have. A physical wheel can be biased, and you cannot tell by looking at it. With an RNG you read the code and you are done. With a wheel, the only honest answer is measurement.1

The wheel is a real European wheel

Thirty-seven pockets, single zero, in the true clockwise order — not the numbers 1 to 36 laid out tidily, which is what a wheel built from a loop counter looks like:2

0  32 15 19  4 21  2 25 17 34  6 27 13 36 11 30  8 23
10  5 24 16 33  1 20 14 31  9 22 18 29  7 28 12 35  3 26

That order is not decoration. On a real wheel it is the thing that makes sector betting meaningful and makes a biased wheel exploitable, and here it is also the same list that drives the rotor mesh — so pocket N physically carries the number the rules engine believes it carries. There is no mapping layer between the model and the maths to get out of step.

One source of truth for the board

The betting layout is generated, not drawn. A single file emits every bet’s coverage mask, its payout, its label and its anchor point on the cloth.2 157 bets, plus 41 called bets — the racetrack macros a real table carries: neighbours of zero, thirds of the wheel, orphans, zero game, and the neighbour bets around each individual number.

The important part is what consumes it. The board art generator and the rules engine read the same file. The green cloth you see printed and the invisible hitboxes you click are produced from one list, so the picture and the payout can never drift apart — which is the failure every hand-built betting layout eventually has, and it is silent when it happens.

The payouts, as a UK single-zero table pays them
BetCoversPays
Straight up1 number35 to 1
Split217 to 1
Street / trio311 to 1
Corner / first four48 to 1
Six line65 to 1
Column / dozen122 to 1
Red, black, odd, even, high, low181 to 1

It is a UK table, and two decisions follow from that. All the labelling is English rather than French. And the even-money bets lose to zero — no la partage, no en prison. That is the harsher of the two conventions and it is the right one for a Midlands bookmaker.

The invariant that guards the payouts

Here is the thing that makes the whole betting layout testable in one line. On a single-zero wheel, every bet — straight up, split, corner, dozen, column, red — has exactly the same expected value:

−1 / 37  =  −2.7027%

Whatever you back. A payout table wrong by a single unit breaks that immediately, and it breaks it in a way that is invisible to playtesting. So the invariant is asserted across all 157 bets — and not only in Python. A verification script drives the compiled Blueprint graphs directly, running 5,809 win checks, 1,570 return checks and 74 wheel lookups, and computes the house edge through the Blueprint’s own arithmetic rather than through a copy of it. It is the strongest test on the table and it costs nothing to run.1

Fairness has to be measured

A physics wheel goes biased through its geometry and its launch conditions, so the design was tested before the wheel was built, in an offline simulator.

A test that cannot fail proves nothing. Every case runs alongside positive controls designed to be biased, and the controls have to fail or the pass is worthless. The strongest is a completely fixed launch: a chi-squared of 1.9 million, with only the eight deflector positions reachable at all.

Baking those in caught two bugs in the test code itself, both of which would have produced confident nonsense. The sector check was comparing eight arcs against n/8 — but 37 pockets do not divide into eight arcs evenly, they split 5, 5, 4, 5, 5, 4, 5, 4, so everything looked clustered including the controls. And the sensitivity sweep had left the release angle randomised, so it was not testing pinched ranges at all; it was measuring nothing and reporting a pass. In both cases the control being wrong in the same direction as the data is what exposed it.

Uniform is not random

The sequence 0, 1, 2, 3 … 36, 0, 1, 2 … has a perfect chi-squared and is perfectly predictable. Uniformity of the histogram says nothing about the order the numbers arrive in, and a player who spots an order has beaten your wheel.

So there is a second battery: eight tests over 400,000 spins each. Repeat rate; lag-k step uniformity for k = 1 to 40; the full 37 × 37 table of consecutive pairs; Wald–Wolfowitz runs on red/black and odd/even;3 gap distribution; compressibility; and block stability across a session.

Crucially it runs against a model where the rotor carries its phase from one spin to the next, the way a real table does — because that carry-over is the only genuine spin-to-spin memory in the system. Test a model without it and the spins are independent by construction, and the whole battery is circular.

Both realistic failure modes slipped past seven of the eight tests. Uniformity, pairs, runs, gaps and compression all looked perfectly clean. Only the lag-k step test caught them, and in one case only around lag 26.

Writing the obvious five tests would have shipped a predictable wheel with a green report to prove it.

Three rules that fell out of the controls

What is deliberately imperfect

About 2% of spins are voided — escapes, plus the occasional ball that never settles, measured over 179 spins on the final geometry. That is a feature and it is staying. A perfectly contained ball reads as fake; a void spin leaves the bets standing and re-opens betting, and it gives the dealer something to do, because he walks over and picks the ball up.

Chasing zero would mean narrowing the launch ranges, and the sensitivity sweep says that is precisely how the wheel becomes biased. The soak reporter flags anything above 5% as a problem, which is the right bar.

What is not proven

The in-engine distribution has not yet earned a verdict, and we are not going to pretend otherwise. The offline proofs are strong and the rules engine is verified against the compiled graphs. But the fairness clock was reset to zero the day a mirrored-numbering bug was fixed, and a chi-squared test over 37 bins refuses to speak below about 185 settled spins.

So the correct claim is “the design is proven fair, the wheel is instrumented, and the wheel’s own data is still accumulating”not “the wheel is proven fair”. One of those is a measurement and the other is a hope.

Every settled spin the table has ever produced is logged and accumulates across sessions, so the verdict builds itself over time rather than being re-rolled whenever the editor restarts. The histogram is kept in wheel order rather than numeric order, for a specific reason: a physics wheel biases by sector, so adjacent pockets cluster — and sorting 0 to 36 scatters exactly those neighbours across the chart and hides the failure mode you are looking for.4

Three things worth stealing

References

The roulette work was an engineering and statistical exercise rather than a literature one, so these are the project’s own sources and the named tests used. Nothing here cites a document that was not read.

  1. “Proving a physics roulette wheel is fair, and the two tests that lied about it.” Second Chance devlog, 4 August 2026. The full account, including the traps, the soak and the payout verification through the compiled Blueprint graphs. Devlog index.
  2. Project source, ArtSource/roulette_bets.py and its generated roulette_bets.json. The single source of truth: wheel order, red/black sets, layout geometry, all 157 bets with coverage masks and anchors, the payout table and the 41 called-bet macros. Self-tests before it writes. Consumed by both the board art generator and the rules engine.
  3. Wald, A., and Wolfowitz, J. “On a test whether two samples are from the same population.” Annals of Mathematical Statistics, 11(2), 1940, pp. 147–162. The runs test, applied here to the red/black and odd/even sequences.
  4. Project source, ArtSource/roulette_fairness_page.py. The fairness dashboard. Scans the current and rotated logs so spins accumulate across sessions, and plots the histogram in wheel order rather than numeric order.

Elsewhere

Join the list