Game Theory

Game theory analyzes strategic decision-making in competitive scenarios. Understanding winning and losing positions is crucial for solving game-based algorithmic problems.

Basic Game Theory Concepts

Game theory studies mathematical models of strategic interaction between rational decision-makers. In competitive programming, we focus on two-player, zero-sum, perfect information games.

Key Concepts:

  • Game State: Current configuration of the game
  • Winning Position: Player to move can force a win
  • Losing Position: Player to move will lose with optimal play
  • Terminal State: Game has ended

Determining Winning/Losing Positions


// Basic game state analysis
bool isWinning(int state, vector<int>& memo) {
    if(memo[state] != -1) return memo[state];
    
    // Check if this is a terminal (losing) state
    if(isTerminal(state)) {
        return memo[state] = false;
    }
    
    // Try all possible moves
    vector<int> nextStates = getNextStates(state);
    
    for(int nextState : nextStates) {
        // If any move leads to a losing position for opponent, this is winning
        if(!isWinning(nextState, memo)) {
            return memo[state] = true;
        }
    }
    
    // All moves lead to winning positions for opponent, so this is losing
    return memo[state] = false;
}
                    

The Nim Game

Nim is a fundamental game where players take turns removing objects from piles. The player who takes the last object wins.

Basic Nim Strategy

The key insight: A position is losing if and only if the XOR (nim-sum) of all pile sizes is 0.


// Determine if current Nim position is winning
bool isNimWinning(vector<int>& piles) {
    int nimSum = 0;
    for(int pile : piles) {
        nimSum ^= pile;
    }
    return nimSum != 0; // Non-zero nim-sum means winning position
}

// Find optimal move in Nim
pair<int, int> findNimMove(vector<int>& piles) {
    int nimSum = 0;
    for(int pile : piles) {
        nimSum ^= pile;
    }
    
    if(nimSum == 0) return {-1, -1}; // Already losing position
    
    // Find a pile where pile XOR nimSum < pile
    for(int i = 0; i < piles.size(); i++) {
        int newSize = piles[i] ^ nimSum;
        if(newSize < piles[i]) {
            return {i, piles[i] - newSize}; // Pile index, amount to remove
        }
    }
    
    return {-1, -1}; // Should not reach here
}
                    

Nim Strategy

In Nim, always try to make the nim-sum (XOR of all piles) equal to 0. If it's already 0, you're in a losing position with perfect play.

Sprague-Grundy Theorem

The Sprague-Grundy theorem allows us to analyze complex games by computing the Grundy number (nimber) of each position.

Computing Grundy Numbers


// Calculate Grundy number for a game state
int grundyNumber(int state, vector<int>& memo) {
    if(memo[state] != -1) return memo[state];
    
    // Terminal states have Grundy number 0
    if(isTerminal(state)) {
        return memo[state] = 0;
    }
    
    // Get all possible next states
    vector<int> nextStates = getNextStates(state);
    set<int> reachableGrundy;
    
    // Collect Grundy numbers of all reachable states
    for(int nextState : nextStates) {
        reachableGrundy.insert(grundyNumber(nextState, memo));
    }
    
    // Grundy number is the minimum excludant (mex)
    int mex = 0;
    while(reachableGrundy.count(mex)) {
        mex++;
    }
    
    return memo[state] = mex;
}

// For combined games, XOR the Grundy numbers
int combinedGrundy(vector<int>& gameStates, vector<vector<int>>& memos) {
    int result = 0;
    for(int i = 0; i < gameStates.size(); i++) {
        result ^= grundyNumber(gameStates[i], memos[i]);
    }
    return result;
}
                    

Minimax Algorithm

Minimax is used to find the optimal move in two-player games by assuming both players play optimally.


// Minimax algorithm for two-player games
int minimax(GameState state, int depth, bool isMaximizing) {
    // Terminal state or depth limit reached
    if(isTerminal(state) || depth == 0) {
        return evaluate(state); // Return game evaluation
    }
    
    vector<GameState> nextStates = getNextStates(state);
    
    if(isMaximizing) {
        int maxEval = INT_MIN;
        for(GameState nextState : nextStates) {
            int eval = minimax(nextState, depth - 1, false);
            maxEval = max(maxEval, eval);
        }
        return maxEval;
    } else {
        int minEval = INT_MAX;
        for(GameState nextState : nextStates) {
            int eval = minimax(nextState, depth - 1, true);
            minEval = min(minEval, eval);
        }
        return minEval;
    }
}

// Alpha-beta pruning optimization
int alphaBeta(GameState state, int depth, int alpha, int beta, bool isMaximizing) {
    if(isTerminal(state) || depth == 0) {
        return evaluate(state);
    }
    
    vector<GameState> nextStates = getNextStates(state);
    
    if(isMaximizing) {
        int maxEval = INT_MIN;
        for(GameState nextState : nextStates) {
            int eval = alphaBeta(nextState, depth - 1, alpha, beta, false);
            maxEval = max(maxEval, eval);
            alpha = max(alpha, eval);
            if(beta <= alpha) break; // Beta cutoff
        }
        return maxEval;
    } else {
        int minEval = INT_MAX;
        for(GameState nextState : nextStates) {
            int eval = alphaBeta(nextState, depth - 1, alpha, beta, true);
            minEval = min(minEval, eval);
            beta = min(beta, eval);
            if(beta <= alpha) break; // Alpha cutoff
        }
        return minEval;
    }
}
                    

Common Game Types

Subtraction Games

Players can remove 1, 2, or 3 objects from a pile:


// Subtraction game with allowed moves {1, 2, 3}
bool isSubtractionWinning(int n, vector<int>& memo) {
    if(memo[n] != -1) return memo[n];
    
    if(n == 0) return memo[n] = false; // Terminal losing state
    
    vector<int> moves = {1, 2, 3};
    for(int move : moves) {
        if(n >= move && !isSubtractionWinning(n - move, memo)) {
            return memo[n] = true; // Found winning move
        }
    }
    
    return memo[n] = false; // All moves lead to winning positions
}
                    

Green Hackenbush

A game played on trees where players remove edges:


// Calculate Grundy number for Green Hackenbush on trees
int hackenbushGrundy(int node, vector<vector<int>>& adj, vector<int>& memo, int parent = -1) {
    if(memo[node] != -1) return memo[node];
    
    int result = 0;
    
    // XOR Grundy numbers of all subtrees
    for(int child : adj[node]) {
        if(child != parent) {
            result ^= (hackenbushGrundy(child, adj, memo, node) + 1);
        }
    }
    
    return memo[node] = result;
}
                    

Advanced Game Theory

Partizan Games

Games where the set of available moves depends on which player is moving:


// Example: Domineering (players place dominoes differently)
struct GameValue {
    int left, right; // Values for left and right players
    
    bool isWinningForLeft() {
        return left >= 0;
    }
    
    bool isWinningForRight() {
        return right <= 0;
    }
};

// Combine partizan game values
GameValue combinePartizan(GameValue a, GameValue b) {
    return {a.left + b.left, a.right + b.right};
}
                    

Game Theory Tips

  • Always assume both players play optimally
  • Memoize game states to avoid recomputation
  • For Nim-like games, think about XOR operations
  • Use Sprague-Grundy theorem for complex combined games

CodeForces Challenge Problems

Practice game theory concepts:

Medium

Nim Games

Practice Nim-like games and winning position analysis.

Game Theory Nim
Solve Problem