Monte Carlo Tree Search (MCTS)
A planning method that grows a lopsided lookahead tree by playing thousands of fast pretend games, digging deeper where wins pile up and dropping lines that lose.
Why Does This Exist?
Some games are too big to search the normal way. Go has around legal positions, so breadth first search drowns in branches. Minimax needs a hand written score for every middle position, and nobody could write a good one for Go for decades. Pure random playouts need no scoring but blunder, because they never look one move ahead on purpose.
Monte Carlo Tree Search, from Kocsis and Szepesvari (2006), takes the middle path. It grows a search tree that is deliberately lopsided: deep along lines that keep winning, shallow where losses pile up. Each round it walks down the tree with a bandit formula called UCT, adds one new node, plays one fast random game from there, and backs the result up the path. No hand written evaluation, no uniform expansion. That recipe became the planning core of AlphaGo, so the details here pay off directly in MuZero and AlphaZero.
The one idea you need first
This page leans on the multi-armed bandit: one state, several levers, unknown payouts, and the tension between pulling the current best and testing the rest. UCT is the bandit rule UCB1 applied at every tree node. If bandits are new, read that page first, then come back.
Think of It Like This
Mapping caves with one spool of rope
You lead a caving club with a single spool of rope and a weekend. Some passages look promising from the entrance, most are unknown. Your rule: each trip, walk down the passages with the best mix of past finds and least exploration, tie off one step of new rope at the frontier, then run ahead with a flashlight as far and fast as you can and report back good or bad. Chalk marks at each junction record visits and finds.
By Sunday the map is lopsided: the rich galleries have long rope lines, dead ends have one chalk mark each. Rope is compute, chalk marks are the visit counts and win rates , and the mix of finds plus novelty is the UCT score.
Where it stops: cavers use gut feel for the mix, while MCTS uses a fixed logarithmic formula with a constant , and cave reports are stories whereas playout results are numbers that average cleanly.
How It Actually Works
Nodes are states , edges are actions . Every edge stores two numbers: , how many times it was tried, and , the average result after trying it. One iteration runs four phases.
Select, expand, roll out, back up
Select. From the root, repeatedly pick the action with the highest UCT score:
exploits what won before. The square root term explores: grows with total parent visits while in the denominator shrinks the bonus each time the action is tried. The constant sets the trade. With rewards in , theory says . An untried action gets infinite score, so everything is tried once first.
Expand. At the first node with an untried action, or a terminal node, stop. If it is not terminal, add one child for an untried action with and .
Roll out. From the new node, play to the end with a fast default policy, often uniform random. No tree, no learning here, just one terminal reward , such as for a win.
Back up. Walk the recorded path upward. For each edge, add one visit and fold into the running average:
When the budget of iterations or milliseconds runs out, play the action with the most visits, , not the highest . Visits already blend value with confidence, which is why they survive noisy playouts.
Worked example
Root state with two actions, , parent visits . Action has and . Action has and . Since :
Selection picks : worse record, far less tested, bigger uncertainty bonus. Expansion adds a child under . The rollout wins, . Backup updates the edge from totals over visits to over :
Next round starts with at instead of , so search leans into the line that just paid off. The diagram above draws exactly these numbers.
Code
This snippet replays the worked example: score both actions with UCT, then back up the win. It uses only the standard library.
import math
def uct(q: float, n_child: int, n_parent: int, c: float = math.sqrt(2)) -> float: """UCT score for one edge. Untried edges score infinite.""" if n_child == 0: return float("inf") return q + c * math.sqrt(math.log(n_parent) / n_child)
def back_up(q: float, n: int, reward: float) -> tuple[float, int]: """Fold one terminal reward into the running average.""" n_new = n + 1 return q + (reward - q) / n_new, n_new
s_a1 = uct(0.60, 7, 10)s_a2 = uct(0.40, 3, 10)print(f"UCT(a1)={s_a1:.4f} UCT(a2)={s_a2:.4f}") # -> UCT(a1)=1.4111 UCT(a2)=1.6390q_new, n_new = back_up(0.40, 3, 1.0)print(f"Q(a2)={q_new:.4f} N={n_new}") # -> Q(a2)=0.5500 N=4The printed scores match the hand computation: against , and the win moves to .
Watch Out For
Playing the highest Q instead of the most visited move
After search, picking feels natural and is wrong. A line tried once that lucked into a win sits at with , while the genuinely strong line sits at with . Playing max walks into the fragile line. Playing max picks the line UCT kept returning to, which is the whole point of the search. Symptom: occasional shocking blunders from long searches. Fix: decide by visit count, always.
Setting the exploration constant without thinking
With selection is pure greed and the tree freezes inside whatever looked good early. With far above on rewards, bonuses swamp values and search spreads uniformly, burning the budget on hopeless branches. Symptom: search depth never builds, or the same weak line repeats forever. Fix: keep for win rate rewards, and scale with the reward range when rewards live elsewhere.
The Quick Version
- MCTS plans by repeating four steps: select with UCT, expand one node, roll out randomly, back up the result.
- UCT adds an exploration bonus to each win rate, so untried and stale lines get revisited on schedule.
- The tree grows lopsided: deep where wins pile up, shallow where losses do, which is how it survives huge branching factors.
- Leaf positions need no hand written score; fast random playouts supply the signal instead.
- The played move is the most visited one, because visit counts already combine value with confidence.