Patterns/Part VI - Math & Discrete/Game Theory

Pattern Reference

Game Theory

"Nim, Grundy numbers, Sprague-Grundy theorem, impartial games, minimax."

Loading...

Deep Dive Tutorial

Combinatorial game theory analyzes two-player zero-sum games where players alternate moves and the last to move wins (normal play convention). The breakthrough insight: any position's winning/losing status can be computed by XOR of "Grundy values." Once you can compute Grundy values, you can solve arbitrary game combinations.

Nim Game — The Foundation

key
Nim theorem (Sprague-Grundy for Nim): Given piles of stones [n1, n2, ..., nk], the current player LOSES if and only if XOR(n1, n2, ..., nk) === 0.

If XOR ≠ 0, current player can always move to XOR = 0 (winning position for opponent becomes losing). If XOR = 0, every move creates XOR ≠ 0 (opponent now wins).
Nim winner
function nimWinner(piles) {
    // XOR all pile sizes
    // 0 → current player loses (all moves put opponent in winning position)
    // non-zero → current player wins
    return piles.reduce((xor, p) => xor ^ p, 0) !== 0;
}

Sprague-Grundy Theorem

lightbulb
Grundy value (nimber) G(position):
- G = 0: LOSING position (previous player wins, i.e., you lose)
- G > 0: WINNING position

G(pos) = mex({G(next) : next ∈ moves(pos)})
where mex = minimum excludant = smallest non-negative integer NOT in the set.

For combined games: G(game1 + game2) = G(game1) XOR G(game2).
Sprague-Grundy values with memoization
function computeGrundy(n, moves) {
    const memo = new Map();
    function grundy(pos) {
        if (memo.has(pos)) return memo.get(pos);
        const reachable = new Set();
        for (const move of moves) {
            if (pos - move >= 0) reachable.add(grundy(pos - move));
        }
        // mex: find smallest non-negative integer not in reachable
        let mex = 0;
        while (reachable.has(mex)) mex++;
        memo.set(pos, mex);
        return mex;
    }
    return grundy(n);
}

// Example: game where you can take 1, 2, or 3 stones
// grundy(0)=0 (lose), grundy(1)=1, grundy(2)=2, grundy(3)=3, grundy(4)=0 (pattern!)
// Pattern: Grundy(n) = n % 4 for moves {1,2,3}

Worked Problems

More Worked Problems

lightbulb
Pattern recognition for game problems:
- "Take 1..k stones from pile" → Nim / Sprague-Grundy
- "Last player to move wins" → normal play convention, compute Grundy
- "Multiple independent games simultaneously" → XOR of Grundy values
- "Optimal play both sides" → usually minimax DP or Grundy
- "Score difference for current player" → dp[i][j] = score_diff on range [i..j]