UNIT 04 • ADVERSARIAL SEARCH & GAME THEORY

Adversarial Search & Game Playing

An engineering deep-dive into multi-agent decision environments: formal two-player zero-sum game trees, optimal Minimax strategies, Alpha-Beta pruning mathematics and move ordering, static evaluation functions with quiescence search, Expectimax for stochastic games, and Monte Carlo Tree Search (MCTS) foundations.

📖 Standard: Russell & Norvig (AIMA 4th Edition)
⏱️ Study Depth: 6 Hours • Theory & Python Implementation
💻 Implementation: Python 3.12 (Minimax, Alpha-Beta, MCTS)
🎯 Verification: 100% Pure Natural English Language
02

Game Tree Topology & Combinatorial Scale

The state space of a game unfolds into a Game Tree: a directed tree where the root node is the initial board state, edges represent legal moves, and nodes at alternating levels represent turns taken by player MAX and player MIN.

Topology of an Alternating Two-Player Game Tree
                      Level 0: MAX Root Node
                             [  MAX  ]
                            /    |    \
                     Move A/   B/      \C
                          v     v       v
                      Level 1: MIN Nodes (Opponent's Turn)
                       (MIN)  (MIN)   (MIN)
                       /   \   / \     / \
                      v     v v   v   v   v
                      Level 2: MAX Nodes (Agent's Turn)
                      [ ]   [ ]...    ... [ ]
                       |     |             |
                       v     v             v
                      Terminal Nodes (Utility Payoffs)
                      +3    -5   0    +9  -2  +1
⚡ Terminology: Plies vs. Moves & The Combinatorial Wall

In game theory and AI, a Ply refers to a single half-move (one turn taken by one player). Two plies make up one full round or move (one move by White followed by one move by Black).

The primary challenge in game-playing AI is the catastrophic combinatorial explosion of the game tree:

Game Domain Branching Factor (b) Average Depth (d in plies) Game Tree Size (b^d) Feasibility of Exhaustive Search
Tic-Tac-Toe ~4 (starts at 9, decreases) 9 plies maximum ~362,880 nodes (5,478 legal states) Trivially Solvable: Fits in fractions of a megabyte of RAM.
Checkers (8x8) ~8 to 10 ~70 plies ~10^40 nodes Weakly Solved: Solved in 2007 by Jonathan Schaeffer's Chinook team.
Chess ~35 ~80 plies ~10^123 nodes (Shannon Number) Intractable: More nodes than atoms in the observable universe (~10^80).
Go (19x19) ~250 ~150 plies ~10^360 nodes Hyper-Intractable: Exhaustive search is fundamentally impossible.
03

The Minimax Algorithm & Optimal Backed-Up Decisions

The fundamental decision algorithm for deterministic two-player zero-sum games is the Minimax Algorithm, formulated by John von Neumann in 1928.

🧠 The Minimax Philosophy

Minimax assumes an optimal, rational adversary. When choosing our move:

  • MAX wants to navigate to states with the highest possible utility.
  • MIN wants to navigate to states with the lowest possible utility (maximizing MIN's advantage).
  • Therefore, MAX must choose the action that maximizes the minimum utility that the opponent can force upon us!
Mathematical Minimax Decision Rule:
For any state s:
- If Terminal-Test(s) is True: MinimaxValue(s) = Utility(s)
- If Player(s) == MAX: MinimaxValue(s) = Maximum of MinimaxValue(Result(s, a)) over all a in Actions(s)
- If Player(s) == MIN: MinimaxValue(s) = Minimum of MinimaxValue(Result(s, a)) over all a in Actions(s)
Trace of Minimax Value Propagation (Bottom-Up Backing Up)
   Step 1: Terminal evaluation gives leaf values: [3, 12, 8] and [2, 4, 6]
   Step 2: MIN nodes select MINIMUM of children:
           Left MIN node:  min(3, 12, 8) = 3
           Right MIN node: min(2, 4, 6)  = 2
   Step 3: MAX root node selects MAXIMUM of children:
           MAX root:       max(3, 2)     = 3 (Choose Left Action!)

                      [ MAX = 3 ]  <-- Root Chooses Action Left!
                        /      \
                       /        \
              ( MIN = 3 )      ( MIN = 2 )
               /   |   \        /   |   \
              v    v    v      v    v    v
             [3]  [12] [8]    [2]  [4]  [6]  <-- Leaf Utilities
📏 Theoretical Properties of Minimax
  • Completeness: Yes, if the game tree is finite.
  • Optimality: Yes, against an optimal adversary. If the opponent plays sub-optimally, Minimax is guaranteed to perform at least as well as against an optimal opponent (though it may not exploit the opponent's specific blunder as aggressively as a tailored model).
  • Time Complexity: O(b^m) where b is the legal branching factor and m is the maximum tree depth. It conducts a complete depth-first search of the game tree.
  • Space Complexity: O(b * m) because it explores via depth-first recursion, only keeping the current branch in memory.
04

Alpha-Beta Pruning: Doubling Search Depth

The computational bottleneck of Minimax is that it visits every single node in the game tree. In practice, large subtrees can be mathematically proven to be completely irrelevant to the root decision. Alpha-Beta Pruning is an algorithmic optimization that returns the exact same mathematical Minimax value while pruning away massive redundant subtrees without evaluating them!

📐 The Two Bounding Parameters: Alpha & Beta

Alpha and Beta represent the dynamic bounds of the backed-up values established so far along the current path:

  • Alpha (α): The value of the best (highest-value) choice found so far at any choice point along the path for MAX. Initialized at the root to -Infinity.
  • Beta (β): The value of the best (lowest-value) choice found so far at any choice point along the path for MIN. Initialized at the root to +Infinity.
The Fundamental Pruning Rule:
Whenever Alpha >= Beta at any node, PRUNE IMMEDIATELY!
No further children of this node need to be evaluated.
Step-by-Step Anatomy of an Alpha-Beta Prune
                          [ Root: MAX ] (alpha = -inf, beta = +inf)
                          /           \
                         /             \
                  ( Node B: MIN )     ( Node C: MIN )
                   /           \               |
                  v             v              v
               Leaf 1         Leaf 2        Leaf 3
               Val = 3        Val = 5       Val = 2

   1. Left branch explores Leaf 1 (3) and Leaf 2 (5).
      Node B (MIN) takes min(3, 5) = 3.
   2. Back at Root (MAX), alpha updates to max(-inf, 3) = 3.
      Root now knows: "I can guarantee a score of AT LEAST 3 by going left."
   3. Root investigates Node C with alpha = 3, beta = +inf.
   4. Node C explores Leaf 3 with value 2.
      Node C (MIN) now has a value of at most 2 (beta updates to 2).
   5. Check condition: alpha (3) >= beta (2) is TRUE!
      --> PRUNE all remaining children of Node C!
      Reason: Node C will never choose anything greater than 2.
              Root (MAX) already has a move guaranteeing 3.
              Root will NEVER choose Node C! Evaluating Node C's remaining
              children is a complete waste of computation.
⚡ The Crucial Role of Move Ordering

The effectiveness of Alpha-Beta pruning depends entirely on the order in which moves are examined:

  • Worst-Case Move Ordering (Worst moves examined first): No branches are pruned. Complexity remains O(b^m), identical to unpruned Minimax.
  • Best-Case Move Ordering (Best moves examined first): Complexity drops to:
    Time Complexity (Best Case) = O(b^(m / 2)) = O((sqrt(b))^m)
    Engineering Impact: Alpha-Beta can search twice as deep in the exact same computation time! For chess, the effective branching factor drops from 35 to approximately sqrt(35) ≈ 6.

Practical Move Ordering Techniques Used in Game Engines:

  1. Killer Move Heuristic: Try moves that caused a beta-cutoff in a sibling node at the same depth.
  2. Capture First: Order captures by Most Valuable Victim – Least Valuable Attacker (e.g., pawn capturing queen).
  3. Transposition Tables: Use large hash tables (Zobrist Hashing) to store previously evaluated board states; retrieve the best move from earlier shallow searches.
  4. Iterative Deepening: Search to depth 1, sort moves by score, then search depth 2 using that sorted order, repeated up to the time limit.
05

Evaluation Functions & Search Cutoffs

Because games like Chess have trees of depth 80+, reaching terminal states is impossible during real gameplay. Real-world engines replace Terminal-Test with a Depth Cutoff Test and substitute the true Utility with an Heuristic Evaluation Function Eval(s).

📐 Designing a Static Evaluation Function

An evaluation function estimates the winning probability or expected payoff of a non-terminal board position. Most classical engines compute a weighted linear combination of board features:

Eval(s) = w1 * f1(s) + w2 * f2(s) + w3 * f3(s) + ... + wn * fn(s)
where fi(s) is a numerical feature extracted from state s, and wi is its relative strategic weight.

Standard Chess Evaluation Features:

  • Material Value: Standard piece weights: Pawn = 1, Knight = 3, Bishop = 3.25, Rook = 5, Queen = 9, King = 10,000.
  • Piece-Square Tables: Assigning bonuses for pieces positioned on strategically active squares (e.g., knights in the center vs. knights on the edge).
  • Mobility: The count of legal moves available. High mobility indicates space control and offensive freedom.
  • Pawn Structure: Penalties for isolated, doubled, or backward pawns; large bonuses for advanced "passed pawns".
  • King Safety: Castle status, open files shielding the king, and proximity of enemy attacking pieces.
⚠️ The Horizon Effect

Occurs when an unavoidable catastrophic loss (e.g., losing a queen) is pushed just beyond the search cutoff depth by a sequence of stalling, delay tactics (e.g., unnecessary spite checks or piece sacrifices). The engine thinks it avoided the disaster, only to suffer an even worse defeat once the horizon advances.

🛡️ Quiescence Search

Evaluating a position during an active piece exchange (e.g., queen just took rook, opponent about to recapture) yields a wildly inaccurate score. Quiescence Search extends search past the depth cutoff for non-quiet (volatile) moves (captures, checks, promotions) until a calm position is reached before applying Eval(s).

07

Monte Carlo Tree Search (MCTS) & Modern Game AI

In games like Go, the branching factor is immense (~250), and writing an accurate static heuristic evaluation function is nearly impossible because a single stone's influence radiates across the entire board. Monte Carlo Tree Search (MCTS) revolutionized game AI by substituting heuristic evaluation with statistical sampling of simulated random playouts.

🔄 The Four Iterative Phases of MCTS
1. Selection

Starting at root, traverse down the existing tree using the UCT formula (Upper Confidence Bound for Trees) to balance exploitation of winning moves against exploration of less-visited moves until an unexpanded node is reached.

2. Expansion

Unless the selected node is a terminal state, create one or more legal child nodes and attach them to the search tree.

3. Simulation (Rollout)

From the newly expanded child, simulate a rapid "rollout" game to completion by picking moves (randomly or via lightweight heuristics) until a terminal win/loss outcome is reached.

4. Backpropagation

Propagate the result (win = +1, loss = 0) back up along the path of visited nodes from the leaf to the root. Increment total visit count N and accumulated score Q for each ancestor.

The UCT Selection Formula (Balancing Exploitation and Exploration):
UCT(s, a) = (Q(s, a) / N(s, a)) + C * sqrt( ln(N(s)) / N(s, a) )
- Exploitation Term: Q(s, a) / N(s, a) — The average win rate of move a.
- Exploration Term: C * sqrt(ln(N(s)) / N(s, a)) — Gives a bonus to actions that have been visited rarely relative to parent visits.
- Constant C: Exploration parameter (theoretically sqrt(2) ≈ 1.414).
🏆 Modern AlphaZero Architecture

DeepMind's AlphaGo and AlphaZero replaced the slow, noisy random rollouts of classical MCTS with dual-headed deep convolutional neural networks:
1. Policy Head: Predicts high-probability candidate moves to guide MCTS Selection and Expansion.
2. Value Head: Directly evaluates the winning probability of any board state, eliminating the need for random rollouts entirely!

08

Case Studies: Tic-Tac-Toe & Chess Applications

⭕ Tic-Tac-Toe: Analytical Solvability

With 9 squares and 3 states per square (X, O, blank), the upper bound of board states is 3^9 = 19,683. Factoring in turn constraints and symmetry reductions (rotations and reflections), there are only 765 fundamentally unique board states.

An unpruned Minimax search explores 549,946 game states. With Alpha-Beta pruning, the search visits approximately 18,000 states. Under optimal Minimax play by both agents, every game terminates in a draw.

♟️ Chess: The Evolution of Mastery
  • Deep Blue (1997): Defeated World Champion Garry Kasparov using massively parallel custom VLSI chips capable of evaluating 200 million chess positions per second using Alpha-Beta search with hand-crafted heuristic evaluation functions and endgame tablebases.
  • Stockfish (Present): Open-source champion combining deep Alpha-Beta pruning, transposition tables, and NNUE (Efficiently Updatable Neural Networks) evaluating millions of nodes per second on consumer CPUs.
  • AlphaZero (2017): Mastered Chess, Shogi, and Go from scratch in 24 hours purely through self-play reinforcement learning and MCTS without any human opening books or heuristic tables.
09

Complete Python Implementation: Minimax & Alpha-Beta Engine

Below is a complete, standalone Python implementation of a Tic-Tac-Toe AI Engine comparing pure Minimax versus Alpha-Beta Pruning, tracking and proving the dramatic reduction in evaluated search nodes.

game_playing_ai.py — Minimax & Alpha-Beta Comparison
"""
AI Track Unit 4: Game Playing & Adversarial Search Engine
Minimax vs. Alpha-Beta Pruning Comparison on Tic-Tac-Toe
Author: DevAscent Engineering Team
"""

from typing import List, Tuple, Optional
import math

Board = List[List[str]]

class TicTacToeGame:
    def __init__(self):
        self.reset()
        self.minimax_nodes_evaluated = 0
        self.alphabeta_nodes_evaluated = 0

    def reset(self):
        self.board: Board = [[' ' for _ in range(3)] for _ in range(3)]

    def print_board(self):
        print("\n  0   1   2")
        for idx, row in enumerate(self.board):
            print(f"{idx} " + " | ".join(row))
            if idx < 2:
                print("  ---------")
        print()

    def get_legal_actions(self) -> List[Tuple[int, int]]:
        actions = []
        for r in range(3):
            for c in range(3):
                if self.board[r][c] == ' ':
                    actions.append((r, c))
        return actions

    def check_terminal(self) -> Tuple[bool, Optional[str]]:
        """Returns (is_terminal, winner) where winner is 'X', 'O', 'Draw', or None."""
        # Check rows & columns
        for i in range(3):
            if self.board[i][0] == self.board[i][1] == self.board[i][2] != ' ':
                return True, self.board[i][0]
            if self.board[0][i] == self.board[1][i] == self.board[2][i] != ' ':
                return True, self.board[0][i]

        # Check diagonals
        if self.board[0][0] == self.board[1][1] == self.board[2][2] != ' ':
            return True, self.board[0][0]
        if self.board[0][2] == self.board[1][1] == self.board[2][0] != ' ':
            return True, self.board[0][2]

        # Check for draw or ongoing
        if len(self.get_legal_actions()) == 0:
            return True, 'Draw'
        return False, None

    # =================================================================
    # 1. PURE MINIMAX ALGORITHM
    # =================================================================
    def minimax(self, is_max_turn: bool) -> int:
        self.minimax_nodes_evaluated += 1
        is_term, winner = self.check_terminal()
        if is_term:
            if winner == 'X':
                return 1   # MAX wins
            elif winner == 'O':
                return -1  # MIN wins
            return 0       # Draw

        if is_max_turn:
            best_val = -math.inf
            for r, c in self.get_legal_actions():
                self.board[r][c] = 'X'
                val = self.minimax(is_max_turn=False)
                self.board[r][c] = ' '
                best_val = max(best_val, val)
            return best_val
        else:
            best_val = math.inf
            for r, c in self.get_legal_actions():
                self.board[r][c] = 'O'
                val = self.minimax(is_max_turn=True)
                self.board[r][c] = ' '
                best_val = min(best_val, val)
            return best_val

    # =================================================================
    # 2. ALPHA-BETA PRUNING ALGORITHM
    # =================================================================
    def alpha_beta(self, alpha: float, beta: float, is_max_turn: bool) -> int:
        self.alphabeta_nodes_evaluated += 1
        is_term, winner = self.check_terminal()
        if is_term:
            if winner == 'X':
                return 1
            elif winner == 'O':
                return -1
            return 0

        if is_max_turn:
            best_val = -math.inf
            for r, c in self.get_legal_actions():
                self.board[r][c] = 'X'
                val = self.alpha_beta(alpha, beta, is_max_turn=False)
                self.board[r][c] = ' '
                best_val = max(best_val, val)
                alpha = max(alpha, best_val)
                if alpha >= beta:
                    break  # Beta Cutoff
            return best_val
        else:
            best_val = math.inf
            for r, c in self.get_legal_actions():
                self.board[r][c] = 'O'
                val = self.alpha_beta(alpha, beta, is_max_turn=True)
                self.board[r][c] = ' '
                best_val = min(best_val, val)
                beta = min(beta, best_val)
                if alpha >= beta:
                    break  # Alpha Cutoff
            return best_val

    def find_best_move_alphabeta(self, player: str) -> Tuple[int, int]:
        is_max = (player == 'X')
        best_val = -math.inf if is_max else math.inf
        best_move = (-1, -1)
        alpha = -math.inf
        beta = math.inf

        for r, c in self.get_legal_actions():
            self.board[r][c] = player
            val = self.alpha_beta(alpha, beta, is_max_turn=not is_max)
            self.board[r][c] = ' '

            if is_max and val > best_val:
                best_val = val
                best_move = (r, c)
                alpha = max(alpha, best_val)
            elif not is_max and val < best_val:
                best_val = val
                best_move = (r, c)
                beta = min(beta, best_val)

        return best_move

# =====================================================================
# BENCHMARK AND EFFICIENCY DEMONSTRATION
# =====================================================================
if __name__ == "__main__":
    game = TicTacToeGame()

    print("=" * 68)
    print("DEMO 1: EFFICIENCY BENCHMARK — MINIMAX VS. ALPHA-BETA PRUNING")
    print("=" * 68)

    # Set up an intermediate board position to measure search nodes
    # X in center, O in top-left
    game.board[1][1] = 'X'
    game.board[0][0] = 'O'
    print("Benchmark Board State (7 Empty Squares Remaining):")
    game.print_board()

    # 1. Run Pure Minimax
    game.minimax_nodes_evaluated = 0
    score_mm = game.minimax(is_max_turn=True)
    nodes_mm = game.minimax_nodes_evaluated

    # 2. Run Alpha-Beta Pruning
    game.alphabeta_nodes_evaluated = 0
    score_ab = game.alpha_beta(-math.inf, math.inf, is_max_turn=True)
    nodes_ab = game.alphabeta_nodes_evaluated

    print(f"Pure Minimax Evaluated    : {nodes_mm:,} nodes (Score: {score_mm})")
    print(f"Alpha-Beta Pruning Evaluated: {nodes_ab:,} nodes (Score: {score_ab})")
    pruned_percent = ((nodes_mm - nodes_ab) / nodes_mm) * 100
    print(f"Pruning Efficiency Gain   : {pruned_percent:.1f}% reduction in explored states!")
    print("Mathematical Proof: Both algorithms yield the exact identical score.")

    print("\n" + "=" * 68)
    print("DEMO 2: OPTIMAL MOVE SELECTION")
    print("=" * 68)
    best_action = game.find_best_move_alphabeta('X')
    print(f"Recommended Optimal Action for 'X' from this state: Row {best_action[0]}, Col {best_action[1]}")
10

Viva Voce & Engineering Interview Questions

Q1: What is a Zero-Sum game, and why is Minimax strictly applicable to it?

Answer: A game is zero-sum if the total sum of utilities across all players equals zero in every terminal state: Utility(MAX) + Utility(MIN) = 0. An advantage gained by one player represents an identical, exact loss for the opponent. Minimax applies directly because when MIN attempts to minimize MAX's payoff, MIN is simultaneously and optimally maximizing its own payoff. In non-zero-sum games (e.g., Prisoner's Dilemma or cooperative economies), players can have win-win or lose-lose outcomes, requiring Nash Equilibrium or cooperative game theory instead of Minimax.

Q2: Explain why Alpha-Beta pruning is guaranteed to return the exact same decision as pure Minimax without altering the outcome.

Answer: Alpha-Beta pruning only prunes a subtree when it is mathematically proven that the root decision will never select it. Specifically, if a MIN node discovers a move with value v <= α, the ancestor MAX node already has an alternative path guaranteeing at least α. MAX will therefore never choose the branch leading to this MIN node. Because the pruned branches can never influence the backed-up value at the root, discarding them does not alter the final optimal decision in any way.

Q3: What is the optimal time complexity of Alpha-Beta pruning, and what prerequisite is required to achieve it?

Answer: Under optimal move ordering (where the best move is examined first at every choice point), the time complexity of Alpha-Beta pruning drops from O(b^m) to O(b^(m/2)). This means the algorithm can search twice as deep within the same CPU time budget. To achieve or approach this optimal ordering, engines employ heuristics such as transposition tables (Zobrist hashing), killer move heuristics, history heuristics, and iterative deepening.

Q4: What is the Horizon Effect, and how does Quiescence Search solve it?

Answer: The Horizon Effect occurs when an engine faces an unavoidable severe loss (such as losing its queen), but delays it past its fixed search horizon by executing useless delaying tactics (such as throwing away checks). The engine falsely believes it avoided the disaster. Quiescence Search prevents this pathology by not stopping search at a rigid cutoff depth if the position is volatile (unquiet). It extends the search along tactical lines (captures, checks, promotions) until a calm, stable board configuration is reached before calling the static evaluation function.

Q5: How does Expectimax differ from Minimax, and why do monotonic utility transformations break Expectimax?

Answer: In Expectimax, chance nodes compute the expected value (weighted probability sum) of their children instead of taking a minimum. In Minimax, applying any order-preserving monotonic transformation (such as multiplying by 2 or adding 100) preserves decisions because max(A, B) depends only on rank order. In Expectimax, probabilities take linear combinations: 0.9 * 10 + 0.1 * 100 = 19, whereas squaring utilities changes the comparison: 0.9 * 100 + 0.1 * 10,000 = 1090. The exact scale and shape of the utility function directly encodes the agent's risk aversion.

Q6: Describe the 4 stages of Monte Carlo Tree Search (MCTS) and explain the role of UCT.

Answer: The four stages are:
1. Selection: Traverse existing tree using UCT until an unexpanded node is reached.
2. Expansion: Add child node(s) to the tree.
3. Simulation: Run a rapid randomized rollout to the terminal state.
4. Backpropagation: Propagate the outcome up the tree to update visit counts and win rates. The UCT formula balances exploitation (picking moves with high average win rate) with exploration (giving bonuses to rarely visited branches), guaranteeing asymptotic convergence to the optimal Minimax decision.