Reusable bot-development kit for the ETH Mathathon (May 16–17 2026) and similar algorithmic-game competitions. Goal: turn unfamiliar rules into a stable, testable bot — and a working stdio submission — as fast as possible.
| Module | Purpose |
|---|---|
core.py |
GameState, SimultaneousState, Simulator, SimultaneousSimulator, TimeBudget |
bots.py |
Random / Greedy / Minimax (α-β) / MinimaxBotTT (TT + iterative deepening + PV ordering) / BeamSearch / MCTS (opponent-aware) / MetaBot |
strategy.py |
RegretMatching · FictitiousPlay · ISMCTS (turn-based hidden info) · SimultaneousMCTSBot (decoupled-UCT for simultaneous-move games; determinize / evaluator / opponent-model hooks) · TitForTat · Grim · Pavlov · ε-Greedy bandit |
nash.py |
solve_zero_sum (LP via scipy, fictitious-play fallback) · NashMatrixBot |
tournament.py |
N-player round-robin — win-rate, avg score, multi-Elo · significance() (auto baseline = 1/seats) · error_report() (error messages without keep_results) |
parallel.py |
ParallelRoundRobin — drop-in process-pool round-robin (~3× speedup) |
tuning.py |
GridTuner for hyperparameter sweeps |
validate.py |
validate_adapter (mutation / hashability / legality / termination checks) · benchmark_bot (per-move latency: mean / p95 / worst / overruns) |
utils.py |
Iterative deepening, evaluator normalization / composition, legality guard |
io.py |
stdio submission loops — run_per_move_loop (one-line) · run_protocol_loop (handshake / multi-line / sentinel) · crash-safe fallback · transcript record + replay |
replay.py |
JSONL move-by-move logs |
| File | Game type | Players | Mode |
|---|---|---|---|
nim_game.py |
Combinatorial subtraction game | 2 | turn-based |
iterated_pd.py |
Iterated Prisoner's Dilemma | 2 | simultaneous |
colonel_blotto.py |
Allocate forces across battlefields | 2 | simultaneous |
beauty_contest.py |
Guess 2/3 of average | 4 | simultaneous |
auction.py |
First-price sealed-bid | 3 | simultaneous |
grid_pursuit.py |
Pursuit-evasion on a grid | 2 | turn-based |
kuhn_poker.py |
Kuhn poker (hidden info, ISMCTS smoke test) | 2 | turn-based + hidden info |
platform_submission_nim.py |
stdio submission template | 1 | live judge |
template_adapter.py |
copy-me scaffold — blank adapter + opening checklist | 2 | turn-based |
figgie.py |
Simplified Jane Street Figgie — belief tracking + decoupled-UCT search | 4 | simultaneous + hidden info |
figgie_submission.py |
Figgie stdio submission (bundles to one file) | 1 | live judge |
| File | Purpose |
|---|---|
bundle.py |
Single-file amalgamator — inlines mathathon_kit/ + examples/ into one self-contained .py for judges that accept only one file. |
Single-header mathathon.hpp with the same TimeBudget / RandomBot / GreedyBot /
MinimaxBot / MCTSBot interfaces and a run_per_move_loop helper, plus a
working Nim example. Use this when Python is too slow.
# Run any example
python examples/nim_game.py
python examples/iterated_pd.py
python examples/beauty_contest.py
# Submit a Python bot to the platform (stdio):
echo 21 | python examples/platform_submission_nim.py
# Bundle it into ONE self-contained file (for single-file judges):
python tools/bundle.py examples/platform_submission_nim.py -o submission.py
echo 21 | python submission.py # verify from outside the repo
# Run the test suite
python -m pytest tests/ -q
# Build & run the C++ skeleton
cd cpp && g++ -std=c++17 -O2 nim_example.cpp -o nim && ./nim --selftestFor a turn-based game implement GameState:
class MyState:
players = (0, 1)
@property
def current_player(self): ...
def legal_actions(self, player=None): ...
def apply(self, action) -> "MyState": ...
def is_terminal(self) -> bool: ...
def score(self, player) -> float: ...For a simultaneous-move game implement SimultaneousState (see
examples/iterated_pd.py):
class MyState:
players = (0, 1)
def active_players(self): ...
def legal_actions(self, player): ...
def apply_joint(self, actions: dict) -> "MyState": ...
def is_terminal(self) -> bool: ...
def score(self, player) -> float: ...Both interfaces work with RoundRobin (set simultaneous=True for the
joint-move version).
- First 30 min. Read the rules. Copy
examples/template_adapter.pyand fill in its TODO markers (it ships with an opening checklist), then runvalidate_adapter(make_state(0))— it catches the silent adapter bugs (mutation, unhashable state, illegal moves, non-termination) before they cost you games. Smoke-test withRandomBotself-play. Submit immediately if the platform allows early submissions — the legality-guarded random bot already won't crash. - Next 60 min. Write a domain heuristic. Run
GreedyBotagainstRandomBotround-robin to confirm the heuristic is signed correctly. - Choose the main engine:
- Turn-based, 2-player, deterministic, repeating positions →
MinimaxBotTT(5–20× faster than plain minimax). - Turn-based, 2-player, no repetition / unbounded depth →
IterativeDeepeningMinimaxorMinimaxBot. - Long horizon / large branching / stochastic →
MCTSBot. - Repeated matrix-style →
RegretMatchingBotorFictitiousPlayBot. - Pure 2-player zero-sum matrix →
solve_zero_sum(payoff_matrix)→NashMatrixBot(unexploitable mixed strategy). - Hidden information, turn-based →
ISMCTSBot(give it adeterminize()sampler). - Hidden information, simultaneous-move →
SimultaneousMCTSBotwith adeterminize()sampler.ISMCTSBotdoes not fit here — it needscurrent_player/apply, which aSimultaneousStatelacks. - Simultaneous-move, multi-round →
SimultaneousMCTSBot(decoupled-UCT). Add anevaluatorfor long games, anaction_filterfor big action sets, and anopponent_policyto switch from equilibrium search to exploiting a weak field. Seeexamples/figgie.py. - Simultaneous one-shot → enumerate-then-best-respond +
RegretMatchingBotover rounds.
- Turn-based, 2-player, deterministic, repeating positions →
- Tune. Use
GridTunerover your engine's parameters. Switch toParallelRoundRobinwhen sweeps get slow. - Stress test. Run
keep_results=Trueround-robins against the random bot and your earlier versions; inspect any games with errors > 0. Usereport.significance()to confirm a tweak's win-rate gain is real and not round-robin noise before you keep it. Runbenchmark_botto confirm your p95/worst move time stays under the judge's limit — one overrun forfeits a game. - Submit. Wrap your bot with
run_per_move_loop(one line in/out) orrun_protocol_loop(handshake / multi-line states / sentinel lines — build the reader fromread_line/read_until/count_prefixed_reader). Always passfallback=random_legal_fallback(parse_state)so a crashing engine emits a legal move instead of forfeiting. If the wire format surprises you, wrap stdin withrecord_stdin(...)and debug offline withreplay_transcript. If the judge accepts only one file, runpython tools/bundle.py your_bot.py -o submission.pyand verify the bundle from a directory outside the repo before uploading.
- Always return a legal action.
with_legality_guard(bot)and the simulator both enforce this defensively, but bake it into your bot too. - The evaluator is your bot. Greedy, Minimax, BeamSearch, and MCTS rollouts all bottleneck on it. Spend most of your time here.
- Optimize for the round-robin average, not just one opponent.
- Keep a
MetaBotstack — fall back from a clever strategy to a robust one if the clever one is exploitable or unstable. - Normalize the evaluator to roughly
[-1, 1]so MCTS' UCB constant (exploration=1.4) stays calibrated. Usenormalize_evaluator(...). - Log losing games and inspect the first bad move, not only the score.
mathathon_kit/ # core engine
examples/ # one adapter per archetype
tools/ # bundle.py — single-file submission amalgamator
cpp/ # C++17 single-header mirror
tests/ # pytest suite (69 tests, all green)
pyproject.toml
$ python -m pytest tests/ -q
..................................................................... [100%]
69 passed