Skip to content

Commit 2da2d01

Browse files
authored
Merge pull request #9 from vnthanhdng/aj/gui
Add GUI to play agents, and set up agent-vs-agent play
2 parents a5c0234 + 19ac441 commit 2da2d01

File tree

6 files changed

+956
-2
lines changed

6 files changed

+956
-2
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,32 @@ During play you can use these commands:
4444
- `help` — show command help
4545
- `quit` — exit the game
4646

47+
## Run the GUI
48+
49+
Play chess with a visual board interface:
50+
51+
```bash
52+
# Play against an AI (default: human vs alphabeta)
53+
python play_gui.py
54+
55+
# Watch two AIs play against each other
56+
python play_gui.py --mode watch --white-agent alphabeta --black-agent minimax --depth 3 --delay 1.0
57+
58+
# Play as white against a specific AI
59+
python play_gui.py --white-agent human --black-agent expectimax --depth 4
60+
61+
# Customize agents and settings
62+
python play_gui.py --mode watch --white-agent minimax --black-agent alphabeta --depth 3 --delay 0.5
63+
```
64+
65+
The GUI provides:
66+
- Visual chess board with Unicode piece symbols
67+
- Click-to-move or text input for moves
68+
- Move history display
69+
- Position evaluation display
70+
- Auto-play mode for watching AI vs AI games
71+
- Undo/reset functionality
72+
4773
## Running tests
4874

4975
There are lightweight unit tests in `tests/` to validate agents and evaluation. Run them with pytest (recommended):
@@ -62,12 +88,14 @@ python test_evaluation.py
6288
## Project layout (key files)
6389

6490
- `main.py` — CLI entrypoint and interactive loop
91+
- `play_gui.py` — GUI entrypoint for visual chess games
6592
- `engine.py` (legacy) — reference search implementations and helpers
6693
- `evaluation.py` (legacy) — standalone evaluation function and piece-square tables
6794
- `utils.py` — general helpers used by CLI and tests
6895
- `src/agents/` — agent implementations (human, search-based agents, and learning agents)
6996
- `src/search/` — search algorithm implementations (minimax, alphabeta, expectimax)
7097
- `src/evaluation/` — modular evaluators and piece-square tables
98+
- `src/gui/` — tkinter-based GUI for chess visualization
7199
- `tests/` — unit tests for agents, search, and evaluation
72100

73101
## License

agent_tournament.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import argparse
2+
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")
83+
84+
85+
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+
)
110+
args = parser.parse_args()
111+
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")
120+
121+
white_agent = create_agent(args.white_agent, chess.WHITE)
122+
black_agent = create_agent(args.black_agent, chess.BLACK)
123+
124+
print(f"Running {args.num_games} games:")
125+
print(f" White = {white_agent.name}")
126+
print(f" Black = {black_agent.name}")
127+
128+
make_agents_play(white_agent, black_agent, iterations=args.num_games)
129+
130+
131+
if __name__ == "__main__":
132+
main()

play_gui.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""Play chess with a GUI - human vs AI or watch two AIs play."""
2+
3+
import argparse
4+
import chess
5+
import sys
6+
from pathlib import Path
7+
8+
# Add project root to path
9+
project_root = Path(__file__).resolve().parent
10+
if str(project_root) not in sys.path:
11+
sys.path.insert(0, str(project_root))
12+
13+
from src.gui import ChessGUI, play_game_with_gui, watch_agents_play
14+
from src.agents import HumanAgent, MinimaxAgent, AlphaBetaAgent, ExpectimaxAgent
15+
from evaluation import evaluate
16+
17+
18+
def main():
19+
parser = argparse.ArgumentParser(description="Play chess with GUI")
20+
parser.add_argument(
21+
"--mode",
22+
choices=["human", "watch"],
23+
default="human",
24+
help="Game mode: 'human' to play against AI, 'watch' to watch two AIs play"
25+
)
26+
parser.add_argument(
27+
"--white-agent",
28+
choices=["human", "minimax", "alphabeta", "expectimax"],
29+
default="human",
30+
help="Agent for white (default: human)"
31+
)
32+
parser.add_argument(
33+
"--black-agent",
34+
choices=["human", "minimax", "alphabeta", "expectimax"],
35+
default="alphabeta",
36+
help="Agent for black (default: alphabeta)"
37+
)
38+
parser.add_argument(
39+
"--depth",
40+
type=int,
41+
default=3,
42+
choices=[2, 3, 4, 5],
43+
help="Search depth for AI agents (default: 3)"
44+
)
45+
parser.add_argument(
46+
"--delay",
47+
type=float,
48+
default=1.0,
49+
help="Delay between moves in watch mode (seconds, default: 1.0)"
50+
)
51+
parser.add_argument(
52+
"--no-graphics",
53+
type=bool,
54+
default=False,
55+
help="Whether to skip rendering the GUI (default, false)"
56+
)
57+
58+
args = parser.parse_args()
59+
# Create agents (None means human player, handled by GUI)
60+
def create_agent(agent_type, color):
61+
if agent_type == "human":
62+
return None # GUI handles human input
63+
elif agent_type == "minimax":
64+
return MinimaxAgent(evaluate, depth=args.depth, name="Minimax", color=color)
65+
elif agent_type == "alphabeta":
66+
return AlphaBetaAgent(evaluate, depth=args.depth, name="AlphaBeta", color=color)
67+
elif agent_type == "expectimax":
68+
return ExpectimaxAgent(evaluate, depth=args.depth, name="Expectimax", color=color)
69+
return None
70+
71+
white_agent = create_agent(args.white_agent, chess.WHITE)
72+
black_agent = create_agent(args.black_agent, chess.BLACK)
73+
74+
if args.mode == "watch":
75+
# Watch two agents play
76+
print(f"Watching {white_agent.name} (White) vs {black_agent.name} (Black)")
77+
print(f"Move delay: {args.delay} seconds")
78+
watch_agents_play(white_agent, black_agent, move_delay=args.delay)
79+
else:
80+
# Human vs AI
81+
white_name = white_agent.name if white_agent else "Human"
82+
black_name = black_agent.name if black_agent else "Human"
83+
print(f"Playing as {white_name} (White)")
84+
print(f"Against {black_name} (Black)")
85+
print("\nEnter moves in the text field (e.g., 'e2e4' or 'Nf3')")
86+
print("Or click on pieces and squares to make moves")
87+
play_game_with_gui(white_agent, black_agent)
88+
89+
90+
if __name__ == "__main__":
91+
main()
92+

src/gui/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""GUI module for chess visualization."""
2+
from .chess_gui import ChessGUI, play_game_with_gui, watch_agents_play
3+
4+
__all__ = ['ChessGUI', 'play_game_with_gui', 'watch_agents_play']
5+

0 commit comments

Comments
 (0)