Skip to content

AlphaZero

BetaWorks, but under active development — APIs and results may shift. Tic-Tac-Toe and Connect Four are exercised; Go and Chess far less so.

AlphaZero searches a known simulator with MCTS, guided by a network that supplies a policy prior and a value estimate. No dynamics model is learned — the rules are given.

from noeira.deep_agents.alphazero import AlphaZeroAgent
Tic-Tac-Toethe correctness gate
Connect Fourthe real target

Self-play generates its own training data, and there is no external reward signal beyond the game outcome:

  1. Play a game against yourself. At each move, run MCTS from the current position, using the network’s policy as a prior and its value in place of a rollout.
  2. Act on the search’s visit counts, with temperature for exploration early in the game.
  3. Store every position with its visit distribution and the eventual game result.
  4. Train the network: policy toward the visit counts, value toward the outcome.
  5. Repeat.

The self-improvement is the point. Search makes the policy better than the raw network; training on search results makes the network better; better priors make the next search stronger.

Why it works at all is worth one sentence: search is a policy improvement operator. The visit distribution after a few hundred simulations is a strictly better policy than the network’s raw prior, because it has actually looked ahead. So training the network toward its own search output is not circular — it is distilling a better policy back into the thing that produced it, and the loop climbs.

Best netfrozen — generatesevery self-play gameSelf-play · MCTSexpanded with the real rules —no learned dynamicsReplay(obs, π, z), one row perboard symmetryLearner netπ → visit countsv → game outcomeArenalearner vs best, full MCTS,both coloursprior + valueminibatchcandidatepromoteon a clear winLose the match andnothing moves: thelearner keeps training,the best keeps generating.
self-play over the real simulatortrainingpromotion gatefrozen

The loop above, run literally, has a failure mode: a learner that gets transiently worse still generates the next batch of self-play, and the poisoned data makes it worse still. Full AlphaZero closes that with a best/learner split.

A frozen best net generates every self-play game — all MCTS expansion, all priors and values. A separate learner trains on that data and touches nothing else. Every arena_every iterations the learner plays the best in the Arena, and only if it wins a clear majority of decisive games is it copied over the best and allowed to generate.

Three details in that gate matter:

The match runs at full MCTS strength, not argmax. The winner is going to be used as net-plus-search, so that is the condition it should be judged in. Argmax evaluation also never consults the value head, which is half of what search depends on.

Both colours, from random openings. Board games are not symmetric in practice, and a fixed opening measures one line rather than strength.

A clear majority, not a bare one. A 51% result over a small match is noise, and promoting on noise reintroduces exactly the drift the gate exists to stop.

train_arena runs this; plain train runs the ungated loop, which is faster and fine for Tic-Tac-Toe.

Board games have symmetry groups the network does not know about. Every recorded (obs, π) is replayed under each of them, with the scalar outcome z copied unchanged because it is symmetry-invariant.

This multiplies the effective data by the group order at no simulation cost, which is a large factor when self-play is the expensive part. AUG is a compile-time parameter; passing IdentityAugmenter turns it off.

Simple rule: if you have the rules, use AlphaZero.

AlphaZeroMuZero
Dynamicsthe real simulatorlearned latent model
Networksone (policy + value)three (h, g, f)
Reward modelnot neededlearned
Applies togames with known rulesanything

MuZero’s learned model is what lets it work on Atari, where “the rules” are not available. On a board game that flexibility is pure cost — a learned dynamics model can only approximate a simulator you already have exactly.

from noeira.deep_agents.alphazero import AlphaZeroAgent
var agent = AlphaZeroAgent[
"gpu", Env, Net,
N_ENVS=64, # self-play games stepped together
NUM_SIMS=500, # MCTS simulations per move
MAX_NODES=1024, # tree capacity — must exceed what NUM_SIMS can expand
BATCH=128,
CAP=1_000_000, # replay capacity, in augmented positions
MAX_TRAJ=42, # longest possible game
](ctx, lr=0.002)

The environment must satisfy GPUTwoPlayerDiscreteEnv & TwoPlayerDiscreteEnv, which is what lets MCTS expand nodes by calling the rules rather than a learned model.

MAX_NODES is the one that bites: it has to exceed the number of nodes NUM_SIMS can create, and the search does not grow the tree to fit.

NUM_SIMS is not a knob to economise on. Dropping Connect Four from 500 to 100 gives visibly weaker MCTS targets — and since those targets are the training signal, a cheap search does not just play worse, it teaches worse.

Terminal window
pixi run mojo run -I . examples/board_games/tictactoe_alphazero_v2_cpu.mojo
pixi run -e nvidia mojo run -I . examples/board_games/connect_four_alphazero_v2.mojo
pixi run -e nvidia mojo run -I . examples/board_games/connect_four_alphazero_gumbel.mojo

Tic-Tac-Toe is the correctness gate. The game is small enough to solve completely, so a correct implementation converges to perfect play and every self-play game draws. If yours does not, the bug is real and visible — do not move to Connect Four until it does.

Connect Four is the real target, and the game the GPU MCTS work was tuned against.

The Gumbel variant uses Gumbel top-k action selection, which is more sample-efficient at low simulation counts.

Do not raise BATCH_SIMS above 1 under PUCT. Selecting several leaves per round against a frozen tree distorts Connect Four visit-policy targets by up to 2× per action — and those targets are the training signal. It is gated behind UNSAFE_BATCHED=True for that reason; the Gumbel orchestrators are the safe way to batch. See the batching hazard.