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
1121from __future__ import annotations
1424
1525
1626def 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
91304if __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 ("\n Launching Tic-Tac-Toe …" )
318+ _play_game ()
0 commit comments