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`;
}
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)
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");
}
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];
}
5. Implementing Minimax Algorithm
| Component | Description | Implementation |
|---|---|---|
| Minimax Function | Core recursive function to evaluate game states | |
| Evaluation Function | Heuristic to score board states | |
Example: Minimax in Tic-Tac-Toe
const board = [
["", "", ""],
["", "", ""],
["", "", ""]
];
// AI plays as "O"
const bestMove = findBestMove(board);
console.log(bestMove); // {row: 1, col: 1}
6. Implementing Alpha-Beta Pruning
| Component | Description | Implementation |
|---|---|---|
| Alpha-Beta Pruning | Optimization of minimax algorithm to reduce the number of nodes evaluated | |
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
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.
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;
}
}
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;
}
}
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 | |
| Kayles | Players remove one or two adjacent pins from a row of pins | Can be solved using Sprague-Grundy theorem | |
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;
}