Search and Belief-Space Planning › Tree Search and Monte Carlo Tree Search
World Models HandbookAll chapters Search and Belief-Space Planning › Tree Search and Monte Carlo Tree SearchOne section · Sections in this chapter2 / 13 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Before we can talk about searching over beliefs, we need the mechanics of searching over states. This section builds MCTS from the ground up: what problem it solves, why sampling is a reasonable answer, the four phases of the algorithm, the bandit theory underneath it, and the practical details that separate a working implementation from a minimal one. Even if you never implement MCTS yourself, the concepts here recur throughout the rest of the book: the difference between tree and default policies, the role of exploration bonuses, and the meaning of a backpropagated return. The belief-space algorithm later reuses MCTS selection, simulation, and backup, and adds history-indexed observation branching plus latent-state sampling from the current belief.
The Search Problem
Recall from Markov and Partially Observable Decision Processes that a Markov decision process is a tuple , where is the state space, the action space, the transition kernel, the reward, and the discount factor. For the finite discrete case used below, the optimal value function satisfies the Bellman optimality equation:
where:
- : the maximum expected discounted return obtainable from state
- : the expected immediate reward for taking action in state
- : the probability of successor state under the transition model
- : the discount factor
If you can evaluate the right-hand side exactly, you can solve for by dynamic programming, as we did for the grid world in Classical Planning and Optimal Control. An exact tabular solution requires enumerating and iterating until convergence, and both scale badly. For factored state variables, the number of joint assignments grows exponentially; a few dozen binary features already yield billions of states, while a continuous space cannot be tabulated at all. But there is a second way to use the Bellman equation: not to solve for the whole function , but to answer a single question about a single state. Given a state , which action maximizes the right-hand side?
That reframing is the key move. You do not need to know everywhere. You need to know it at the states that arise from your current situation, and ideally only at those. Everything MCTS does follows from taking that observation seriously. The value function is a book with pages for every state in the world, most of which you will never read; MCTS is a procedure for reading only the pages you need. It is guided by the assumption that you do not need many of them.
The naive way to exploit it is a depth-limited expectimax tree. From the root state , expand every action, then every successor with nonzero probability, then every action from there, and so on to depth . If each state has available actions and each state-action pair has at most possible successors, the tree can have leaves. In a deterministic 6-by-6 grid, and four actions already produce length-20 action sequences before transpositions or pruning. Even this tiny domain therefore yields roughly a trillion candidates, each of which requires model transitions and value bookkeeping rather than a single machine instruction. The point is not merely that we need a faster computer; uniform enumeration spends its effort in the wrong shape for this problem.
MCTS addresses this by refusing to build the tree uniformly. Instead of expanding every branch to depth , it runs a fixed number of simulations, each of which walks from the root down to a leaf along a policy that balances exploitation of branches with strong sampled returns against exploration of under-sampled ones, evaluates the leaf cheaply, and then updates statistics along the visited path. The represented tree is therefore usually asymmetric. Its exact shape reflects the selection rule, the rollout or leaf evaluator, the model, and the finite budget; an initially misleading estimate can direct substantial work toward a poor branch before later evidence corrects it.
MCTS is an anytime algorithm once every legal root action has an initialized decision statistic: you can stop it at a budget boundary and return the current recommendation. Under its consistency conditions, increasing the simulation budget improves the estimate asymptotically, but a single finite-budget recommendation need not improve monotonically. This interface makes MCTS usable inside a receding-horizon control loop, where the real constraint is often the wall-clock budget between two control commands. In practice you rarely run MCTS to convergence; you run it until the clock runs out and then act on the current tree. The tree should therefore contain useful decisions at the operating budget, not only after far more simulations.
The Four Phases
Each MCTS simulation executes four phases in sequence.
-
Selection. Starting at the root, repeatedly choose the child that maximizes the tree policy. The tree policy is what makes MCTS work, and we will spend most of this section on it. Selection stops when you reach a node that has not been fully expanded, or a terminal node. Because each step of selection moves one level down the tree, a single simulation visits nodes, and the tree policy is consulted at each one.
-
Expansion. If the selected node is not terminal and has unexpanded actions, add one or more children. A common choice is to add all children at once and mark the node fully expanded. You can also add children lazily, one per visit; this matters when is large, and we will return to it. When child creation evaluates the model, eager expansion can require up to model queries, many for children that may never be revisited. Laziness delays that work until a child is needed.
-
Simulation (rollout). From the newly created node, simulate a trajectory using some default policy, such as uniform random actions or a cheap heuristic, until you hit a terminal state or a depth limit. Sum the discounted rewards to get a return estimate . In rollout-based MCTS, this phase evaluates what lies beyond the explicit tree; other variants use a learned or heuristic leaf evaluator instead. A random default policy often produces noisy leaf values, while an accurate goal-directed heuristic can reduce that variance.
-
Backpropagation. Walk back up the path you came down, incrementing visit counts and updating the running mean return at each visited node or edge. Every visited ancestor receives an update on that simulation; the update does not guarantee that its finite-sample estimate becomes more accurate.
The distinction between the tree policy (used during selection, inside the tree) and the default policy (used during simulation, outside the tree) is central. The tree policy can afford more computation per decision because it is applied only along the explicit-tree prefix and its job is to allocate future simulations well. The default policy is applied throughout the remaining rollout for every simulation, so its cost is multiplied across many simulated steps. The useful balance depends on depth, branching, simulator cost, and how much a better tree or default policy changes return variance and action ranking.
From UCB to UCT to PUCT
The tree policy treats action selection at each node like a multi-armed bandit problem. Each selected edge produces a sampled downstream return, and the selection rule balances exploiting actions with high observed returns against exploring actions with little evidence. A canonical rule is UCB1 (Auer, Cesa-Bianchi, and Fischer, 2002):
where:
- : the running mean return observed when action was tried from
- : the number of times node has been visited
- : the number of times action has been tried from
- : the exploration constant controlling the size of the uncertainty bonus
The expression is evaluated only after every available action has been visited at least once; implementations usually force unvisited actions to be selected before applying it. Read the score as a conversation between two terms. The first term, , is exploitation: it prefers actions that have paid off so far. The second term is exploration: it grows when is small relative to , so an action that has rarely been tried gets a large bonus. The logarithm in the numerator means the bonus grows slowly. Kocsis and Szepesvári (2006) applied upper-confidence selection recursively in a search tree and called the result UCT. They proved consistency for the finite-horizon and discounted MDP settings analyzed in their paper; the result depends on bounded rewards, a correct generative model, and sufficient exploration.
The bonus has the shape of a concentration bound. In the stationary bandit setting used to derive UCB1, uncertainty shrinks roughly as , while the term makes an under-sampled action eligible again as the parent is revisited. The coefficient absorbs the reward range and the desired confidence level. Recursive tree search is more delicate than an independent stationary bandit at every node because the return distribution changes as policies deeper in the tree change. UCT's consistency analysis handles that coupling under its stated assumptions; the single-node UCB1 regret guarantee should not be copied to every node without those conditions.
UCT has one important weakness for our purposes. The exploration bonus has no place to insert side information about which actions are likely to be good. If a learned policy assigns most of its probability to one action, plain UCT does not use that ranking: it normally gives every legal action an initial visit and then allocates later visits from sampled returns and counts alone. With hundreds of legal moves, those mandatory first visits can consume much of a small simulation budget even when a useful prior is available.
PUCT fixes this by multiplying the exploration term by a prior :
where:
- : the action prior, normally produced by a policy network, indicating how promising action looks before any search statistics are collected
- : PUCT's exploration coefficient, playing the same qualitative role as the UCB coefficient (though the two formulas have different scales, so their numerical values are not interchangeable)
The denominator makes the prior-weighted bonus decay relative to repeated visits to the same action. AlphaGo and AlphaZero use closely related prior-guided tree policies, with implementation-specific coefficients and root exploration. We will return to that design in the final section and in MuZero and Value-Equivalent Models.
The PUCT score combines three signals. The prior says how promising the action looks before search at this node. The running mean summarizes sampled returns, and records how often the edge has been visited. High-prior actions receive a larger early exploration bonus; repeated visits reduce that bonus relative to alternatives. A positive prior and continued exploration can let later return evidence override an imperfect ranking. A zero prior is different: in the displayed formula it gives no exploration bonus, so implementations commonly add root noise, probability floors, or forced initial visits. PUCT is a knowledge-guided selection rule, not a general guarantee of using fewer simulations than UCT.
The constant controls how strongly search favors under-explored branches. The textbook UCB1 coefficient is when rewards are normalized to the range assumed by its bound, but practical UCT and PUCT implementations tune the coefficient to their reward and value scales. AlphaZero describes a fixed ; later systems such as MuZero use a visit-dependent coefficient. These choices should not be transferred between problems without checking the scale of . A useful sanity check: if you rescale your rewards by a factor of 10 and do not rescale , you have effectively shrunk the exploration bonus by the same factor, and the search will behave differently.
Backpropagation and the Value Estimate
When a simulation returns a discounted return from the leaf, that value is the return from the leaf. But the node statistics you need to update are indexed by the path from the root. If the path is and is the reward received when entering node , then the return from node is defined recursively:
where:
- : the return backed up to node , obtained by working backward from the leaf
- : the reward on the transition from to
- : the discount factor
- : the leaf evaluation or remaining rollout return at node
Working backward along the path and adding each corresponding to node gives the correctly indexed discounted sample return. Backing up the root return unchanged to every node instead shifts the reward timing and biases deeper-node estimates. This matters because selection compares deeper-node values against each other, and a systematic shift in those values biases the search. A buggy implementation can still populate a tree and sometimes reach the goal, so the defect may surface as inefficient allocation or different action choices rather than a crash.
Once you have them, the node statistics are simple running averages:
where:
- : the number of simulations in which action was taken from node
- : the discounted return from node recorded on simulation
- the sum runs over simulations whose selected path passed through the edge
This estimator is a sample mean of returns generated by the search procedure; it is not automatically an unbiased estimate of . The tree policy and leaf evaluation change as the tree grows, so the samples are neither identically distributed nor drawn from the final policy. Uneven action sampling alone does not bias the conditional mean for an edge, but adaptive downstream policies, finite horizons, and approximate leaf values can. Treat a finite-budget MCTS as a search statistic tied to a particular algorithm and budget, not as a calibrated critic unless calibration is checked separately.
Choosing a Final Action
When the budget runs out, you need to pick an action. Two options:
- Most-visited child. Return . This convention reports which action received the largest allocation under the configured tree policy and budget.
- Highest-value child. Return . This convention reports the largest sampled mean and can be sensitive to actions supported by very few visits.
Many implementations choose the root action by visit count while using value estimates inside the tree. Both statistics reflect returns propagated through a subtree, but they answer different questions. is the observed mean for one edge; records how often the selection rule returned to that edge after comparing it with alternatives. A high value supported by two visits is fragile, while a large visit count can still be wrong when the prior, exploration rule, or model is biased. Visit count is an allocation statistic, not a calibrated confidence probability.
The visit distribution at the root can itself serve as a policy or a policy-training target. AlphaZero uses as a search-policy target, where is a temperature. If a search allocates 80% of its simulations to one action, that is a strong preference under that search configuration and budget. We will exploit this in the Policies Learned in Imagination chapter. The temperature controls how sharp the distribution is: at you take the raw visit proportions, at you get a one-hot distribution when the most-visited action is unique (ties require an explicit rule), and larger flattens the distribution. A training scheme may use a higher temperature while collecting exploratory data than at deployment, but the schedule is a design choice rather than a property of MCTS itself.
Progressive Widening
The scheme above assumes that expanding a node means adding all children. That can be manageable when is four and each child is cheap to instantiate. It is impossible to enumerate when is a continuous set, such as a bounded subset of , and costly when the discrete action set contains hundreds of moves. Progressive widening (Coulom, 2007; later refined by Auger, Couëtoux, and Teytaud, 2013) limits the number of instantiated children after visits to
where:
- : the number of children the node is allowed to have at visit count
- : the widening constant that sets the overall rate of growth
- : the widening exponent controlling how quickly the child budget broadens
The constants are problem-dependent; is a common starting point, not a universal default. Early visits instantiate few children and go deep; later visits broaden as the budget grows. POMCPOW (Sunberg and Kochenderfer, 2018) applies progressive widening to sampled actions and observations so a finite search tree can be used in continuous spaces. Widening does not by itself ensure that an optimal continuous action is ever sampled. That conclusion additionally requires an action proposal with suitable support and regularity assumptions. The practical tradeoff is depth against coverage: a slow schedule gives each instantiated child more visits, while a fast schedule covers more candidates with fewer samples per child.
Let us now build the MCTS algorithm. The first cell binds shared plot-style helpers alongside computation imports. Figure construction remains in the later visualization cells, while arrays and statistics are prepared beforehand.
# Shared simulation setup and plot-style bindings.
# Figure construction stays in the plot cells below.
import math
from collections import deque
import numpy as np
# Plot helpers are defined once here so later plot cells can use them locally.
try:
from book_plot_style import label_bars, polish_axes, use_book_style
except ImportError: # fallback for environments without book_plot_style
def use_book_style():
pass
def polish_axes(ax):
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
def label_bars(ax, bars):
for b in bars:
ax.annotate(
f"{int(b.get_height())}",
(b.get_x() + b.get_width() / 2, b.get_height()),
ha="center",
va="bottom",
fontsize=7,
)
# Rows top to bottom, columns left to right. True marks a wall.
GRID = np.array(
[
[0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 1, 0],
[1, 1, 1, 0, 1, 0],
[0, 0, 0, 0, 1, 0],
[0, 1, 1, 1, 1, 0],
],
dtype=bool,
)
H, W = GRID.shape
START = (0, 0)
GOAL = (5, 5)
MOVES = ((-1, 0), (1, 0), (0, -1), (0, 1)) # up, down, left, right
N_ACTIONS = len(MOVES)
ACTION_NAMES = ("up", "down", "left", "right")
STEP_COST = -0.02
GOAL_REWARD = 1.0
GAMMA = 0.97
def step(state, action):
"""Deterministic grid transition. Returns (next_state, reward, done)."""
if state == GOAL:
return state, 0.0, True
r, c = state
dr, dc = MOVES[action]
nr, nc = r + dr, c + dc
if not (0 <= nr < H and 0 <= nc < W) or GRID[nr, nc]:
nr, nc = r, c # bumping a wall leaves you where you were
nxt = (nr, nc)
if nxt == GOAL:
return nxt, GOAL_REWARD, True
return nxt, STEP_COST, FalseThe maze is deliberately shaped so that the goal is not reachable by greedy Manhattan descent: the bottom-left arm looks closer to the goal than the top corridor, but it dead-ends. A policy that follows Manhattan distance alone enters the trap, while a sufficiently deep and well-sampled search can represent the necessary backtracking. Let us confirm the geometry before comparing finite-budget planners.
. . . . . . . # # # # . . . . . # . # # # . # . . . . . # . . # # # # G
The interior walls create a top corridor, a right-hand descent, and a bottom-left dead end. Every route out of that dead end must backtrack before reaching the goal. In the fully observed case, a planner can index the exact distance-to-goal map by the current cell. In the partially observed case, the map is still known, but the current cell is not; the controller must evaluate actions across its belief rather than select one state's lookup entry.
Next we compute two reference quantities. The breadth-first distance map gives us the true shortest-path length from every free cell to the goal, which we will use both as a rollout heuristic and as a diagnostic. Value iteration converges to a high-accuracy numerical reference for , which we will use to measure how well MCTS is doing.
FREE = [(r, c) for r in range(H) for c in range(W) if not GRID[r, c]]
N_CELLS = len(FREE)
S_IDX = {s: i for i, s in enumerate(FREE)}
def signature(state):
"""4-bit local wall pattern: up, down, left, right."""
r, c = state
bits = 0
for a, (dr, dc) in enumerate(MOVES):
nr, nc = r + dr, c + dc
blocked = not (0 <= nr < H and 0 <= nc < W) or GRID[nr, nc]
if blocked:
bits |= 8 >> a
return bits
SIG = np.array([signature(s) for s in FREE])
# Transition tensor: T[a][i, j] = P(next = FREE[j] | state = FREE[i], action = a)
T = np.zeros((N_ACTIONS, N_CELLS, N_CELLS))
for i, s in enumerate(FREE):
for a in range(N_ACTIONS):
s2, _, _ = step(s, a)
T[a, i, S_IDX[s2]] = 1.0
# Deterministic observation matrix: O[o, i] = 1 iff cell i emits signature o
O = np.zeros((16, N_CELLS))
for i in range(N_CELLS):
O[SIG[i], i] = 1.0
def bfs_distances(goal=GOAL):
dist = {goal: 0}
queue = deque([goal])
while queue:
s = queue.popleft()
r, c = s
for dr, dc in MOVES:
nr, nc = r + dr, c + dc
nxt = (nr, nc)
if (
0 <= nr < H
and 0 <= nc < W
and not GRID[nr, nc]
and nxt not in dist
):
dist[nxt] = dist[s] + 1
queue.append(nxt)
return dist
DIST = bfs_distances()
def value_iteration(gamma=GAMMA, tol=1e-12):
V = {s: 0.0 for s in FREE}
for _ in range(20000):
delta = 0.0
for s in FREE:
if s == GOAL:
continue
best = -np.inf
for a in range(N_ACTIONS):
s2, r, done = step(s, a)
q = r + (0.0 if done else gamma * V[s2])
best = max(best, q)
delta = max(delta, abs(best - V[s]))
V[s] = best
if delta < tol:
break
return V
V_STAR = value_iteration()
print(f"Free cells: {N_CELLS}")
print(f"Shortest path from {START} to {GOAL}: {DIST[START]} steps")
print(f"Optimal value at the start: {V_STAR[START]:.4f}")
print(f"Value of the dead-end cell (5, 0): {V_STAR[(5, 0)]:.4f}")The asymmetry the value function encodes is easier to read as a picture than as a pair of scalars. The cell below stores the converged value-iteration result for every free cell and the exact shortest-path distance to the goal, so the figure cell can project both without doing any planning of its own.
# Presentation inputs only: converged values and exact distances from V_STAR / DIST.
VALUE_GRID = np.full((H, W), np.nan)
DIST_GRID = np.full((H, W), np.nan)
for s in FREE:
VALUE_GRID[s] = V_STAR[s]
DIST_GRID[s] = DIST[s]
# Fully observed greedy policy from V_STAR, used as a sanity reference only.
def greedy_action_from_value(state):
qs = []
for a in range(N_ACTIONS):
s2, r, done = step(state, a)
qs.append(r + (0.0 if done else GAMMA * V_STAR[s2]))
return int(np.argmax(qs))
GREEDY_ACTION = {}
for s in FREE:
if s == GOAL:
continue
GREEDY_ACTION[s] = greedy_action_from_value(s)
print(f"max V* over free cells: {np.nanmax(VALUE_GRID):.4f}")
print(f"min V* over free cells: {np.nanmin(VALUE_GRID):.4f}")
Both quantities agree with intuition: ten steps is the true shortest route, and the remote dead-end cell is worth only against at the start. An agent that wanders into the bottom-left arm pays for it. The gap is large enough to be decision-relevant, which makes this maze a useful example: the planner must discover the difference from finite rollouts, and under partial observability it must also infer which region it occupies.
We now implement MCTS. The four phases map directly onto the four blocks of the simulation loop, in the order we described them.
class MCTSNode:
__slots__ = (
"state",
"parent",
"reward",
"prior",
"children",
"N",
"W",
"Q",
)
def __init__(self, state, parent=None, reward=0.0, prior=1.0):
self.state = state
self.parent = parent
self.reward = reward # reward received when entering this node
self.prior = prior # P(s, a) in the PUCT formula
self.children = {} # action -> MCTSNode
self.N = 0
self.W = 0.0 # sum of returns
self.Q = 0.0 # W / N
def tree_score(child, c, rng):
"""PUCT score with a forced first visit per action."""
if child.N == 0:
return 1e9 + 100.0 * child.prior + rng.random()
bonus = c * child.prior * math.sqrt(child.parent.N) / (1.0 + child.N)
return child.Q + bonus
def mcts(
root_state,
n_sims,
priors=None,
rollout=None,
gamma=GAMMA,
c=1.5,
max_depth=30,
seed=0,
):
"""Run n_sims simulations from root_state and return the search tree root."""
rng = np.random.default_rng(seed)
root = MCTSNode(root_state)
for _ in range(n_sims):
node, path, depth = root, [root], 0
# --- 1. Selection -------------------------------------------------
while node.children and node.state != GOAL and depth < max_depth:
a = max(
node.children,
key=lambda k: tree_score(node.children[k], c, rng),
)
node = node.children[a]
path.append(node)
depth += 1
# --- 2. Expansion -------------------------------------------------
if node.state != GOAL and depth < max_depth:
for a in range(N_ACTIONS):
s2, r, _ = step(node.state, a)
p = (
1.0 / N_ACTIONS
if priors is None
else float(priors(node.state, a))
)
node.children[a] = MCTSNode(s2, parent=node, reward=r, prior=p)
# Continue through one newly expanded edge before rolling out.
# This gives every child an action return on its first visit.
a = max(
node.children,
key=lambda k: tree_score(node.children[k], c, rng),
)
node = node.children[a]
path.append(node)
depth += 1
# --- 3. Simulation ------------------------------------------------
s, total, disc = node.state, 0.0, 1.0
for _ in range(max_depth - depth):
if s == GOAL:
break
a = (
int(rng.integers(N_ACTIONS))
if rollout is None
else int(rollout(s, rng))
)
s, r, done = step(s, a)
total += disc * r
disc *= gamma
if done:
break
# --- 4. Backpropagation -------------------------------------------
g = total
for n in reversed(path[1:]):
g = n.reward + gamma * g
n.N += 1
n.W += g
n.Q = n.W / n.N
root.N += 1
root.W += g
root.Q = root.W / root.N
return root
def guided_action(state, rng, epsilon=0.3):
"""Rollout policy: mostly follow the BFS shortest path, sometimes wander."""
if rng.random() < epsilon:
return int(rng.integers(N_ACTIONS))
best_a, best_d = 0, None
for a in range(N_ACTIONS):
nxt, _, _ = step(state, a)
d = DIST.get(nxt, 10**6) + (0.5 if nxt == state else 0.0)
if best_d is None or d < best_d:
best_a, best_d = a, d
return best_a
def root_choice(root):
return max(root.children, key=lambda k: root.children[k].N)The 1e9 branch inside tree_score forces every action to be visited once before ordinary PUCT scoring takes over. This safeguard is especially important for zero or numerically tiny priors, whose exploration bonuses may otherwise be absent or negligible. A positive-prior action is not permanently locked out by one unlucky rollout under the displayed bonus, because its exploration term grows as the parent is revisited while its own count stays fixed. Here the prior orders unequal-prior unvisited actions, and the random term breaks ties between equal priors across seeds.
Let us run 400 simulations from the start and see what the search finds.
root = mcts(START, n_sims=400, seed=0)
chosen = root_choice(root)
best_child = root.children[chosen]
summary = {
ACTION_NAMES[a]: (int(root.children[a].N), round(root.children[a].Q, 4))
for a in range(N_ACTIONS)
}Chosen action: right
Estimated value: -0.3891 (true optimum 0.6004)
up: visits= 95 Q=-0.3993
down: visits= 95 Q=-0.3993
left: visits= 100 Q=-0.3951
right: visits= 110 Q=-0.3891With uniform random rollouts and uniform priors, 400 simulations give right, the first step of the shortest path, only a small plurality of visits. The two wall-bumping actions (up and left) still receive almost one quarter of the budget each, and all four finite-horizon value estimates remain negative. This is useful evidence rather than a successful convergence result: a weak rollout policy can leave the search with little signal even when the final action happens to be correct. Visit counts show how the budget was allocated, but only strong concentration would show that search had decisively separated the actions.
Let us look at what that means in the maze. We run a closed-loop episode where MCTS re-plans at every step, then draw the resulting trajectory.
def mcts_episode(
start, n_sims=300, max_steps=30, rollout=None, priors=None, seed=0
):
state, path, total = start, [start], 0.0
for t in range(max_steps):
if state == GOAL:
break
tree = mcts(
state, n_sims=n_sims, rollout=rollout, priors=priors, seed=seed + t
)
a = root_choice(tree)
state, r, done = step(state, a)
total += r
path.append(state)
if done:
break
return path, total
PATH_UNIFORM, RET_UNIFORM = mcts_episode(
START, n_sims=300, rollout=None, seed=1
)
PATH_GUIDED, RET_GUIDED = mcts_episode(
START, n_sims=300, rollout=guided_action, seed=1
)
# Sanity check: both runs should reach the goal.
assert PATH_UNIFORM[-1] == GOAL, (
"uniform-rollout planner failed to reach the goal"
)
assert PATH_GUIDED[-1] == GOAL, (
"guided-rollout planner failed to reach the goal"
)Uniform rollouts: 13 steps, return 0.760 Guided rollouts: 10 steps, return 0.820
Both planners solve the maze, and the guided-rollout run gets there in three fewer steps. That result is consistent with the lower value-estimate error produced by the goal-directed rollout policy in the experiment below. It illustrates the mechanism in this deterministic seeded run, but does not establish a general speedup.


The left panel shows that repeated closed-loop search reaches the goal in this seeded run: the guided rollout follows the ten-step route, while the uniform rollout takes thirteen steps. The right panel explains why this one trajectory should not be mistaken for a well-resolved root estimate. The 400-simulation search spreads visits nearly evenly, including across the two wall-bumping actions. Closed-loop replanning recovers a useful action sequence here even though each individual uniform-rollout search remains uncertain. Read visits and values together: the visit count records the selection rule's allocation, while the value is the sampled mean return on an edge. Neither is a calibrated confidence measure.
How Good Is the Guidance?
We can compare finite-budget root estimates under uniform and guided rollouts. The experiment below measures absolute error relative to over eight seeds. It tests these two implementations on this maze; it does not establish a general sample-complexity result.
BUDGETS = [10, 25, 50, 100, 200, 400, 800]
SEEDS = list(range(8))
GUIDANCE_ERRORS = {"No guidance": [], "Guided rollouts": []}
GUIDANCE_VISIT_SHARE = {"No guidance": [], "Guided rollouts": []}
OPTIMAL_FIRST_ACTION = guided_action(
START, np.random.default_rng(0), epsilon=0.0
)
for label, policy in [
("No guidance", None),
("Guided rollouts", guided_action),
]:
for budget in BUDGETS:
errs, shares = [], []
for s in SEEDS:
tree = mcts(START, n_sims=budget, rollout=policy, seed=s)
a = root_choice(tree)
errs.append(abs(tree.children[a].Q - V_STAR[START]))
shares.append(tree.children[OPTIMAL_FIRST_ACTION].N / budget)
GUIDANCE_ERRORS[label].append(float(np.mean(errs)))
GUIDANCE_VISIT_SHARE[label].append(float(np.mean(shares)))Reference V*(start): 0.6004 No guidance value error: 0.981 0.982 0.982 0.985 0.986 0.988 0.990 Guided rollouts value error: 0.174 0.203 0.220 0.239 0.251 0.265 0.274
The finite-budget result is more cautionary than a textbook convergence curve. At every tested budget, guided rollouts produce much smaller error than uniform rollouts, but neither series approaches zero by 800 simulations. The uniform estimate stays about one unit from , while the guided error rises from about to . One plausible explanation is that more of the fixed 30-step horizon becomes controlled by a still-shallow tree rather than the stronger default policy, but this experiment does not isolate the cause. Asymptotic UCT results require their stated assumptions about horizon, exploration, and sampling; this small implementation and budget are an experiment about finite-compute behavior, not evidence of convergence. Guidance changes where finite compute is useful, while approximation choices can also change the observed value estimate.


The experiment demonstrates a narrower practical point: guidance can materially change finite-budget search. A better rollout policy, leaf evaluator, or action prior can reduce the simulations needed for a chosen decision-quality target, but only when its extra evaluation cost and errors are included. Unguided search remains useful in some domains; guidance changes its sample efficiency rather than defining whether search has value.
One caveat before we move on. Both curves in the first panel are relative to , the value of the fully observed MDP. That is the right benchmark here, but it stops being the right benchmark the moment we take away the agent's knowledge of its own state, which is exactly what we do next. Under partial observability, "estimated minus optimal" would compare a belief-conditioned value estimate with a fully observed state value, so the comparison would no longer mean the same thing. Keep this in mind as you read the next section; the numbers are still informative, but they measure a different quantity.