Why Game-Tree Search Matters
A game opponent chooses moves too. A puzzle solver may ask whether some path reaches a goal; a game-playing program must ask which move remains good after the opponent chooses the least favorable reply. A promising one-move gain can become a loss after a forced response. Minimax turns that alternating choice into a precise value, and alpha-beta pruning avoids exploring replies that cannot change the decision.
The model here is a deterministic, alternating, two-player, zero-sum, perfect-information game. Every score is from one fixed player's perspective: MAX prefers larger scores; MIN prefers smaller scores. Terminal utilities are exact under the game rules; a static evaluation at a depth boundary is an estimate. A completed depth-limited search is exact for its specified evaluation tree, which can still misjudge the actual game.
This lesson builds from minimax to alpha-beta, then adds move ordering, iterative deepening, a bound-aware transposition table, and bounded quiescence search. The runnable example uses a small acyclic game graph so every value and cutoff can be checked without a board-game library. It also makes an interrupted search return the last fully completed iteration, a useful behavior when a game engine must choose under a budget.
Choose a move by backing up the opponent's best responses: take a maximum on MAX turns and a minimum on MIN turns. Alpha-beta skips branches that cannot improve an ancestor's available alternative; deeper iterations, sound cache bounds, and tactical extensions help spend the search budget effectively.
Core Concepts and Terminology
A position is a complete game state, including whose turn it is and every rule-relevant right or history item. A move leads to another position. A ply is one player's move, so depth $2$ includes our move and the opponent's reply. Different move sequences may reach the same position: those are transpositions, even though they occupy different nodes in the expanded search tree.
Let $U(s)$ be a terminal utility, $E(s)$ a static evaluation in the same MAX-oriented score system, and $A(s)$ the legal moves. Define $\operatorname{opt}_s$ as maximum on MAX turns and minimum on MIN turns. Without quiescence, depth-limited minimax is
$$V_d(s)=\begin{cases}U(s)&\text{if }s\text{ is terminal},\\E(s)&\text{if }d=0,\\\operatorname{opt}_s\{V_{d-1}(\operatorname{result}(s,a)):a\in A(s)\}&\text{otherwise}.\end{cases}$$
The search carries an open alpha-beta window $(\alpha,\beta)$. Alpha reflects a lower alternative already available to MAX, and beta an upper alternative available to MIN. At a MAX node, increase alpha as better children appear; at a MIN node, decrease beta. If $\alpha\ge\beta$, remaining children cannot change the relevant ancestor's choice. A returned score outside the caller's window may be only a bound; it must not automatically be treated as an exact minimax value.
The diagram shows the depth-$2$ worked tree. Scores at leaves are MAX's utilities. After MIN branch $1$ establishes $3$, MAX can reject branch $2$ as soon as MIN finds reply $2$, and branch $3$ as soon as MIN finds reply $0$. The skipped replies are shown only to define the full input; the search does not evaluate them.
| Term | Concrete example | Role |
|---|---|---|
| MAX / MIN | Root $0$ is MAX; its children $1,2,3$ are MIN | Determines maximum versus minimum backup |
| Utility / evaluation | Leaf $4$ has utility $3$; nonterminal $3$ has static estimate $4$ | Exact terminal outcome versus depth-boundary estimate |
| Ply / remaining depth | Root depth limit $2$ becomes remaining depth $1$ after one move | Counts individual moves, not pairs of turns |
| Alpha / beta | MIN $2$ inherits $\alpha=3$, then reduces $\beta$ to $2$ | Ancestor alternatives justify a cutoff |
| Fail-soft return | MIN $2$ returns $2$, outside its window $(3,+\infty)$ | Returns the discovered bound, rather than clamping to the window |
| Transposition table | Cache key $(s,d,q)$ plus value, move, and flag | Reuses compatible exact results or cutoff bounds |
EXACT / LOWER / UPPER | UPPER 2 means the true search value is at most $2$ | Records what a cached score actually proves |
| Move ordering | Previous best move first, then favorable static estimates | Changes work and tied move choice, not the completed value |
| Iterative deepening | Complete depths $1$, then $2$, then $3$ | Supplies ordering hints and a completed fallback result |
| Quiescence search | At depth zero, examine selected tactical replies | Reduces some horizon errors under an explicit extension policy |
How Game-Tree Search Works, Step by Step
- Set a score convention and terminal rules. Every utility and evaluation in these programs is from MAX's viewpoint. A position with no outgoing moves is terminal, and its supplied score is its utility. Real adapters must distinguish wins, losses, draws, stalemate, and forced passes according to their rules.
- Start an iteration with a full window. For requested depth $d$, search the root with $(-\infty,+\infty)$. The driver tries $d=1,2,\ldots,D$. Keep a committed root score and move separate from the in-progress result.
- Admit and probe. Before each call, enforce the global node budget. Count an admitted call, update peak active depth, and form the table key $(s,d,q)$. A matching
EXACTentry can return directly;LOWERcan return when its score is at least beta;UPPERcan return when its score is at most alpha. Otherwise search normally. This implementation uses cache bounds only for immediate returns and does not tighten a non-cutting window. - Handle a terminal or horizon position. Terminal utility takes precedence. At depth zero with no tactical budget left, read the static evaluation. Otherwise depth zero enters the bounded quiescence policy described below.
- Order and try moves. With ordering enabled, try the most recently recorded move for this state first, then sort by successor evaluation: descending for MAX, ascending for MIN, breaking remaining ties by original move index. At ordinary depth, all legal moves are candidates; during quiescence, only marked tactical moves are candidates.
- Apply, recurse, undo. Append the child to the active path, search it, then remove it before processing its score. Python and Java use
finally; C++ uses a scope guard whose destructor pops the path. An interruption unwinds through those same cleanup steps. - Back up and test the window. MAX raises its best score and alpha; MIN lowers its best score and beta. Stop scanning moves when $\alpha\ge\beta$. Update the selected move only on a strict improvement, so ties keep the first searched candidate. A cached bound's associated move is an ordering hint, not a promised exact principal variation.
- Store what was proved. Save the caller's original window $(\alpha_0,\beta_0)$. After a searched non-base node returns $v$, store
UPPERif $v\le\alpha_0$,LOWERif $v\ge\beta_0$, otherwiseEXACT. Direct terminal/static reads are known exact for that horizon even when outside the window. An aborted call stores no result for itself; completed descendants may have valid entries. - Commit only after root completion. A completed full-window root result becomes the new committed answer. If a later iteration aborts, return the previous completed depth. If depth $1$ never completes, report
NO_COMPLETED_DEPTH; its dummy score is not an evaluation and its move is not playable.
The quiescence policy is part of the searched model. The toy graph marks tactical edges explicitly. At depth zero, a nonterminal position can either stop with $E(s)$, called stand pat, or follow a tactical edge while decrementing $q$. At $q=0$ it must stop. Thus, with $T(s)$ the tactical successors,
$$Q_0(s)=E(s),\qquad Q_q(s)=\operatorname{opt}_s\left(\{E(s)\}\cup\{Q_{q-1}(t):t\in T(s)\}\right)\quad(q\gt0),$$
with terminal positions always returning $U(s)$. Ordinary-depth recursion decrements $d$ and preserves $q$; depth-zero tactical recursion preserves $d=0$ and decrements $q$. This bounds an active line by $D+Q$ plies. Stand pat is an explicit option in this demonstration's evaluation model. In chess it relies on additional assumptions, can fail in zugzwang, and must not be used to ignore check; checked positions require legal evasions. The sample has no check or repetition rules and is not a complete chess searcher.
flowchart TD
A["Driver: next depth, full root window"] --> B["Search root"]
B --> C{"Iteration completed?"}
C -->|Yes| D["Commit root score and move"]
D --> E{"Requested depth reached?"}
E -->|No| A
E -->|Yes| F["Return COMPLETE"]
C -->|No| G["Return last completed result or NO_COMPLETED_DEPTH"]
subgraph AB["Alpha-beta call"]
H{"Node budget available?"} -->|No| I["Throw cancellation; unwind all path edits"]
H -->|Yes| J["Count call; probe matching TT horizon"]
J --> K{"Exact or cutoff-compatible entry?"}
K -->|Yes| L["Return cached score and move"]
K -->|No| M{"Terminal or depth-zero with no q budget?"}
M -->|Yes| N["Read utility or evaluation; store EXACT"]
M -->|No| O["At q node: initialize stand pat and test cutoff"]
O --> P["Order all legal or tactical moves"]
P --> Q{"Candidate left and window open?"}
Q -->|Yes| R["Apply child; recurse; always undo"]
R --> S["Back up score; update alpha or beta"]
S --> Q
Q -->|No| T["Store EXACT, LOWER, or UPPER; return"]
end
B -. "call entry" .-> H
I -. "propagate abort" .-> C
O -. "stand-pat cutoff" .-> T
Worked Example with a Full Trace
Use the exact graph in the first program fixture. Root $0$ is MAX with moves indexed $0,1,2$, leading to MIN positions $1,2,3$. Their static evaluations are $3,2,4$. MIN $1$ has terminal replies $4:3$ and $5:5$; MIN $2$ has $6:2$ and $7:9$; MIN $3$ has $8:0$ and $9:7$. All terminal positions are labeled MAX-to-move, keeping turn alternation consistent, though the label does not affect a terminal utility.
Enable the transposition table, disable move ordering, set quiescence limit $Q=0$, and request maximum depth $D=2$ with a total budget of $1{,}000{,}000$. Every iteration starts with empty active path and full root window, while the cache and ordering hints persist within the run. This tree has no useful transposition hits.
At depth $1$, only the child estimates are visible: $\max(3,2,4)=4$, so the provisional choice is move $2$, leading to state $3$. At depth $2$, the full minimax value is
$$\max\bigl(\min(3,5),\min(2,9),\min(0,7)\bigr)=\max(3,2,0)=3.$$
The complete call trace follows. A row is listed at admission time; its return and stored flag are determined after its descendants finish. All calls use $q=0$. Path length minus one is active ply depth; the remaining-depth column is the separate recursion argument. Every child is popped after its return.
| Iteration | Active path on entry | Remaining depth | Entry window $(\alpha,\beta)$ | Eventual return / flag | State change or consequence |
|---|---|---|---|---|---|
| $1$ | $[0]$ | $1$ | $(-\infty,+\infty)$ | $4$, EXACT, move $2$ | Alpha progresses $-\infty\to3\to3\to4$ |
| $1$ | $[0,1]$ | $0$ | $(-\infty,+\infty)$ | $3$, EXACT | Static read; undo to $[0]$ |
| $1$ | $[0,2]$ | $0$ | $(3,+\infty)$ | $2$, EXACT | Known static value; root best stays $3$ |
| $1$ | $[0,3]$ | $0$ | $(3,+\infty)$ | $4$, EXACT | Root switches to move $2$ |
| $2$ | $[0]$ | $2$ | $(-\infty,+\infty)$ | $3$, EXACT, move $0$ | Alpha becomes $3$ after branch $1$ and stays there |
| $2$ | $[0,1]$ | $1$ | $(-\infty,+\infty)$ | $3$, EXACT, move $0$ | Beta becomes $3$, then stays $3$ |
| $2$ | $[0,1,4]$ | $0$ | $(-\infty,+\infty)$ | $3$, EXACT | Terminal utility; undo leaf $4$ |
| $2$ | $[0,1,5]$ | $0$ | $(-\infty,3)$ | $5$, EXACT | Terminal utility; MIN still prefers $3$ |
| $2$ | $[0,2]$ | $1$ | $(3,+\infty)$ | $2$, UPPER, move $0$ | Beta becomes $2$; skip reply $7$ |
| $2$ | $[0,2,6]$ | $0$ | $(3,+\infty)$ | $2$, EXACT | Terminal read triggers parent's cutoff |
| $2$ | $[0,3]$ | $1$ | $(3,+\infty)$ | $0$, UPPER, move $0$ | Beta becomes $0$; skip reply $9$ |
| $2$ | $[0,3,8]$ | $0$ | $(3,+\infty)$ | $0$, EXACT | Terminal read triggers parent's cutoff |
The entry for MIN $2$ proves only that its value is at most $2$. Its true value happens to equal $2$ in this input, but that fact was not established by examining every reply. A cache must retain the weaker proven claim. Static and terminal reads, by contrast, establish exact values of their own requested horizons even when outside the caller's window.
| Pass | Committed score / move | Admitted nodes | Evaluations | Window cutoffs | TT returns | Depth-zero calls | Peak ply so far |
|---|---|---|---|---|---|---|---|
| Depth $1$ | $4$ / move $2$ | $4$ | $3$ | $0$ | $0$ | $3$ | $1$ |
| Depth $2$ | $3$ / move $0$ | $8$ | $4$ | $2$ | $0$ | $4$ | $2$ |
| Total | Final score $3$, move $0$ | $12$ | $7$ | $2$ | $0$ | $7$ | $2$ |
After each pass, the active path is empty. The table contains $12$ distinct horizon entries; no lookups return early in this fixture. Move $0$ leads to state $1$, whose best MIN reply is state $4$. The returned move is a root action index, not an entire principal variation.
Trace It by Hand
Pause after MIN state $1$ returns $3$ in the depth-$2$ pass. Predict what happens after MIN state $2$ examines its first reply, utility $2$. Must it examine the reply worth $9$?
Reveal: the inherited alpha is $3$. MIN reduces beta to $2$, so $\alpha\ge\beta$; the second reply is skipped, the path is restored, and the cached entry is UPPER 2. MAX retains score $3$ from move $0$.
Now enable ordering. The previous best root move leads to state $3$, so it is tried first in depth $2$. That branch returns $0$; state $1$ then raises the root score to $3$, and only state $2$ is cut off. The two passes now admit $13$ nodes rather than $12$. Ordering helps when its predictions are useful; it is not a per-input improvement guarantee.
Finally cap the total budget at $7$ nodes with ordering disabled. Depth $1$ consumes $4$ nodes; depth $2$ admits root $0$, MIN $1$, and leaf $4$, then refuses admission of leaf $5$. The driver returns PARTIAL, completed depth $1$, score $4$, move $2$, with the active path fully restored.
Implementation in Python, C++, and Java
Each complete program implements the same recurrence and prints the same output. The adapter represents a node as a MAX/MIN label, a bounded static or terminal score, and an ordered list of moves (child, tactical). Normal search uses every move. Quiescence uses only marked tactical moves plus the explicit stand-pat option. The active state is a path of immutable node IDs, so apply/undo is append/pop; a mutable board adapter must also restore pieces, side to move, rule rights, and its position key.
Input contract. Supply $1\le V\le64$ non-null nodes, at most $4096$ total edges, Boolean player/tactical labels, and integer scores in $[-1000,1000]$. A successor of state $s$ must have ID $t$ with $s\lt t\lt V$ and the opposite player label. This topological numbering deliberately excludes cycles and repetition adjudication; duplicate moves and transpositions are allowed. An empty move list means terminal, and its score is the supplied exact utility. The constructor copies the graph's move lists; a run does not change the caller's graph.
The start ID lies in $[0,V-1]$, requested depth in $[1,12]$, quiescence budget in $[0,6]$, and global admission budget in $[1,1{,}000{,}000]$. These finite limits keep demonstration recursion and counters bounded. A sentinel INF = 1_000_000 lies strictly outside all scores; C++ and Java use 64-bit counters. Invalid numeric sizes, scores, edges, and search ranges are rejected. The public operation is run; the recursive helper assumes those validations and initialized per-run state.
Cache compatibility. The exact key is $(s,d,q)$, including remaining normal depth and remaining tactical depth. A deeper heuristic search can return a different score, so this lesson does not substitute a deeper horizon's value for a shallower one. Hints can cross horizons because they only reorder legal candidates. Evaluation policy and graph are fixed throughout a run, the table is cleared for the next run, and integer IDs uniquely identify complete toy positions. Production hashing needs collision handling and all rule-relevant state in its identity.
Results and counters. run returns status, completed depth, committed score, committed root move index, six statistics, and history. Statistics are [nodes,evals,cutoffs,tt_hits,qnodes,peak]: admitted calls including table returns; actual score reads; move-loop or stand-pat window cutoffs; usable cache returns; all calls with normal depth zero, including terminal/cache calls; and maximum admitted active ply depth. A window cutoff counts even if it occurs on the last candidate, so it is not the number of skipped moves. History records each attempted depth, DONE or ABORT, its score/move, and five counter deltas; peak is a global maximum. An aborted history row has dummy score $0$ and move $-1$.
COMPLETE means every requested depth finished. PARTIAL returns the last finished depth and its legal root move when the root is nonterminal. NO_COMPLETED_DEPTH has completed depth $0$, dummy score $0$, and move $-1$; callers must choose a separate legal fallback. A terminal root also has move $-1$, but its score is meaningful. Budget exhaustion is detected on the next attempted admission: exactly consuming the budget does not invalidate a pass that already finished. Counters include work from an aborted iteration, and the path is restored before status is returned.
Python 3, C++14 or newer, and Java 17 or newer run the samples below; save the Java panel as Main.java. The fixtures cover pruning, ordering that hurts, transposition reuse, a horizon mistake corrected by a tactical extension, interruption, no completed pass, and a terminal root.
Expected standard output, identical in all three languages:
worked: COMPLETE, depth=2, score=3, move=0nodes=12, evals=7, cutoffs=2, tt_hits=0, qnodes=7, peak=2, restored=YESpasses=[1:DONE:4:2:4,2:DONE:3:0:8]ordered: COMPLETE, depth=2, score=3, move=0nodes=13, evals=8, cutoffs=1, tt_hits=0, qnodes=8, peak=2, restored=YESpasses=[1:DONE:4:2:4,2:DONE:3:0:9]transposition: COMPLETE, depth=3, score=4, move=0nodes=19, evals=9, cutoffs=2, tt_hits=2, qnodes=8, peak=3, restored=YESpasses=[1:DONE:4:0:3,2:DONE:4:0:7,3:DONE:4:0:9]static horizon: COMPLETE, depth=1, score=8, move=0nodes=3, evals=2, cutoffs=0, tt_hits=0, qnodes=2, peak=1, restored=YESpasses=[1:DONE:8:0:3]quiescence: COMPLETE, depth=1, score=2, move=1nodes=5, evals=4, cutoffs=0, tt_hits=0, qnodes=4, peak=2, restored=YESpasses=[1:DONE:2:1:5]interrupted: PARTIAL, depth=1, score=4, move=2nodes=7, evals=4, cutoffs=0, tt_hits=0, qnodes=4, peak=2, restored=YESpasses=[1:DONE:4:2:4,2:ABORT:0:-1:3]no completed pass: NO_COMPLETED_DEPTH, depth=0, score=0, move=-1nodes=1, evals=0, cutoffs=0, tt_hits=0, qnodes=0, peak=0, restored=YESpasses=[1:ABORT:0:-1:1]terminal root: COMPLETE, depth=2, score=-7, move=-1nodes=2, evals=2, cutoffs=0, tt_hits=0, qnodes=0, peak=0, restored=YESpasses=[1:DONE:-7:-1:1,2:DONE:-7:-1:1]checks = PASS
Complexity, Correctness, and Invariants
Let $b$ be maximum branching, $d$ a fixed normal depth, $Q$ the tactical cap, and $H=d+Q$ the maximum active horizon. First ignore transpositions, sorting cost, and quiescence. Full minimax visits a tree of $O(b^d)$ nodes for constant $b\gt1$. Alpha-beta has the same worst-case bound with unfavorable move order. On a uniform depth-$d$ tree with perfect ordering, the minimum leaf count is
$$b^{\lceil d/2\rceil}+b^{\lfloor d/2\rfloor}-1,$$
giving the familiar $O(b^{d/2})$ best-order scale for fixed $b$. This is a regular-tree search bound, not a guarantee on every game or evaluator. An actual terminal root takes one call in a single search. The provided driver still runs all $D$ requested iterations, so a terminal root uses $D$ admitted calls and $D$ utility reads if the budget suffices.
With bounded quiescence, a conservative full-tree bound is $O(b^{d+Q})$ for $b\gt1$, though tactical branching is often much smaller. Iterative deepening repeats depths $1$ through $D$. For geometrically growing regular trees their total full-width work remains $O(b^{D+Q})$; a single chain with no quiescence instead costs $1+2+\cdots+(D+1)-1=\Theta(D^2)$ calls when every normal pass reaches its boundary. Move ordering and cache reuse can change each pass's work substantially. There is no distribution-free average-case bound: branching, evaluation quality, tactical density, transpositions, and move order all matter.
For the actual programs, let $N$ be admitted calls, $T$ stored cache entries, and, for each expanded non-base call $v$, let $g_v$ be its outgoing degree and $k_v$ its candidate count after tactical filtering. Assuming expected constant-time hash operations, construction plus a run costs
$$O\left(V+E+D+N+\sum_v\left(g_v+k_v\log(k_v+1)\right)\right)$$
with ordering enabled; remove sorting work when ordering is disabled. The node budget bounds $N$, not move-list scanning, sorting, allocation, or elapsed time. The run initializes $O(V)$ hints, records $O(D)$ history, and can store at most $V(D+1)(Q+1)$ distinct horizon keys. Each stack frame materializes candidate indices, so auxiliary space is $O(V+T+D+bH)$, excluding the copied input graph of size $O(V+E)$. A lazy move iterator without full sorting can reduce active traversal storage toward $O(H)$ plus the table and other state. A transposition table has a real memory cost; it does not preserve a pure depth-only space bound.
Correctness of minimax and pruning. Terminal/base cases equal the specified evaluation model. At a normal node, induction over remaining depth makes max/min backup its minimax value; the same induction over $q$ handles the stand-pat-plus-tactical quiescence model. At a MAX node, an already established score at least beta makes remaining children irrelevant to an ancestor MIN alternative. At a MIN node, a score at most alpha similarly makes its remaining replies irrelevant to an ancestor MAX alternative. A cutoff can therefore return a directional bound without knowing the exact subtree value.
Every table entry preserves that proof: LOWER is at most the true value, UPPER is at least the true value, and EXACT equals it for the same state and horizon. Returning a compatible entry is then as safe as the corresponding completed search or cutoff. Matching horizons and fixed evaluation policy are essential. Full-window root searches cannot fail outside the score range, so a completed root result is exact for its model. Strict-improvement updates select an optimal root action, though tied choices can change with ordering.
Finally, each child call restores its parent's active path even while cancellation propagates. No interrupted ancestor stores an incomplete result; a root iteration is committed only after normal return. Thus a partial search preserves a previously proven depth-limited result. This proves the implementation's evaluation-tree result, not that the heuristic predicts the eventual winner or that the latest score must improve monotonically with depth.
| Approach | Time / work model | Auxiliary search space | Key assumption or tradeoff |
|---|---|---|---|
| Full depth-limited minimax | $O(b^d)$ for constant $b\gt1$ | $O(d)$ with lazy successors | Exact for a fixed depth/evaluation model; examines every branch |
| Alpha-beta, unfavorable ordering | Worst case $O(b^d)$ | $O(d)$ with lazy successors | Same completed minimax value, potentially little pruning |
| Alpha-beta, ideal uniform-tree ordering | Best-order scale $O(b^{d/2})$ for fixed $b$ | $O(d)$ with lazy successors | Requires highly favorable child order; equality permits cutoffs |
| Iterative deepening plus bounded quiescence | Conservative full-tree $O(b^{D+Q})$ for $b\gt1$ | Active line at most $D+Q$ | Last completed depth available; extensions use an explicit policy |
| This implementation with table and sorting | Measured bound in prose, with $N$ admissions and candidate sorting | $O(V+T+D+b(D+Q))$ excluding copied graph | Expected hash access; key includes both horizons; table may save repeated work |
| Exact dynamic programming on the explicit acyclic model | $O((D+1)(Q+1)(V+E))$ to fill all horizon states | $O(V(D+1)(Q+1))$ | Often preferable for this small explicit DAG; computes every subproblem rather than pruning |
| Expectiminimax | Usually exponential in decision/chance branching | Depth-proportional with lazy traversal | Adds probability-weighted chance nodes; ordinary max/min backup alone is insufficient |
| Monte Carlo tree search | Depends on simulation budget, policy, and tree growth | Grows with retained simulation tree | Allocates trials selectively; finite-budget estimates do not supply alpha-beta's fixed-tree exactness |
Before returning, every recursive call restores its caller's active path and returns either the exact value for its state/horizon or a sound window bound. Cache flags preserve that distinction, alpha-beta discards only decision-irrelevant branches, and the driver commits only a completed full-window root result.
Variants, Pitfalls, and Optimizations
A bound is not an exact score. Suppose a MIN node is searched with alpha $6$. Its first reply is worth $5$, so it cuts off before another reply worth $3$. The returned $5$ proves only an upper bound; the true value is $3$. Later, if a MAX root already has alternative $4$, reusing EXACT 5 would incorrectly prefer this branch. A correct UPPER 5 entry cannot cut at alpha $4$, so the node is searched again. The diagram makes that failure visible.
Horizon and state identity matter. A board arrangement alone may omit side to move, castling/en-passant rights, repetition history, or progress counters. A transposition key must distinguish everything that can change legal moves, utility, or evaluation. The toy DAG excludes repetition; adding a generic visited set would not implement draw rules. If an engine uses Zobrist hashes, apply/undo must restore the hash as well as the board, and collision checks or another explicit collision policy are required. Root-relative mate-distance scores may also need normalization when stored and retrieved at different plies; this example uses position-only bounded scores.
Use depth information carefully. A cached shallow score cannot answer a deeper search. Engines often use entries searched to at least a requested depth under their practical search policy, but that should not be confused with equality of two fixed heuristic horizons: $V_1(s)$ can differ from $V_2(s)$. The code deliberately requires exact $(d,q)$ equality for value reuse. It retains move hints across horizons, verifies candidate legality by generating the current list, and never treats the stored move of a bound as an exact continuation.
Quiescence reduces some errors and introduces its own policy. In the tactical fixture, move $0$ has static value $8$ but allows a tactical reply producing $-5$; move $1$ has static value $3$ but its tactical reply yields $2$. At normal depth $1$, no extension chooses move $0$ with score $8$. With one quiescence ply, MIN backs up $\min(8,-5)=-5$ and $\min(3,2)=2$, so MAX chooses move $1$ with score $2$. A hard tactical cap can still hide longer combinations, and a poor tactical filter can omit a decisive threat. Stand pat is not a legal substitute for an evasion when in check; a chess adapter must generate all required evasions and handle terminal/draw rules before static evaluation.
Negamax is a score-convention rewrite. In a zero-sum alternating game, side-to-move scores permit $N(s,d)=\max_a(-N(\operatorname{result}(s,a),d-1))$, with child window $(-\beta,-\alpha)$. These programs instead use MAX-oriented scores with explicit MAX and MIN nodes. Negating scores or windows halfway through that convention is a bug. Games with extra turns require the correct player transition, and chance or hidden-information games require different models rather than pretending every node is a fully observed MIN turn.
Advanced pruning needs separate reasoning. Killer/history heuristics, a previous principal variation, and transposition moves can improve ordering. Aspiration windows search around an earlier estimate but must widen and re-search on fail-low or fail-high before committing an exact root result. Principal variation search similarly relies on re-search rules. Null-move pruning, late-move reductions, and other selective techniques can change the searched model or require recovery searches; they do not follow automatically from the sound alpha-beta cutoff proof. Measure performance on varied positions: the sample's ordering heuristic increases work from $12$ to $13$ nodes.
Cancellation is not a score. Do not return $0$ as though a timed-out subtree were a draw. Propagate interruption separately, restore every applied move, retain only valid finished descendant entries, and return the last completed iteration. COMPLETE here means the requested depth budget was completed; it does not mean the whole underlying game was solved. A real clock limit also needs time checks, because admission count alone does not bound elapsed time.
Keep score perspective consistent, preserve EXACT/LOWER/UPPER flags with compatible state and horizons, and undo every move during cancellation. Treat heuristic leaves and bounded quiescence as a specified approximation, and never overwrite the last completed root answer with an interrupted result.
Practice Problems and Real-World Use
Begin with the worked tree and verify that exhaustive minimax and alpha-beta return score $3$. Then independently compute values for every state and horizon on a small acyclic graph using reverse-topological dynamic programming. This makes a useful oracle because it uses neither search windows nor move-order pruning. Test selected root moves for optimality, rather than demanding one fixed index among equal-valued alternatives across different orderings.
The supplied implementations were checked on 14,841 inputs per language: $13{,}624$ valid iterative searches with both cache and ordering independently on/off, $1{,}200$ warm-cache probe sequences containing $8{,}400$ narrow/full-window queries, and $17$ invalid inputs. The corpus includes exhaustive four-state graph/score combinations, randomized trees and transposition graphs, negative and tied scores, duplicate moves, tactical extensions, maximum bounds, and interrupted passes. An independent dynamic-programming oracle checked values, optimal root choices, and every audited directional bound. There were $302{,}380$ stored-entry bound checks, $342{,}022$ return-window checks, and $350{,}560$ path-restoration checks. C++ and Java matched Python's statuses, scores, moves, counters, histories, and cache digests; repeated runs agreed, and the C++ corpus passed address/undefined-behavior instrumentation.
For primary background, consult the AIMA authors' adversarial-search chapter. For a production implementation to study separately, see Stockfish's search source, including its quiescence and bound-handling logic. Its additional rules and selective techniques go well beyond this demonstration; the recurrence, exact horizon keys, resource contract, trace, and validation here are specified directly in the lesson.
| Practice problem or system | Concrete task | Idea tested | Difficulty |
|---|---|---|---|
| Two-ply adversarial tree | Reproduce the twelve-call iterative trace and both skipped replies | Minimax backup, alpha/beta changes, bound flags | Easy |
| Tic-tac-toe solver | Generate legal positions, terminal wins/draws, and compare full minimax with alpha-beta | Complete states, exact terminal utilities, optimal move choice | Medium |
| Move-order benchmark | Shuffle replies and compare static ordering with previous-best-first | Same completed value, different work and tied moves | Medium |
| Shared-position DAG | Reach one complete state by two move sequences at the same horizon | Transposition reuse, exact keys, directional bounds | Medium |
| Horizon trap | Reproduce the $8\to-5$ tactical reversal and the alternative worth $2$ | Quiescence policy and depth-zero instability | Medium |
| Interruptible search service | Abort at every possible admission boundary in a small game | Apply/undo safety and last completed iteration | Hard |
| Chess-engine source study | Inspect legal evasions, rule-dependent keys, table flags, and move ordering | Differences between a teaching model and a full engine | Hard |
| Stochastic-game comparison | Add a dice outcome node and compare expectation with worst-case backup | Why chance requires expectiminimax rather than ordinary MIN | Hard |