Skip to content

Commit 7fe2762

Browse files
authored
Merge pull request #13 from vnthanhdng/main-changes-2025-12-02
Round Robin Tournament
2 parents 74db8de + 884676f commit 7fe2762

File tree

7 files changed

+504
-131
lines changed

7 files changed

+504
-131
lines changed

agent_tournament.py

Lines changed: 30 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,131 +1,43 @@
1+
"""
2+
Lightweight agent-versus-agent runner using shared utilities.
3+
4+
This script used to duplicate play/timing logic; it now delegates to
5+
`scripts.agent_utils` to keep behavior consistent with other scripts.
6+
"""
17
import argparse
28
import chess
3-
import time
4-
from src.agents.base_agent import BaseAgent
5-
from src.agents import MinimaxAgent, AlphaBetaAgent, ExpectimaxAgent
6-
from evaluation import evaluate
7-
8-
9-
def play_single_game(white_agent: BaseAgent, black_agent: BaseAgent, timeout_seconds: int = 120):
10-
"""
11-
Play one game between agents with a hard timeout and move-time tracking.
12-
Returns:
13-
result (str): "white", "black", "draw", or "timeout"
14-
white_avg (float)
15-
black_avg (float)
16-
"""
17-
board = chess.Board()
18-
19-
white_times = []
20-
black_times = []
21-
22-
start_game_time = time.time()
23-
24-
while not board.is_game_over():
25-
# Hard 2 minute timeout
26-
if time.time() - start_game_time > timeout_seconds:
27-
print("Game terminated due to timeout.")
28-
return "timeout", 0, 0
29-
30-
current_agent = white_agent if board.turn == chess.WHITE else black_agent
31-
32-
move_start = time.time()
33-
print(f"{current_agent}")
34-
move = current_agent.choose_move(board)
35-
move_end = time.time()
36-
37-
if move is None:
38-
print("Error: Agent returned None move.")
39-
return "error", 0, 0
40-
41-
# Track move time
42-
if board.turn == chess.WHITE:
43-
white_times.append(move_end - move_start)
44-
else:
45-
black_times.append(move_end - move_start)
46-
47-
board.push(move)
48-
49-
# Compute averages
50-
white_avg = sum(white_times) / len(white_times) if white_times else 0
51-
black_avg = sum(black_times) / len(black_times) if black_times else 0
52-
53-
# Determine outcome
54-
if board.is_checkmate():
55-
winner = "white" if board.turn == chess.BLACK else "black"
56-
else:
57-
winner = "draw"
58-
59-
return winner, white_avg, black_avg
60-
61-
62-
def make_agents_play(white_agent: BaseAgent, black_agent: BaseAgent, iterations: int):
63-
"""
64-
Run `iterations` number of games and report average move times.
65-
"""
66-
white_avg_list = []
67-
black_avg_list = []
68-
69-
for game_idx in range(1, iterations + 1):
70-
print(f"\n=== Starting Game {game_idx}/{iterations} ===")
71-
result, w_avg, b_avg = play_single_game(white_agent, black_agent)
72-
73-
print(f"Game {game_idx} result: {result}")
74-
print(f" White ({white_agent.name}) avg move time: {w_avg:.4f} sec")
75-
print(f" Black({black_agent.name}) avg move time: {b_avg:.4f} sec")
76-
77-
white_avg_list.append(w_avg)
78-
black_avg_list.append(b_avg)
79-
80-
print("\n===== FINAL RESULTS ACROSS ALL GAMES =====")
81-
print(f"{white_agent.name} mean move time: {sum(white_avg_list)/iterations:.4f} sec")
82-
print(f"{black_agent.name} mean move time: {sum(black_avg_list)/iterations:.4f} sec")
9+
from scripts.agent_utils import create_agent, play_single_game_with_stats
8310

8411

8512
def main():
86-
parser = argparse.ArgumentParser(description="Play chess with agents")
87-
parser.add_argument(
88-
"--white-agent",
89-
choices=["minimax", "alphabeta", "expectimax"],
90-
default="minimax",
91-
)
92-
parser.add_argument(
93-
"--black-agent",
94-
choices=["minimax", "alphabeta", "expectimax"],
95-
default="alphabeta",
96-
)
97-
parser.add_argument(
98-
"--depth",
99-
type=int,
100-
default=3,
101-
choices=[2, 3, 4, 5],
102-
help="Search depth for AI agents (default: 3)",
103-
)
104-
parser.add_argument(
105-
"--num-games",
106-
type=int,
107-
default=1,
108-
help="Number of games to run (default: 1)"
109-
)
13+
parser = argparse.ArgumentParser(description="Play games between two agents")
14+
parser.add_argument("--white-agent", default="minimax", help="Agent key for White")
15+
parser.add_argument("--black-agent", default="alphabeta", help="Agent key for Black")
16+
parser.add_argument("--depth", type=int, default=3, help="Default search depth for search agents")
17+
parser.add_argument("--num-games", type=int, default=1, help="Number of games to run")
18+
parser.add_argument("--vi-iterations", type=int, default=3, help="ValueIteration iterations")
19+
parser.add_argument("--q-train", type=int, default=0, help="QLearning training episodes")
20+
parser.add_argument("--q-epsilon", type=float, default=0.0, help="QLearning epsilon during matches")
11021
args = parser.parse_args()
11122

112-
def create_agent(agent_type, color):
113-
if agent_type == "minimax":
114-
return MinimaxAgent(evaluate, depth=args.depth, name="Minimax", color=color)
115-
elif agent_type == "alphabeta":
116-
return AlphaBetaAgent(evaluate, depth=args.depth, name="AlphaBeta", color=color)
117-
elif agent_type == "expectimax":
118-
return ExpectimaxAgent(evaluate, depth=args.depth, name="Expectimax", color=color)
119-
raise RuntimeError("Invalid agent type")
23+
white = create_agent(args.white_agent, chess.WHITE, depth=args.depth, vi_iterations=args.vi_iterations, q_numTraining=args.q_train, q_epsilon=args.q_epsilon)
24+
black = create_agent(args.black_agent, chess.BLACK, depth=args.depth, vi_iterations=args.vi_iterations, q_numTraining=args.q_train, q_epsilon=args.q_epsilon)
25+
26+
print(f"Running {args.num_games} games: White={white}, Black={black}")
27+
28+
white_times = []
29+
black_times = []
12030

121-
white_agent = create_agent(args.white_agent, chess.WHITE)
122-
black_agent = create_agent(args.black_agent, chess.BLACK)
31+
for i in range(1, args.num_games + 1):
32+
print(f"\n=== Game {i}/{args.num_games} ===")
33+
result = play_single_game_with_stats(white, black)
34+
print(f"Result: {result}")
12335

124-
print(f"Running {args.num_games} games:")
125-
print(f" White = {white_agent.name}")
126-
print(f" Black = {black_agent.name}")
12736

128-
make_agents_play(white_agent, black_agent, iterations=args.num_games)
37+
if white_times:
38+
print(f"\nWhite mean move time: {sum(white_times)/len(white_times):.4f}s")
39+
if black_times:
40+
print(f"Black mean move time: {sum(black_times)/len(black_times):.4f}s")
12941

13042

13143
if __name__ == "__main__":

scripts/agent_utils.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""
2+
Utilities for creating agents and running games.
3+
4+
Centralizes agent factory logic so scripts can reuse the same constructors
5+
and parameters (depth, training iterations, etc.).
6+
"""
7+
from typing import Callable
8+
from pathlib import Path
9+
import sys
10+
import chess
11+
import time
12+
13+
project_root = Path(__file__).resolve().parents[1]
14+
if str(project_root) not in sys.path:
15+
sys.path.insert(0, str(project_root))
16+
17+
from src.agents import (
18+
MinimaxAgent,
19+
AlphaBetaAgent,
20+
ExpectimaxAgent,
21+
RandomAgent,
22+
SimpleAgent,
23+
QLearningAgent,
24+
ValueIterationAgent,
25+
)
26+
from src.evaluation import evaluate
27+
28+
29+
def create_agent(agent_key: str, color: chess.Color, *, depth: int = 3, vi_iterations: int = 3, q_numTraining: int = 0, q_epsilon: float = 0.0):
30+
"""Create an agent instance from a short key.
31+
32+
Parameters are provided with sane defaults for fast tests.
33+
"""
34+
key = agent_key.lower()
35+
if key == "minimax":
36+
return MinimaxAgent(evaluate, depth=depth, name="Minimax", color=color)
37+
if key == "alphabeta":
38+
return AlphaBetaAgent(evaluate, depth=depth, name="AlphaBeta", color=color)
39+
if key == "expectimax":
40+
return ExpectimaxAgent(evaluate, depth=depth, name="Expectimax", color=color)
41+
if key == "random":
42+
return RandomAgent(name="Random", color=color)
43+
if key == "simple":
44+
return SimpleAgent(name="Simple", color=color)
45+
if key == "qlearning":
46+
return QLearningAgent(name="QLearning", color=color, numTraining=q_numTraining, epsilon=q_epsilon)
47+
if key == "valueiteration":
48+
return ValueIterationAgent(discount=0.9, iterations=vi_iterations, name="ValueIteration", color=color)
49+
50+
raise RuntimeError(f"Unknown agent type '{agent_key}'")
51+
52+
53+
def play_game(white_agent, black_agent, timeout_seconds: int = 120):
54+
"""Play one game between two agents. Returns outcome string: 'white','black','draw','timeout','error'."""
55+
board = chess.Board()
56+
start_time = time.time()
57+
58+
while not board.is_game_over():
59+
if time.time() - start_time > timeout_seconds:
60+
return "timeout"
61+
62+
current = white_agent if board.turn == chess.WHITE else black_agent
63+
move = current.select_move(board)
64+
if move is None:
65+
return "error"
66+
board.push(move)
67+
68+
if board.is_checkmate():
69+
return "white" if board.turn == chess.BLACK else "black"
70+
return "draw"
71+
72+
73+
def play_single_game_with_stats(white_agent, black_agent, timeout_seconds: int = 120):
74+
"""Play a game and return (result, white_avg_time, black_avg_time)."""
75+
result = play_game(white_agent, black_agent, timeout_seconds)
76+
white_avg = white_agent.total_time / white_agent.moves_made if white_agent.moves_made else 0
77+
black_avg = black_agent.total_time / black_agent.moves_made if black_agent.moves_made else 0
78+
return result, white_avg, black_avg

scripts/play_agents.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Run automated matches between two agents (agent vs agent)."""
2+
3+
import argparse
4+
import chess
5+
import time
6+
from pathlib import Path
7+
import sys
8+
9+
project_root = Path(__file__).resolve().parents[1]
10+
if str(project_root) not in sys.path:
11+
sys.path.insert(0, str(project_root))
12+
13+
from src.agents.base_agent import BaseAgent
14+
from scripts.agent_utils import create_agent, play_game as play_game_simple, play_single_game_with_stats
15+
16+
17+
def play_single_game(white_agent: BaseAgent, black_agent: BaseAgent, timeout_seconds: int = 600):
18+
return play_game_simple(white_agent, black_agent, timeout_seconds)
19+
20+
21+
def make_agents_play(white_agent: BaseAgent, black_agent: BaseAgent, iterations: int):
22+
results = {"white": 0, "black": 0, "draw": 0, "timeout": 0, "error": 0}
23+
w_times = []
24+
b_times = []
25+
26+
for i in range(1, iterations + 1):
27+
print(f"\n=== Game {i}/{iterations} ===")
28+
# reset stats
29+
white_agent.reset_stats()
30+
black_agent.reset_stats()
31+
32+
result, w_avg, b_avg = play_single_game_with_stats(white_agent, black_agent)
33+
results[result] = results.get(result, 0) + 1
34+
w_times.append(w_avg)
35+
b_times.append(b_avg)
36+
37+
print(f"Result: {result}")
38+
print(f" White ({white_agent}): avg move time {w_avg:.4f}s")
39+
print(f" Black ({black_agent}): avg move time {b_avg:.4f}s")
40+
41+
42+
print("\n=== Summary ===")
43+
print(results)
44+
if w_times:
45+
print(f"White mean move time: {sum(w_times)/len(w_times):.4f}s")
46+
if b_times:
47+
print(f"Black mean move time: {sum(b_times)/len(b_times):.4f}s")
48+
49+
50+
def create_agent_from_key(agent_key: str, color: chess.Color, *, depth: int = 3, vi_iterations: int = 3, q_numTraining: int = 0, q_epsilon: float = 0.0):
51+
# thin wrapper to preserve CLI API used previously; forwards parameters to shared factory
52+
return create_agent(agent_key, color, depth=depth, vi_iterations=vi_iterations, q_numTraining=q_numTraining, q_epsilon=q_epsilon)
53+
54+
55+
def main():
56+
parser = argparse.ArgumentParser(description="Run automated agent vs agent matches")
57+
parser.add_argument("--white-agent", default="qlearning", help="Agent for White")
58+
parser.add_argument("--black-agent", default="valueiteration", help="Agent for Black")
59+
parser.add_argument("--num-games", type=int, default=1, help="Number of games to run")
60+
parser.add_argument("--depth", type=int, default=3, help="Default search depth for search agents")
61+
parser.add_argument("--white-depth", type=int, default=None, help="Search depth for White agent (overrides --depth)")
62+
parser.add_argument("--black-depth", type=int, default=None, help="Search depth for Black agent (overrides --depth)")
63+
parser.add_argument("--vi-iterations", type=int, default=3, help="Iterations for ValueIterationAgent")
64+
parser.add_argument("--q-train", type=int, default=0, help="Number of training episodes for QLearningAgent before matches")
65+
parser.add_argument("--q-epsilon", type=float, default=0.0, help="Exploration epsilon for QLearningAgent during matches")
66+
args = parser.parse_args()
67+
68+
depth_white = args.white_depth if args.white_depth is not None else args.depth
69+
depth_black = args.black_depth if args.black_depth is not None else args.depth
70+
71+
white = create_agent_from_key(args.white_agent, chess.WHITE, depth=depth_white, vi_iterations=args.vi_iterations, q_numTraining=args.q_train, q_epsilon=args.q_epsilon)
72+
black = create_agent_from_key(args.black_agent, chess.BLACK, depth=depth_black, vi_iterations=args.vi_iterations, q_numTraining=args.q_train, q_epsilon=args.q_epsilon)
73+
74+
print(f"Playing {args.num_games} games: White={white}, Black={black}")
75+
make_agents_play(white, black, iterations=args.num_games)
76+
77+
78+
if __name__ == "__main__":
79+
main()

0 commit comments

Comments
 (0)