Skip to content

Commit a5f1655

Browse files
committed
Add Minimax with Alpha-Beta Pruning algorithm
1 parent af131b7 commit a5f1655

File tree

1 file changed

+290
-67
lines changed

1 file changed

+290
-67
lines changed

backtracking/minimax.py

Lines changed: 290 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
"""
2-
Minimax helps to achieve maximum score in a game by checking all possible moves
3-
depth is current depth in game tree.
2+
Minimax Algorithm with Alpha-Beta Pruning
3+
==========================================
44
5-
nodeIndex is index of current node in scores[].
6-
if move is of maximizer return true else false
7-
leaves of game tree is stored in scores[]
8-
height is maximum height of Game tree
5+
Minimax is a decision-rule algorithm used in two-player, zero-sum games. It
6+
explores all possible moves recursively and picks the optimal one for the
7+
"maximising" player while assuming the opponent plays optimally (minimises
8+
the score).
9+
10+
Alpha-Beta Pruning is an optimisation that cuts off branches in the search
11+
tree that cannot influence the final decision, reducing the effective branching
12+
factor from O(b^d) towards O(b^(d/2)).
13+
14+
References:
15+
- https://en.wikipedia.org/wiki/Minimax
16+
- https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning
17+
- Russell, S. & Norvig, P. (2020). Artificial Intelligence: A Modern
18+
Approach (4th ed.), Chapter 5.
919
"""
1020

1121
from __future__ import annotations
@@ -14,82 +24,295 @@
1424

1525

1626
def minimax(
17-
depth: int, node_index: int, is_max: bool, scores: list[int], height: float
27+
depth: int,
28+
node_index: int,
29+
is_maximising: bool,
30+
scores: list[int],
31+
alpha: float = -math.inf,
32+
beta: float = math.inf,
33+
height: int | None = None,
1834
) -> int:
1935
"""
20-
This function implements the minimax algorithm, which helps achieve the optimal
21-
score for a player in a two-player game by checking all possible moves.
22-
If the player is the maximizer, then the score is maximized.
23-
If the player is the minimizer, then the score is minimized.
24-
25-
Parameters:
26-
- depth: Current depth in the game tree.
27-
- node_index: Index of the current node in the scores list.
28-
- is_max: A boolean indicating whether the current move
29-
is for the maximizer (True) or minimizer (False).
30-
- scores: A list containing the scores of the leaves of the game tree.
31-
- height: The maximum height of the game tree.
32-
33-
Returns:
34-
- An integer representing the optimal score for the current player.
35-
36-
>>> import math
37-
>>> scores = [90, 23, 6, 33, 21, 65, 123, 34423]
38-
>>> height = math.log(len(scores), 2)
39-
>>> minimax(0, 0, True, scores, height)
40-
65
41-
>>> minimax(-1, 0, True, scores, height)
42-
Traceback (most recent call last):
43-
...
44-
ValueError: Depth cannot be less than 0
45-
>>> minimax(0, 0, True, [], 2)
46-
Traceback (most recent call last):
47-
...
48-
ValueError: Scores cannot be empty
36+
Return the optimal score for the current player using Minimax with
37+
Alpha-Beta Pruning on a complete binary game tree whose leaf values are
38+
given by *scores*.
39+
40+
The tree is stored implicitly: leaf nodes are at depth 0, and internal
41+
nodes are addressed by *node_index* at each *depth* level.
42+
43+
Parameters
44+
----------
45+
depth : Remaining depth to explore (0 = leaf node).
46+
node_index : Index of the current node within its level.
47+
is_maximising : True if the current player wants to maximise the score.
48+
scores : List of scores at the leaf level (length must be a power of 2).
49+
alpha : Best (highest) value the maximiser can guarantee so far.
50+
beta : Best (lowest) value the minimiser can guarantee so far.
51+
height : Total height of the tree (computed automatically on first call).
52+
53+
Returns
54+
-------
55+
int : The optimal score reachable from this node.
56+
57+
Examples
58+
--------
4959
>>> scores = [3, 5, 2, 9, 12, 5, 23, 23]
50-
>>> height = math.log(len(scores), 2)
51-
>>> minimax(0, 0, True, scores, height)
60+
>>> minimax(3, 0, True, scores)
5261
12
53-
"""
5462
55-
if depth < 0:
56-
raise ValueError("Depth cannot be less than 0")
57-
if len(scores) == 0:
58-
raise ValueError("Scores cannot be empty")
63+
>>> scores = [3, 5, 2, 9]
64+
>>> minimax(2, 0, True, scores)
65+
3
5966
60-
# Base case: If the current depth equals the height of the tree,
61-
# return the score of the current node.
62-
if depth == height:
67+
>>> scores = [-1, 4, 2, 6, -3, -5, 0, 7]
68+
>>> minimax(3, 0, False, scores)
69+
0
70+
71+
>>> minimax(0, 0, True, [42])
72+
42
73+
74+
>>> minimax(0, 0, False, [7])
75+
7
76+
"""
77+
if height is None:
78+
height = int(math.log2(len(scores)))
79+
80+
# Base case: we are at a leaf node
81+
if depth == 0:
6382
return scores[node_index]
6483

65-
# If it's the maximizer's turn, choose the maximum score
66-
# between the two possible moves.
67-
if is_max:
68-
return max(
69-
minimax(depth + 1, node_index * 2, False, scores, height),
70-
minimax(depth + 1, node_index * 2 + 1, False, scores, height),
71-
)
72-
73-
# If it's the minimizer's turn, choose the minimum score
74-
# between the two possible moves.
75-
return min(
76-
minimax(depth + 1, node_index * 2, True, scores, height),
77-
minimax(depth + 1, node_index * 2 + 1, True, scores, height),
84+
left_child = 2 * node_index
85+
right_child = 2 * node_index + 1
86+
87+
if is_maximising:
88+
best = -math.inf
89+
for child in (left_child, right_child):
90+
value = minimax(depth - 1, child, False, scores, alpha, beta, height)
91+
best = max(best, value)
92+
alpha = max(alpha, best)
93+
if beta <= alpha:
94+
break # Beta cut-off: minimiser will never allow this branch
95+
return int(best)
96+
else:
97+
best = math.inf
98+
for child in (left_child, right_child):
99+
value = minimax(depth - 1, child, True, scores, alpha, beta, height)
100+
best = min(best, value)
101+
beta = min(beta, best)
102+
if beta <= alpha:
103+
break # Alpha cut-off: maximiser will never allow this branch
104+
return int(best)
105+
106+
107+
# ---------------------------------------------------------------------------
108+
# Tic-Tac-Toe: a concrete, runnable demonstration of Minimax
109+
# ---------------------------------------------------------------------------
110+
111+
Board = list[list[str]]
112+
113+
_HUMAN = "O"
114+
_AI = "X"
115+
_EMPTY = "_"
116+
117+
118+
def _winning_player(board: Board) -> str | None:
119+
"""
120+
Return the winner symbol if any row, column, or diagonal is complete,
121+
otherwise return None.
122+
123+
Examples
124+
--------
125+
>>> b = [['X','X','X'],['O','_','_'],['O','_','_']]
126+
>>> _winning_player(b)
127+
'X'
128+
>>> b = [['O','X','X'],['X','O','_'],['O','_','O']]
129+
>>> _winning_player(b)
130+
'O'
131+
>>> b = [['X','O','X'],['O','X','O'],['O','X','_']]
132+
>>> _winning_player(b) is None
133+
True
134+
"""
135+
lines = (
136+
# rows
137+
board[0], board[1], board[2],
138+
# columns
139+
[board[0][0], board[1][0], board[2][0]],
140+
[board[0][1], board[1][1], board[2][1]],
141+
[board[0][2], board[1][2], board[2][2]],
142+
# diagonals
143+
[board[0][0], board[1][1], board[2][2]],
144+
[board[0][2], board[1][1], board[2][0]],
78145
)
146+
for line in lines:
147+
if line[0] != _EMPTY and len(set(line)) == 1:
148+
return line[0]
149+
return None
150+
79151

152+
def _is_board_full(board: Board) -> bool:
153+
"""
154+
Return True if there are no empty cells left.
80155
81-
def main() -> None:
82-
# Sample scores and height calculation
83-
scores = [90, 23, 6, 33, 21, 65, 123, 34423]
84-
height = math.log(len(scores), 2)
156+
Examples
157+
--------
158+
>>> _is_board_full([['X','O','X'],['O','X','O'],['O','X','O']])
159+
True
160+
>>> _is_board_full([['X','O','X'],['O','_','O'],['O','X','O']])
161+
False
162+
"""
163+
return all(cell != _EMPTY for row in board for cell in row)
85164

86-
# Calculate and print the optimal value using the minimax algorithm
87-
print("Optimal value : ", end="")
88-
print(minimax(0, 0, True, scores, height))
165+
166+
def _evaluate(board: Board) -> int:
167+
"""
168+
Heuristic score: +10 if AI wins, -10 if human wins, 0 otherwise.
169+
170+
Examples
171+
--------
172+
>>> b = [['X','X','X'],['O','_','_'],['O','_','_']]
173+
>>> _evaluate(b)
174+
10
175+
>>> b = [['O','O','O'],['X','_','_'],['X','_','_']]
176+
>>> _evaluate(b)
177+
-10
178+
>>> b = [['X','O','X'],['O','X','O'],['O','X','O']]
179+
>>> _evaluate(b)
180+
0
181+
"""
182+
winner = _winning_player(board)
183+
if winner == _AI:
184+
return 10
185+
if winner == _HUMAN:
186+
return -10
187+
return 0
188+
189+
190+
def minimax_ttt(board: Board, depth: int, is_maximising: bool) -> int:
191+
"""
192+
Minimax (without alpha-beta, for clarity) applied to Tic-Tac-Toe.
193+
Returns the best achievable score from *board* for the current player.
194+
195+
Parameters
196+
----------
197+
board : 3×3 grid; cells are '_', 'X', or 'O'.
198+
depth : Remaining search depth (used to prefer shorter wins).
199+
is_maximising : True when it is the AI's (X's) turn.
200+
201+
Examples
202+
--------
203+
>>> b = [['X','X','_'],['O','O','_'],['_','_','_']]
204+
>>> minimax_ttt(b, 9, True) # AI should win by playing (0,2)
205+
10
206+
>>> b = [['O','O','_'],['X','X','_'],['_','_','_']]
207+
>>> minimax_ttt(b, 9, False) # Human should win by playing (1,2)
208+
-10
209+
"""
210+
score = _evaluate(board)
211+
if score in (10, -10):
212+
return score
213+
if _is_board_full(board):
214+
return 0
215+
216+
if is_maximising:
217+
best = -math.inf
218+
for i in range(3):
219+
for j in range(3):
220+
if board[i][j] == _EMPTY:
221+
board[i][j] = _AI
222+
best = max(best, minimax_ttt(board, depth - 1, False))
223+
board[i][j] = _EMPTY
224+
return int(best)
225+
else:
226+
best = math.inf
227+
for i in range(3):
228+
for j in range(3):
229+
if board[i][j] == _EMPTY:
230+
board[i][j] = _HUMAN
231+
best = min(best, minimax_ttt(board, depth - 1, True))
232+
board[i][j] = _EMPTY
233+
return int(best)
234+
235+
236+
def best_move(board: Board) -> tuple[int, int]:
237+
"""
238+
Return the board position (row, col) of the AI's best next move.
239+
240+
Examples
241+
--------
242+
>>> b = [['X','O','X'],['O','X','_'],['_','_','O']]
243+
>>> best_move(b)
244+
(2, 0)
245+
"""
246+
best_val = -math.inf
247+
move = (-1, -1)
248+
for i in range(3):
249+
for j in range(3):
250+
if board[i][j] == _EMPTY:
251+
board[i][j] = _AI
252+
move_val = minimax_ttt(board, 9, False)
253+
board[i][j] = _EMPTY
254+
if move_val > best_val:
255+
best_val = move_val
256+
move = (i, j)
257+
return move
258+
259+
260+
def _print_board(board: Board) -> None:
261+
"""Pretty-print the Tic-Tac-Toe board."""
262+
print("\n 0 1 2")
263+
for idx, row in enumerate(board):
264+
print(f"{idx} {' '.join(row)}")
265+
print()
266+
267+
268+
def _play_game() -> None:
269+
"""Interactive Tic-Tac-Toe: Human (O) vs. AI (X)."""
270+
board: Board = [[_EMPTY] * 3 for _ in range(3)]
271+
print("=== Tic-Tac-Toe: Human (O) vs AI (X) ===")
272+
print("Enter moves as 'row col' (e.g., '1 2').\n")
273+
274+
for turn in range(9):
275+
_print_board(board)
276+
if turn % 2 == 0:
277+
# Human's turn
278+
try:
279+
raw = input("Your move (row col): ").strip().split()
280+
r, c = int(raw[0]), int(raw[1])
281+
except (ValueError, IndexError):
282+
print("Invalid input. Try again.")
283+
continue
284+
if not (0 <= r < 3 and 0 <= c < 3) or board[r][c] != _EMPTY:
285+
print("Cell unavailable. Try again.")
286+
continue
287+
board[r][c] = _HUMAN
288+
else:
289+
# AI's turn
290+
r, c = best_move(board)
291+
board[r][c] = _AI
292+
print(f"AI plays at ({r}, {c})")
293+
294+
winner = _winning_player(board)
295+
if winner:
296+
_print_board(board)
297+
print(f"{'You win! 🎉' if winner == _HUMAN else 'AI wins! 🤖'}")
298+
return
299+
300+
_print_board(board)
301+
print("It's a draw! 🤝")
89302

90303

91304
if __name__ == "__main__":
92305
import doctest
93306

94307
doctest.testmod()
95-
main()
308+
309+
# --- Demo: generic game tree ---
310+
print("=== Generic Minimax Demo ===")
311+
demo_scores = [3, 5, 2, 9, 12, 5, 23, 23]
312+
result = minimax(3, 0, True, demo_scores)
313+
print(f"Scores : {demo_scores}")
314+
print(f"Optimal val : {result}") # Expected: 12
315+
316+
# --- Demo: Tic-Tac-Toe interactive ---
317+
print("\nLaunching Tic-Tac-Toe …")
318+
_play_game()

0 commit comments

Comments
 (0)