Inspiration
House Edge asked us to predict the next spin of simulated slot machines as five probabilities: 0×, 1×, 2×, 5× or 20×. The best "black-box" models can't recover machine state that is hidden, so we went the other way. We treated the logs as evidence about how the simulator actually works and built a model from what we found.
What it does
For every query, our model outputs the probability of each payout on the machine's next spin, using only the ticks before the cutoff. It works on all four test conditions: stable, short history, drift (a pattern change part-way through) and shared change. It runs in about 10 seconds on one CPU, uses only the Python standard library, and produces the same output every time.
How we built it
We worked out the structure of the simulator from the data, step by step:
- Win chance comes from a lag pair. Each machine looks at its own results from two earlier spins \(i\) and \(j\). The win chance jumps when the XOR of those two win/loss results lands on that machine's "hot" cell: $$\text{logit}\,P(\text{win}t) = b_m + \Delta{\text{type}}\cdot\mathbb{1}\big[\text{win}{t-i}\oplus\text{win}{t-j} = \text{hot}_m\big]$$ In the main round the pairs were (1,3), (2,5) and (4,7).
- Payout size has two regimes. Given a win, the mix of 1×, 2×, 5× and 20× payouts is one of two fixed vectors per machine type. Which one applies depends on whether the payout at lag \(i\) was 2× or more, flipped per machine. The regime also shifts the win chance.
- Changes are flips. In drift and shared-change episodes, the hot cell and the cold cell swap places once.
- Neighbours matter a little. Other machines' recent wins nudge the win chance up.
The final model is a Bayesian mixture over every hypothesis per machine: lag pair, no change or a flip at tick \(s\), which cell is hot, and the size orientation. Each hypothesis is weighted by its exact Beta-Binomial marginal likelihood. Empirical-Bayes priors are fitted on the training logs, and the output is the posterior-predictive distribution.
For round two we wrote an automatic adaptation pipeline. It re-discovers the lag pairs and size rule from the new data, refits every constant and tunes on validation. It also tries a generic fallback model and keeps it only if it scores better. It found the new pairs (1,4), (2,6) and (3,8) in about a minute.
Results (public validation)
| Round | Pooled starter | Ours | Max |
|---|---|---|---|
| Main | 30 | 58.43 | 60 |
| Adaptation | ~10 | 19.65 | 20 |
Main-round progression: 30 → 53.8 (lag-pair model) → 57.5 (size-regime rule) → 58.2 (empirical-Bayes priors, regime shift, cross-machine effects) → 58.4 (tuning).
Challenges we ran into
- Short histories. About 33 usable spins are often not enough to tell which lag pair a machine uses. This is where almost all of our remaining error is.
- Avoiding overfitting. We only kept a setting if it also improved predictions on held-out training outcomes.
- Reproducibility. Every submission was rebuilt from a fresh folder in a Docker
python:3.13container with no network, and matched our uploaded CSV to within \(10^{-13}\). - The 90-minute adaptation window. We had prepared the pipeline and a runbook in advance, so we only had to run it.
What we learned
Understanding how the data is generated beats generic model capacity. A small, exact Bayesian model of the real mechanism got within a few percent of the best possible score, and it adapted to a changed simulator automatically.
What's next
- Better pair identification for short histories.
- Pooling change-point evidence across machines in shared-change episodes.