Skip to main content

Using Game Theory and Minimax

1. Implementing Nim Game

Feature Description Implementation
Game State Represents the current number of stones and whose turn it is const gameState = { stones: 15, currentPlayer: 'player' };
Valid Moves Player can take 1, 2, or 3 stones function validMoves(stones) { return [1, 2, 3].filter(move => move <= stones); }
Minimax Function Recursively evaluates game states to find optimal move function minimax(state, isMaximizing) { ... }

Example: Nim Game Logic

function nimGame(stones, playerTurn) {
  if (stones === 0) return playerTurn === 'computer' ? 'player wins' : 'computer wins';

  const moves = [1, 2, 3].filter(move => move <= stones);
  const bestMove = moves.reduce((best, move) => {
    const newState = { stones: stones - move, currentPlayer: playerTurn === 'player' ? 'computer' : 'player' };
    return minimax(newState, false) > minimax(best, true) ? move : best;
  }, moves[0]);

  return `Player takes ${bestMove} stones`;
}
Note: The Nim game is a classic example of a zero-sum game where both players play optimally.

2. Calculating Grundy Numbers

Concept Description Formula Notes
Grundy Number Unique number assigned to each game state in combinatorial game theory G(s) = mex{G(s') : s' ∈ S(s)} Used in Sprague-Grundy theorem for game analysis
mex Minimum Excludant function mex(S) = min {k ∈ ℕ : k ∉ S} Smallest non-negative integer not in set S

Example: Grundy Number Calculation

function mex(s) {
    let i = 0;
    while (s.has(i)) i++;
    return i;
}

function calculateGrundy(state) {
    if (state.isTerminal()) return 0;

    let reachable = state.getMoves();
    let grundyNumbers = new Set();

    for (let move of reachable) {
        let next = state.applyMove(move);
        grundyNumbers.add(calculateGrundy(next));
    }

    return mex(grundyNumbers);
}

// Example: Nim game state with 3 piles [1, 2, 3]
let nimState = new NimState([1, 2, 3]);
let grundy = calculateGrundy(nimState);
console.log(grundy); // Output: 0 (losing position)
Note: Grundy numbers are essential for analyzing impartial games where each player has the same moves available. The Grundy number of a position is zero if and only if it's a losing position for the current player.

3. Implementing Sprague-Grundy Theorem

Concept Description Application
Game State Represents a position in a game with available moves Used to classify positions as winning or losing
Nimbers (Grundy Numbers) Unique numbers assigned to each game state Determines whether a position is equivalent to a Nim heap
Mex (Minimum Excludant) Smallest non-negative integer not present in a set Used to calculate Grundy numbers recursively

Example: Calculating Grundy Numbers

function mex(set) {
  let mex = 0;
  while (set.has(mex)) mex++;
  return mex;
}

function grundy(state, memo = new Map()) {
  if (memo.has(state)) return memo.get(state);
  
  const reachable = getReachableStates(state);
  const mexSet = new Set();
  
  for (const nextState of reachable) {
    mexSet.add(grundy(nextState, memo));
  }
  
  const result = mex(mexSet);
  memo.set(state, result);
  return result;
}

// Example: Nim game with heaps [1, 2, 3]
const nimState = [1, 2, 3];
const grundyValue = grundy(nimState);
console.log(grundyValue); // Output: 0 (losing position for current player)

Example: Game Outcome Determination

function isWinningPosition(state) {
  const grundyValue = grundy(state);
  return grundyValue !== 0;
}

function optimalMove(state) {
  const reachable = getReachableStates(state);
  const grundyValue = grundy(state);
  
  for (const nextState of reachable) {
    const nextGrundy = grundy(nextState);
    if ((grundyValue ^ nextGrundy) === 0) {
      return nextState;
    }
  }
  
  // If no winning move found, make any valid move
  return reachable[0];
}

// Example usage:
const game = [2, 3, 4];
if (isWinningPosition(game)) {
  console.log("Current player can force a win");
  const move = optimalMove(game);
  console.log("Optimal move:", move);
} else {
  console.log("Current player is in a losing position");
}
Note: The Sprague-Grundy theorem applies only to impartial games where both players have the same moves available from any position.
Warning: Memoization is crucial for performance when calculating Grundy numbers recursively to avoid exponential time complexity.

4. Solving Stone Game Variations

Game Variant Strategy Key Insight Time Complexity
Classic Stone Game Maximize own score while minimizing opponent's gain Use dynamic programming to compute optimal moves O(n^2)
Stone Game III Optimal play with 3 piles per turn Use minimax with memoization to decide best move O(n)
Stone Game VII Remove 1 or 2 stones from either end Dynamic programming with state (l, r) O(n^2)

Example: Stone Game III Solution

function stoneGameIII(stoneValue) {
    const n = stoneValue.length;
    const dp = new Array(n + 1).fill(0);
    
    for (let i = n - 1; i >= 0; i--) {
        dp[i] = stoneValue[i] + dp[i + 1];
        if (i + 1 < n) {
            dp[i] = Math.max(dp[i], stoneValue[i] + stoneValue[i + 1] + dp[i + 2]);
        }
        if (i + 2 < n) {
            dp[i] = Math.max(dp[i], stoneValue[i] + stoneValue[i + 1] + stoneValue[i + 2] + dp[i + 3]);
        }
    }

    return dp[0];
}
Note: For Stone Game III, the player chooses 1 to 3 piles in turn, and the goal is to maximize the difference between scores.

5. Implementing Minimax Algorithm

Component Description Implementation
Minimax Function Core recursive function to evaluate game states
function minimax(board, depth, isMaximizing) {
  const score = evaluate(board);
  if (score === 10 || score === -10) return score;
  if (isBoardFull(board)) return 0;

  if (isMaximizing) {
    let best = -Infinity;
    for (let i = 0; i < 3; i++) {
      for (let j = 0; j < 3; j++) {
        if (board[i][j] === "") {
          board[i][j] = "O";
          best = Math.max(best, minimax(board, depth + 1, false));
          board[i][j] = "";
        }
      }
    }
    return best;
  } else {
    let best = Infinity;
    for (let i = 0; i < 3; i++) {
      for (let j = 0; j < 3; j++) {
        if (board[i][j] === "") {
          board[i][j] = "X";
          best = Math.min(best, minimax(board, depth + 1, true));
          board[i][j] = "";
        }
      }
    }
    return best;
  }
}
Evaluation Function Heuristic to score board states
function evaluate(board) {
  // Check rows, columns, diagonals
  for (let i = 0; i < 3; i++) {
    if (board[i][0] === board[i][1] && board[i][1] === board[i][2]) {
      if (board[i][0] === "O") return 10;
      else if (board[i][0] === "X") return -10;
    }
    if (board[0][i] === board[1][i] && board[1][i] === board[2][i]) {
      if (board[0][i] === "O") return 10;
      else if (board[0][i] === "X") return -10;
    }
  }

  if (board[0][0] === board[1][1] && board[1][1] === board[2][2]) {
    if (board[0][0] === "O") return 10;
    else if (board[0][0] === "X") return -10;
  }

  if (board[0][2] === board[1][1] && board[1][1] === board[2][0]) {
    if (board[0][2] === "O") return 10;
    else if (board[0][2] === "X") return -10;
  }

  return 0;
}

Example: Minimax in Tic-Tac-Toe

const board = [
  ["", "", ""],
  ["", "", ""],
  ["", "", ""]
];

// AI plays as "O"
const bestMove = findBestMove(board);
console.log(bestMove); // {row: 1, col: 1}
Note: The minimax algorithm assumes optimal play from both players.
Warning: For large game trees, minimax can be computationally expensive. Consider alpha-beta pruning for optimization.

6. Implementing Alpha-Beta Pruning

Component Description Implementation
Alpha-Beta Pruning Optimization of minimax algorithm to reduce the number of nodes evaluated
function alphabeta(node, depth, alpha, beta, maximizingPlayer) {
    if (depth === 0 || node.isTerminal()) {
        return node.evaluate();
    }

    if (maximizingPlayer) {
        let maxEval = -Infinity;
        for (const child of node.getChildren()) {
            let eval = alphabeta(child, depth - 1, alpha, beta, false);
            maxEval = Math.max(maxEval, eval);
            alpha = Math.max(alpha, eval);
            if (beta <= alpha) break;
        }
        return maxEval;
    } else {
        let minEval = Infinity;
        for (const child of node.getChildren()) {
            let eval = alphabeta(child, depth - 1, alpha, beta, true);
            minEval = Math.min(minEval, eval);
            beta = Math.min(beta, eval);
            if (beta <= alpha) break;
        }
        return minEval;
    }
}

Example: Alpha-Beta Pruning in Action

const gameTree = {
  value: null,
  children: [
    { value: 3, children: [] },
    { value: 5, children: [] },
    { value: 2, children: [
      { value: 1, children: [] },
      { value: 4, children: [] }
    ]},
    { value: 8, children: [] }
  ]
};

// Evaluate with depth = 2, alpha = -Infinity, beta = Infinity
const result = alphabeta(gameTree, 2, -Infinity, Infinity, true);
console.log(result); // Output: 5
Note: Alpha-Beta pruning reduces the effective branching factor from b to sqrt(b), improving performance in game trees.

7. Solving Bash Game

The Bash Game is a classic algorithmic problem where two players take turns removing 1 to m stones from a pile of n stones. The player who removes the last stone wins.

Parameter Description Values
n Total number of stones in the pile Positive integer
m Maximum number of stones that can be removed in one turn Positive integer (≤ n)

The key to solving this game is to determine if the current player is in a winning or losing position. A position is winning if there exists at least one move that puts the opponent in a losing position.

Example: Determining Winning Position

function canWinBashGame(n, m) {
    // If n is divisible by (m + 1), the current player is in a losing position
    return n % (m + 1) !== 0;
}

// Example usage:
console.log(canWinBashGame(5, 2)); // true - First player can win
console.log(canWinBashGame(4, 2)); // false - First player is in a losing position

This solution leverages the mathematical property that if the number of stones is a multiple of (m + 1), the second player can always mirror the moves of the first player and ensure a win. Otherwise, the first player has a winning strategy.

Note: This approach assumes both players play optimally.

8. Finding Winning Strategies

Example: Minimax with Alpha-Beta Pruning

function minimaxWithAlphaBeta(board, depth, alpha, beta, isMaximizing) {
  const score = evaluateBoard(board);
  
  if (depth === 0 || isGameOver(board)) {
    return score;
  }

  if (isMaximizing) {
    let maxEval = -Infinity;
    for (let move of getValidMoves(board)) {
      board[move] = 'X';
      const eval = minimaxWithAlphaBeta(board, depth - 1, alpha, beta, false);
      board[move] = ' ';
      maxEval = Math.max(maxEval, eval);
      alpha = Math.max(alpha, eval);
      if (beta <= alpha) break;
    }
    return maxEval;
  } else {
    let minEval = Infinity;
    for (let move of getValidMoves(board)) {
      board[move] = 'O';
      const eval = minimaxWithAlphaBeta(board, depth - 1, alpha, beta, true);
      board[move] = ' ';
      minEval = Math.min(minEval, eval);
      beta = Math.min(beta, eval);
      if (beta <= alpha) break;
    }
    return minEval;
  }
}

function evaluateBoard(board) {
  const wins = [
    [0,1,2], [3,4,5], [6,7,8],
    [0,3,6], [1,4,7], [2,5,8],
    [0,4,8], [2,4,6]
  ];
  
  for (let win of wins) {
    const [a, b, c] = win;
    if (board[a] === board[b] && board[b] === board[c]) {
      if (board[a] === 'X') return 10;
      if (board[a] === 'O') return -10;
    }
  }
  return 0;
}

function isGameOver(board) {
  return evaluateBoard(board) !== 0 || !board.includes(' ');
}
Strategy Description Use Case Complexity
Minimax Recursive algorithm that explores all possible moves to find optimal play Two-player zero-sum games with perfect information O(b^m)
Alpha-Beta Pruning Optimization that reduces the number of nodes evaluated in minimax Games where move order matters (e.g., chess, tic-tac-toe) O(b^(d/2))
Monte Carlo Tree Search (MCTS) Uses random sampling to explore game tree and find winning strategies Games with large search spaces (e.g., Go, Poker) O(n^0.5) per simulation

Winning Strategy Components

  • Perfect Information: All game state is visible to both players
  • Zero-Sum Nature: One player's gain equals another's loss
  • Optimal Play: Each move maximizes the player's chance of winning
  • Search Depth: Determines how far ahead the algorithm looks

Strategy Comparison

  • Minimax: Guarantees optimal solution but can be slow
  • Alpha-Beta: Faster than minimax, still guarantees optimal
  • MCTS: Good for large search spaces but not guaranteed optimal

9. Implementing Expectiminimax

The expectiminimax algorithm is an extension of the minimax algorithm used in game theory to handle games with chance events (like dice rolls or card draws). It extends the minimax tree by incorporating probabilities for stochastic nodes.

Node Type Description Calculation
MAX Player nodes where the player chooses the best move Select max value from child nodes
MIN Opponent nodes where the opponent chooses the best move for them Select min value from child nodes
CHANCE Nodes representing random events (e.g., dice rolls) Expected value = Σ(probability × value of child node)

Example: Simple Dice Game

Consider a simple game where a player rolls a die and gets a reward based on the outcome. The player can choose to roll or stop.

function expectiminimax(node, depth) {
  if (depth === 0 || node.isTerminal()) {
    return node.evaluate();
  }

  if (node.type === 'MAX') {
    let maxEval = -Infinity;
    for (const child of node.getChildren()) {
      const eval = expectiminimax(child, depth - 1);
      maxEval = Math.max(maxEval, eval);
    }
    return maxEval;
  } else if (node.type === 'MIN') {
    let minEval = Infinity;
    for (const child of node.getChildren()) {
      const eval = expectiminimax(child, depth - 1);
      minEval = Math.min(minEval, eval);
    }
    return minEval;
  } else if (node.type === 'CHANCE') {
    let expectedValue = 0;
    const children = node.getChildren();
    for (const child of children) {
      const probability = child.getProbability();
      const eval = expectiminimax(child, depth);
      expectedValue += probability * eval;
    }
    return expectedValue;
  }
}
Note: The expectiminimax algorithm is particularly useful in games like poker, where chance events (card draws) influence the game state. It allows for optimal decision-making under uncertainty.

10. Solving Coin Game

Property Description Values
gameState Represents the current state of the coin game Array<number>
currentPlayer Indicates which player's turn it is 0 | 1
scores Stores the scores of both players [number, number]

Example: Coin Game State

const gameState = [1, 2, 3, 4, 5];
const currentPlayer = 0;
const scores = [0, 0];

Example: Minimax Algorithm

function minimax(state, depth, isMaximizing) {
  if (depth === 0 || state.length === 0) {
    return evaluate(state);
  }

  if (isMaximizing) {
    let maxEval = -Infinity;
    for (let i = 0; i < state.length; i++) {
      const newState = state.slice(0, i).concat(state.slice(i + 1));
      const eval = minimax(newState, depth - 1, false);
      maxEval = Math.max(maxEval, eval);
    }
    return maxEval;
  } else {
    let minEval = Infinity;
    for (let i = 0; i < state.length; i++) {
      const newState = state.slice(0, i).concat(state.slice(i + 1));
      const eval = minimax(newState, depth - 1, true);
      minEval = Math.min(minEval, eval);
    }
    return minEval;
  }
}
Note: The minimax algorithm evaluates all possible moves to determine the optimal move for a player, assuming that the opponent also plays optimally.

11. Implementing Impartial Games

Game Type Description Key Properties Example
Nim A classic impartial game where players alternately remove objects from heaps Winning strategy based on XOR of heap sizes
function nimSum(heaps) {
  let xor = 0;
  for (let heap of heaps) xor ^= heap;
  return xor;
}
Kayles Players remove one or two adjacent pins from a row of pins Can be solved using Sprague-Grundy theorem
function kaylesState(pins) {
  // State representation and move generation
  return pins.reduce((acc, pin) => acc + (pin ? 1 : 0), 0);
}

Example: Nim Game Strategy

function nimMove(heaps) {
  const xor = heaps.reduce((a, b) => a ^ b, 0);
  if (xor === 0) return null; // Losing position
  for (let i = 0; i < heaps.length; i++) {
    const target = heaps[i] ^ xor;
    if (target < heaps[i]) {
      return { heap: i, amount: heaps[i] - target };
    }
  }
  return null;
}
Note: In impartial games, the Sprague-Grundy theorem can be used to compute game values for each position.

Was this article helpful?