Inspiration
Slot machines are built to feel random, but the House Edge simulator isn't memoryless. Each machine's next payout depends on its own recent spins, sometimes on combinations of two earlier spins, and sometimes on its neighbours. The rules also change mid-episode. The scoring rewards probabilities that are calibrated, so a model that only picks the most likely payout scores badly. We wanted to see how close a careful probabilistic model could get to the simulator's true odds using only the history it's allowed to see.
What it does
For every query machine, it predicts the probability of each next-spin payout $y \in {0, 1, 2, 5, 20}\times$. It reads only plays with tick $< t$ from the machine and its episode. Missing ticks are treated as unknown, never as zero.
How we built it
1. vommix: a Bayesian mixture of lag-context experts. Each expert $e$ conditions on the outcome (lose / win) at one lag $L \in {1,\dots,8}$ or at a pair of lags (all 28 pairs). It keeps exponentially decayed counts at three half-lives (16, 64, 128 ticks), shrunk toward a pooled prior. We replay each machine's history in order and update the expert weights by their predictive likelihood:
$$ w_e \leftarrow \frac{w_e \, P_e(y_t \mid \text{ticks} < t)^{\eta}}{\sum_{e'} w_{e'} \, P_{e'}(y_t \mid \text{ticks} < t)^{\eta}} $$
The forecast is the weighted average $\hat p = \sum_e w_e P_e$. Every machine discovers its own lag rule, and nothing about which lags matter is hard-coded. The starting prior over experts is learned from each round's training data, so the same code adapts when round two changes the relationships.
2. vomboost: gradient-boosted residuals. The mixture can't pool knowledge across machines, so we trained XGBoost on every observed training tick (~128k rows). Its base margin is the mixture's log-probabilities, so the trees learn only a correction:
$$ \log \tilde p_k = \log \hat p_k + f_k(\mathbf{x}), \qquad \tilde p = \mathrm{softmax}(\log \tilde p) $$
The features cover calibration (history length, how concentrated the posterior is), change detection (recent minus long-run surprise, for the machine and its whole episode) and the neighbouring machines' recent outcomes. Mixture features are cross-fitted across episodes, and the number of trees is chosen by early stopping on held-out training episodes.
3. A standard-library entry. XGBoost is needed only for training. We export the trees into the checkpoint and walk them in pure Python using XGBoost's float32 split rule. The submitted predict.py needs nothing beyond the standard library, and our packaging script reruns it from a fresh folder and requires the CSV to match within $10^{-8}$.
Results (validation)
| Model | Main round (/60) | Adaptation round (/20) |
|---|---|---|
| Best kit baseline | 30.8 | – |
vommix |
49.4 | 16.6 |
vomboost |
53.9 | 18.0 |
A paired episode bootstrap put the main-round gain over vommix at +4.5 points (95% CI [3.9, 5.1]), and all 400 resamples were positive. The gain held in all four conditions (stable, short history, drift, shared change), in both halves of validation, and on synthetic rounds we generated with different lag rules. For the final adaptation entry we averaged five boosters trained on train + validation.
Challenges
- Leakage. With neighbour and episode features it's easy to peek at the future. Every feature is computed only from ticks before the prediction tick, and unit tests check this.
- Not overfitting to one dataset. Round two was designed to change which relationships recur. We pre-registered our selection rules (tune on one half of validation, confirm on the other) and checked every change on synthetic kits, so we wouldn't tune lag pairs to the main round.
- Short history. We first thought machines with little history were at an information limit. The trees cut that condition's loss ratio from 0.29 to 0.16 by pooling calibration across machines, so we were wrong.
- Reproducibility limits. The rerun environment has no network, 30 minutes and 2 cores, which led us to the pure-Python tree evaluator instead of shipping XGBoost.
- Time. The adaptation round was only 90 minutes, so the whole retrain → package → verify pipeline was scripted in advance.
What we learned
- On this problem, a principled Bayesian model plus a flexible residual learner beat both used alone. Linear stacking added nothing; the trees helped because they pooled calibration and detected changes.
- Proper scoring rules reward honesty: averaging experts beat choosing one.
- Validation discipline (a held-out audit half, synthetic rounds, bootstrap intervals) mattered as much as the model.
Built with
Python (standard library), XGBoost, NumPy.