Adversarial search: minimax and alpha-beta pruning
Choose moves in a two-player game and prune branches that cannot change the result.
On this page
Recursion and strings.
- Choose moves in a two-player game and prune branches that cannot change the result.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
In a two-player, turn-taking game with perfect information, such as tic-tac-toe, one player's gain is the other's loss. Minimax search assumes both players play optimally. The maximizing player, X here, picks the move with the highest value, and the minimizing player, O, picks the lowest. Leaf positions are scored +1 if X has won, −1 if O has won, and 0 for a draw, and the scores are passed up the tree.
Minimax must visit every position in the game tree. Alpha-beta pruning returns the same value while skipping branches that cannot affect it. It carries two bounds: alpha, the best value the maximizer is already guaranteed, and beta, the best the minimizer is already guaranteed. When alpha is at least beta, the remaining moves at that node cannot change the final result, so the search cuts them off.
The program evaluates a position in which X is in a corner and O is in the center, with X to move. Both methods return 0, a draw with best play. Plain minimax visits 7332 positions while alpha-beta visits only 844. How much pruning you get depends on move ordering: examining the best moves first prunes the most. For large games such as chess, programs combine alpha-beta with depth limits and evaluation functions rather than searching to the end.
Code example
LINES = [(0, 1, 2), (3, 4, 5), (6, 7, 8), (0, 3, 6), (1, 4, 7), (2, 5, 8), (0, 4, 8), (2, 4, 6)]
nodes = 0
def winner(board):
for a, b, c in LINES:
if board[a] != "." and board[a] == board[b] == board[c]:
return board[a]
return None
def terminal_score(board):
won = winner(board)
if won:
return 1 if won == "X" else -1
return 0 if "." not in board else None
def minimax(board, player):
global nodes
nodes += 1
score = terminal_score(board)
if score is not None:
return score
other = "O" if player == "X" else "X"
scores = [minimax(board[:i] + player + board[i + 1:], other) for i, cell in enumerate(board) if cell == "."]
return max(scores) if player == "X" else min(scores)
def alphabeta(board, player, alpha, beta):
global nodes
nodes += 1
score = terminal_score(board)
if score is not None:
return score
other = "O" if player == "X" else "X"
value = -2 if player == "X" else 2
for i, cell in enumerate(board):
if cell != ".":
continue
child = alphabeta(board[:i] + player + board[i + 1:], other, alpha, beta)
if player == "X":
value = max(value, child)
alpha = max(alpha, value)
else:
value = min(value, child)
beta = min(beta, value)
if alpha >= beta:
break
return value
start = "X...O...."
nodes = 0
plain = minimax(start, "X")
plain_nodes = nodes
nodes = 0
pruned = alphabeta(start, "X", -2, 2)
print(plain, pruned)
print(plain_nodes, nodes)0 0 7332 844
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
LINES = [(0, 1, 2), (3, 4, 5), (6, 7, 8), (0, 3, 6), (1, 4, 7), (2, 5, 8), (0, 4, 8), (2, 4, 6)]nodes = 0 def winner(board): for a, b, c in LINES: if board[a] != "." and board[a] == board[b] == board[c]: return board[a] return None def terminal_score(board): won = winner(board) if won: return 1 if won == "X" else -1 return 0 if "." not in board else None def minimax(board, player): global nodes nodes += 1 score = terminal_score(board) if score is not None: return score other = "O" if player == "X" else "X" scores = [minimax(board[:i] + player + board[i + 1:], other) for i, cell in enumerate(board) if cell == "."] return max(scores) if player == "X" else min(scores) def alphabeta(board, player, alpha, beta): global nodes nodes += 1 score = terminal_score(board) if score is not None: return score other = "O" if player == "X" else "X" value = -2 if player == "X" else 2 for i, cell in enumerate(board): if cell != ".": continue child = alphabeta(board[:i] + player + board[i + 1:], other, alpha, beta) if player == "X": value = max(value, child) alpha = max(alpha, value) else: value = min(value, child) beta = min(beta, value) if alpha >= beta: break return value start = "X...O...."nodes = 0plain = minimax(start, "X")plain_nodes = nodesnodes = 0pruned = alphabeta(start, "X", -2, 2)print(plain, pruned)print(plain_nodes, nodes)Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| Minimax tries every legal move and recursively scores the resulting position. | visits every position | — | |
| X chooses the highest value, O the lowest: each assumes the opponent replies optimally. | visits every position | — | |
| When the maximizer's guaranteed value reaches the minimizer's, the remaining moves cannot change the result, so they are skipped. | prune | — | |
| Both algorithms agree: with best play from this position the game is a draw. | prune | 0 0 | |
| Minimax visited 7332 positions and alpha-beta only 844, with the same answer. | prune | 0 0 7332 844 |
Behind the scenes
How Python processes this code
Python runs this source through its interpreter. It creates objects for values and resolves names as each statement runs.
Cost and common mistakes
Minimax is O(b^d) for branching factor b and depth d. Alpha-beta with ideal ordering reaches about O(b^(d/2)), which doubles the depth you can search in the same time.
- Alpha-beta never changes the minimax value. It only avoids work that cannot affect it.
- Pruning quality depends on move ordering. In the worst ordering nothing is pruned.
- Minimax assumes a perfect opponent and complete information. Games with chance or hidden information need other methods such as expectimax or sampling.
Try it yourself
What would minimax return for a position where X can win in one move?
Reveal answer and reasoning
+1. The winning move yields a terminal score of +1, and the maximizing player picks the maximum of the child values.