11import asyncio
22import argparse
3+ import time
34from typing import List , Optional , Tuple , Dict
45from agents .base_agent import BaseOthelloAgent
56from agents .utils import OthelloLogic
67
7-
88class ClassicalAgent (BaseOthelloAgent ):
99 """
10- Agente Clássico que utiliza o algoritmo Minimax com poda Alpha-Beta.
11- Suporta múltiplos níveis de dificuldade e otimização por tabelas de transposição.
10+ Classical Othello Agent using the Minimax algorithm with Alpha-Beta Pruning.
11+
12+ This agent supports three difficulty levels:
13+ - Normal: Standard search depth.
14+ - Hard: Deeper search with mobility heuristics.
15+ - Very Hard: Deepest search with transposition tables (cache) and endgame solving.
1216 """
1317
1418 def __init__ (self , difficulty : str = "normal" ):
1519 super ().__init__ ()
16- self .difficulty = difficulty
20+ self .set_difficulty (difficulty )
21+ # Cache to store evaluated board states and avoid redundant calculations
1722 self .transposition_table : Dict [Tuple , Tuple [float , Optional [List [int ]]]] = {}
1823
19- if difficulty == "normal" :
24+ def set_difficulty (self , difficulty : str ):
25+ self .difficulty = difficulty
26+ if difficulty in ["normal" , "n" ]:
2027 self .depth = 4
2128 self .use_mobility = False
22- elif difficulty == "hard" :
29+ elif difficulty in [ "hard" , "h" ] :
2330 self .depth = 6
2431 self .use_mobility = True
25- elif difficulty == "very_hard" :
32+ elif difficulty in ["very_hard" , "vh" ]:
33+ # Note: Depth 8 may cause timeouts in the early/mid game due to Python's execution speed
2634 self .depth = 8
2735 self .use_mobility = True
2836
2937 async def deliberate (
3038 self , board : List [List [int ]], valid_actions : List [List [int ]]
3139 ) -> Tuple [int , int ]:
32- """Calcula a melhor jogada baseada na dificuldade configurada."""
40+ """
41+ Decision-making entry point. Selects the best move using Minimax.
42+
43+ Args:
44+ board: Current 8x8 board state (0: empty, 1: black, 2: white).
45+ valid_actions: List of available [x, y] move coordinates.
46+
47+ Returns:
48+ A tuple (x, y) representing the chosen move.
49+ """
50+ # Add a tiny delay so humans can watch the game unfold
51+ await asyncio .sleep (0.5 )
52+
3353 empty_cells = sum (row .count (0 ) for row in board )
3454 current_depth = self .depth
3555
36- # Solucionador de Fim de Jogo
37- if self .difficulty == "very_hard" and empty_cells <= 12 :
56+ # Endgame Solver: If few moves remain, search until the end of the game
57+ if self .difficulty in [ "very_hard" , "vh" ] and empty_cells <= 12 :
3858 current_depth = empty_cells
39- print (f"[Endgame] Resolvendo { empty_cells } casas restantes ." )
59+ print (f"[Endgame] Solving the last { empty_cells } positions ." )
4060
41- self .transposition_table = {} # Limpar cache para nova jogada
61+ self .transposition_table = {} # Clear cache for each new move to stay current
62+ start_t = time .time ()
4263
43- _ , move = self .minmax (
64+ score , move = self .minmax (
4465 board ,
4566 depth = current_depth ,
4667 alpha = float ("-inf" ),
@@ -50,6 +71,9 @@ async def deliberate(
5071 use_mobility = self .use_mobility ,
5172 )
5273
74+ elapsed = time .time () - start_t
75+ print (f"Move took { elapsed :.2f} s (Depth: { current_depth } , Mobility: { self .use_mobility } )" )
76+
5377 return tuple (move ) if move else tuple (valid_actions [0 ])
5478
5579 def minmax (
@@ -62,9 +86,23 @@ def minmax(
6286 player_id : int ,
6387 use_mobility : bool = False ,
6488 ) -> Tuple [float , Optional [List [int ]]]:
65- """Algoritmo Minimax com Poda Alpha-Beta e Move Ordering."""
66-
67- # 1. Cache Check
89+ """
90+ Recursive Minimax algorithm with Alpha-Beta pruning and move ordering.
91+
92+ Args:
93+ board: 8x8 matrix representing the game state.
94+ depth: Current remaining depth in the search tree.
95+ alpha: The best value the maximizing player can guarantee (Best for Me).
96+ beta: The best value the minimizing player can guarantee (Best for Opponent).
97+ maximizing_player: True if it's the agent's turn to maximize the score.
98+ player_id: The ID assigned to this agent (1 or 2).
99+ use_mobility: Whether to use move count difference in the evaluation function.
100+
101+ Returns:
102+ A tuple containing (evaluation_score, best_move_coordinates).
103+ """
104+
105+ # 1. Transposition Table Check (Cache)
68106 board_tuple = tuple (tuple (row ) for row in board )
69107 state_key = (board_tuple , depth , maximizing_player )
70108 if state_key in self .transposition_table :
@@ -74,53 +112,55 @@ def minmax(
74112 current_p = player_id if maximizing_player else opponent
75113 valid_moves = OthelloLogic .get_valid_moves (board , current_p )
76114
115+ # Base case: reach depth limit or game over
77116 if depth == 0 or not valid_moves :
78117 return OthelloLogic .evaluate_board (board , player_id , use_mobility ), None
79118
80- # 2. Move Ordering (Priorizar cantos para acelerar a poda )
119+ # 2. Move Ordering (Prioritize corners to trigger Alpha-Beta pruning faster )
81120 valid_moves .sort (key = lambda m : m [0 ] in [0 , 7 ] and m [1 ] in [0 , 7 ], reverse = True )
82121
83122 best_move = None
84123 if maximizing_player :
85124 max_eval = float ("-inf" )
86125 for move in valid_moves :
87- new_board = OthelloLogic .simulate_move (
88- board , current_p , move [0 ], move [1 ]
89- )
90- eval_score , _ = self .minmax (
91- new_board , depth - 1 , alpha , beta , False , player_id , use_mobility
92- )
126+ new_board = OthelloLogic .simulate_move (board , current_p , move [0 ], move [1 ])
127+ eval_score , _ = self .minmax (new_board , depth - 1 , alpha , beta , False , player_id , use_mobility )
93128 if eval_score > max_eval :
94129 max_eval , best_move = eval_score , move
95130 alpha = max (alpha , eval_score )
96131 if beta <= alpha :
97- break
132+ break # Beta cut-off
98133 res = (max_eval , best_move )
99134 else :
100135 min_eval = float ("inf" )
101136 for move in valid_moves :
102- new_board = OthelloLogic .simulate_move (
103- board , current_p , move [0 ], move [1 ]
104- )
105- eval_score , _ = self .minmax (
106- new_board , depth - 1 , alpha , beta , True , player_id , use_mobility
107- )
137+ new_board = OthelloLogic .simulate_move (board , current_p , move [0 ], move [1 ])
138+ eval_score , _ = self .minmax (new_board , depth - 1 , alpha , beta , True , player_id , use_mobility )
108139 if eval_score < min_eval :
109140 min_eval , best_move = eval_score , move
110141 beta = min (beta , eval_score )
111142 if beta <= alpha :
112- break
143+ break # Alpha cut-off
113144 res = (min_eval , best_move )
114145
146+ # Save result to cache before returning
115147 self .transposition_table [state_key ] = res
116148 return res
117149
118150
119151if __name__ == "__main__" :
120- parser = argparse .ArgumentParser (description = "Classical Agent - MinMax " )
152+ parser = argparse .ArgumentParser (description = "Classical Othello Agent - Minimax with Alpha-Beta Pruning " )
121153 parser .add_argument (
122- "-d" , "--difficulty" , choices = ["normal" , "hard" , "very_hard" ], default = "normal"
154+ "-d" , "--difficulty" ,
155+ choices = ["n" , "normal" , "h" , "hard" , "vh" , "very_hard" ],
156+ default = "normal" ,
157+ help = "Difficulty level (normal/hard/very_hard)"
123158 )
124159 args = parser .parse_args ()
125- agent = ClassicalAgent (difficulty = args .difficulty )
126- asyncio .run (agent .run ())
160+
161+ # Map shorthand arguments to full difficulty names
162+ diff_map = {"n" : "normal" , "h" : "hard" , "vh" : "very_hard" }
163+ difficulty = diff_map .get (args .difficulty , args .difficulty )
164+
165+ agent = ClassicalAgent (difficulty = difficulty )
166+ asyncio .run (agent .run ())
0 commit comments