AI Agents Are Training Us for the Azul Tournament
Blog post #45
A friend of mine has an Azul tournament in a few weeks. Three- and four-player games, real opponents, real prizes. He’s not bad but he’s not Stockfish-level either, and Azul has the kind of move-by-move score variance where a stronger opening usually decides the whole match. So we did the obvious thing: we built our own coach.

What changed since last log
This was a tangent from the day’s work — a few hours of unplanned engineering. But it produced a real artifact: a public web app where anyone can play Azul against bots and ask for per-move advice from a search algorithm.
The pitch was simple. Chess has Stockfish; Azul has nothing comparable. The game has a small branching factor (around 30–150 moves per turn), perfect-information within a round, and is only mildly stochastic between rounds. That’s MCTS territory.
What shipped
A complete coaching system, deployed at bltg85-azul-coach.hf.space (source on GitHub):
- Heuristic bot — hand-tuned weights for floor penalty, pattern-line progress, and end-game potential (row / column / colour-set bonuses). Wins 87% against the framework’s bundled naive bot. ~1 ms per move.
- MCTS bot — UCB selection on top of the heuristic as rollout policy. Pluggable iteration budget. Multi-player adapted with a per-player value vector and max-N backprop.
- Web UI — single-page Flask app. Click a tile group in a factory or the centre, click a destination on your wall. A side panel runs the coach on demand and shows the top-3 moves with visit counts and value estimates. Undo, seed control for replayability, per-game JSON logs.
We started on a vendored copy of michelleblom/AZUL — an RMIT teaching framework with a clean GameState / Player interface. Saved us a week of writing rules code.
What’s working
The bot strength curve lands where it should. We benched seven configurations against three heuristic baselines (10 games each, rotating seats to balance position):

Random and naive bots score essentially zero. The heuristic baseline against itself sits around 10–25% (high variance in 10 games; expected ~25% for symmetric play). MCTS scales with compute.
The interesting story is the iteration curve:

Log-linear from 100 to 1000 iterations, then superlinear from 1000 to 2000 — the jump from 30% to 60% win rate. Below ~1000 iters, UCB can’t reliably distinguish similar-quality moves; above that threshold, it starts converging on the actually-best line.
What’s unclear or broken
We oversold the early results. The first MCTS bench was 5 games — MCTS:500 won 60% — and we cheerfully announced “MCTS beats heuristic.” The follow-up 20-game bench made the honest picture clear: MCTS:500 lands at 25%, statistically indistinguishable from the heuristic baseline. The 5-game result was binomial noise.
It took real compute (MCTS:2000, 12 seconds per move, 60% win rate over 10 games) to actually demonstrate an edge. There’s a lesson there about claimed-versus-confirmed improvement that I’ll probably re-learn next month anyway.
Other rough edges encountered along the way:
- Bots favoured factories over centre takes in a way that felt unrealistic. Adding a small “centre denial” bonus to the heuristic fixed it. The bot can’t model opponents, so a static bonus is a cheap stand-in for the missing concept.
- Rollouts could hang on pathological game states where no player ever completed a wall row. Added a ply limit + forced end-of-game scoring.
- Memory ballooned during long benches until I realised the
PlayerTracestub I’d written for fast-cloning was accumulating across rollouts. A no-opStartRoundfixed it.
Decisions made
- No neural net. AlphaZero-style self-play would be weeks of GPU time for, at best, a marginal win against a well-tuned MCTS in a game this small. The cost-vs-value isn’t there for a coaching tool.
- The coach shows multiple candidates, not one. Pedagogically, “move A scores +14, move B scores +11, move C goes straight to floor” teaches you the trade-offs in a way “the bot says A” doesn’t.
- Deterministic seeds, undo button. You can replay specific positions, try a different move, watch how the future unfolds with the same opponents. This turns out to be more useful for actually getting better than chasing win rate.
Tooling & process
- Framework as scaffold, not as constraint. We vendored a copy of the upstream AZUL teaching framework and built around it. Could have forked, but a vendored copy keeps the boundary clean.
- Hugging Face Spaces for the deploy. Free, Docker SDK, ~16 GB RAM, runs full Flask without rewrites. Render was the first instinct but the user (rightly) preferred a path where everything could be done via CLI after the initial signup. HF + the
hfCLI +op runfor the token = end-to-end automation. - 1Password for the HF token. Per the global Claude Code config, secrets stay in
opand we pull them viaop run --env-file=.env -- <cmd>. The token never lands in a.envfile as plaintext or in shell history.
— Stefan