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.
| Bet | Covers | Pays |
|---|---|---|
| Straight up | 1 number | 35 to 1 |
| Split | 2 | 17 to 1 |
| Street / trio | 3 | 11 to 1 |
| Corner / first four | 4 | 8 to 1 |
| Six line | 6 | 5 to 1 |
| Column / dozen | 12 | 2 to 1 |
| Red, black, odd, even, high, low | 18 | 1 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
- Never couple the ball’s release angle to the previous result. Counter-intuitively, full coupling is safe, because it behaves like a constant. It is the partial blend that leaves a weak correlation, and it accumulates.
- Release height alone is a strong enough randomiser to rescue an otherwise deterministic table.
- The dealer’s pull-back on the rotor is belt-and-braces, not load-bearing.
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
- Bake a positive control into every statistical test you write. Ours caught two bugs in the test itself, and either would have produced a confident, wrong answer.
- Know the sample size your test needs before you read its verdict. Chi-squared over 37 bins needs roughly 185 observations. Below that it is wrong in both directions, and it looks authoritative while being wrong.
- Uniformity is not randomness. Test the order, and include a lag test — seven of our eight sequence tests missed both realistic failure modes.
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.
- “Proving a physics roulette wheel is fair, and the two tests that lied about it.” Second Chance devlog, 4 August 2026.
- Project source,
ArtSource/roulette_bets.pyand its generatedroulette_bets.json. - 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.
- Project source,
ArtSource/roulette_fairness_page.py.
Elsewhere
- Last Tenner Bets — the shop itself.
- Making the snooker real — the same treatment for the snooker table at The Baulk Line.
- How to win at rock paper scissors — the evidence, and the prediction engine behind Jack.
- The devlog — the day-to-day version.