Skip to content

Commit 3d1f45b

Browse files
Update code
1 parent d8e1329 commit 3d1f45b

6 files changed

Lines changed: 210 additions & 163 deletions

File tree

agents/ai_agent.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,21 @@ def __init__(self, model_path):
2424
)
2525

2626
async def deliberate(self, board, valid_actions):
27-
obs = torch.FloatTensor(np.array(board)).to(self.device)
27+
28+
# Add a tiny delay so humans can watch the game unfold
29+
await asyncio.sleep(0.5)
30+
obs = np.array(board)
31+
if self.player_id == 1:
32+
# Inverter: onde é 1 vira 2, onde é 2 vira 1
33+
obs = np.where(obs == 1, 1, np.where(obs == 2, -1, 0))
34+
else:
35+
obs = np.where(obs == 2, 1, np.where(obs == 1, -1, 0))
36+
37+
obs_tensor = torch.FloatTensor(obs).to(self.device)
2838

2939
with torch.no_grad():
3040
# A rede dá uma pontuação (Q-value) para cada uma das 64 casas
31-
q_values = self.model(obs)
41+
q_values = self.model(obs_tensor)
3242

3343
# Criar uma máscara para ignorar jogadas inválidas
3444
mask = torch.zeros(64).to(self.device)

agents/classical_agent.py

Lines changed: 75 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,67 @@
11
import asyncio
22
import argparse
3+
import time
34
from typing import List, Optional, Tuple, Dict
45
from agents.base_agent import BaseOthelloAgent
56
from agents.utils import OthelloLogic
67

7-
88
class 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

119151
if __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())

agents/utils.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
class OthelloLogic:
22
SIZE = 8
33
DIRECTIONS = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
4+
WEIGHTS = [
5+
[100, -20, 10, 5, 5, 10, -20, 100],
6+
[-20, -50, -2, -2, -2, -2, -50, -20],
7+
[10, -2, 5, 1, 1, 5, -2, 10],
8+
[5, -2, 1, 0, 0, 1, -2, 5],
9+
[5, -2, 1, 0, 0, 1, -2, 5],
10+
[10, -2, 5, 1, 1, 5, -2, 10],
11+
[-20, -50, -2, -2, -2, -2, -50, -20],
12+
[100, -20, 10, 5, 5, 10, -20, 100],
13+
]
414

515
@staticmethod
616
def get_flips(board, player_id, x, y):
@@ -52,23 +62,13 @@ def get_valid_moves(board, player_id):
5262
def evaluate_board(board, player_id, use_mobility=False):
5363
opponent = 3 - player_id
5464
score = 0
55-
weights = [
56-
[100, -20, 10, 5, 5, 10, -20, 100],
57-
[-20, -50, -2, -2, -2, -2, -50, -20],
58-
[10, -2, 5, 1, 1, 5, -2, 10],
59-
[5, -2, 1, 0, 0, 1, -2, 5],
60-
[5, -2, 1, 0, 0, 1, -2, 5],
61-
[10, -2, 5, 1, 1, 5, -2, 10],
62-
[-20, -50, -2, -2, -2, -2, -50, -20],
63-
[100, -20, 10, 5, 5, 10, -20, 100],
64-
]
65-
65+
6666
for y in range(8):
6767
for x in range(8):
6868
if board[y][x] == player_id:
69-
score += weights[y][x]
69+
score += OthelloLogic.WEIGHTS[y][x]
7070
elif board[y][x] == opponent:
71-
score -= weights[y][x]
71+
score -= OthelloLogic.WEIGHTS[y][x]
7272
if use_mobility:
7373
my_moves = len(OthelloLogic.get_valid_moves(board, player_id))
7474
opp_moves = len(OthelloLogic.get_valid_moves(board, opponent))

src/__init__.py

Whitespace-only changes.

src/environment.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,18 @@ def reset(self):
1212
self.board = [[0] * 8 for _ in range(8)]
1313
self.board[3][3], self.board[4][4] = 2, 2 # Branco
1414
self.board[3][4], self.board[4][3] = 1, 1 # Preto
15-
return self.get_state()
15+
return self.get_state(player_id=1)
1616

17-
def get_state(self):
18-
"""Converte o tabuleiro (list) num Tensor que o PyTorch entende."""
19-
# Transformamos o board numa matriz numpy
17+
def get_state(self, player_id):
2018
state = np.array(self.board)
21-
# Opcional: Normalizar para a IA ver -1 (inimigo), 0 (vazio), 1 (eu)
22-
# Se a IA for o jogador 1, não muda nada. Se for o 2, invertemos.
23-
return (
24-
torch.FloatTensor(state).unsqueeze(0).unsqueeze(0)
25-
) # Formato (1, 1, 8, 8)
19+
# Normalização: O jogador atual vê as suas peças como 1 e as do outro como -1
20+
if player_id == 1:
21+
norm_state = np.where(state == 1, 1, np.where(state == 2, -1, 0))
22+
else:
23+
norm_state = np.where(state == 2, 1, np.where(state == 1, -1, 0))
24+
25+
# SUCESSO: Devolver norm_state
26+
return torch.FloatTensor(norm_state).unsqueeze(0).unsqueeze(0)
2627

2728
def step(self, action_idx, player_id):
2829
"""
@@ -34,7 +35,7 @@ def step(self, action_idx, player_id):
3435

3536
# 1. Punição por jogada inválida
3637
if [x, y] not in valid_moves:
37-
return self.get_state(), -10, True # Jogo acaba com penalização
38+
return self.get_state(player_id), -10, True # Jogo acaba com penalização
3839

3940
# 2. Executar a jogada
4041
self.board = OthelloLogic.simulate_move(self.board, player_id, x, y)
@@ -58,7 +59,7 @@ def step(self, action_idx, player_id):
5859
if p1_count == p2_count:
5960
reward = 0
6061

61-
return self.get_state(), reward, done
62+
return self.get_state(player_id), reward, done
6263

6364
def get_valid_mask(self, player_id):
6465
"""Retorna um array de 64 posições com 1 onde a jogada é válida e 0 onde não é."""

0 commit comments

Comments
 (0)