Skip to content

Commit 937a47c

Browse files
committed
parallel processing
1 parent f487efd commit 937a47c

File tree

3 files changed

+314
-3
lines changed

3 files changed

+314
-3
lines changed

DominoGame.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,8 @@ def determine_next_starting_player(self, round_winner):
417417
def main():
418418
from DominoPlayer import HumanPlayer, RandomPlayer
419419
# from HumanPlayerWithAnalytics import HumanPlayerWithAnalytics
420-
from analytic_agent_player import AnalyticAgentPlayer
420+
# from analytic_agent_player import AnalyticAgentPlayer
421+
from analytic_agent_player_parallel import AnalyticAgentPlayer
421422

422423
parser = argparse.ArgumentParser(description="Play a game of Dominoes")
423424
parser.add_argument("variant", choices=["cuban", "venezuelan", "international"], help="Choose the domino variant to play")

analytic_agent_player_parallel.py

+310
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
from DominoPlayer import HumanPlayer, available_moves, stats
2+
from collections import defaultdict
3+
from DominoGameState import DominoGameState
4+
# from domino_game_analyzer import DominoTile, PlayerPosition, GameState, get_best_move_alpha_beta, list_possible_moves, PlayerPosition_SOUTH, PlayerPosition_names
5+
# from get_best_move import DominoTile, PlayerPosition, GameState, get_best_move_alpha_beta, list_possible_moves, PlayerPosition_SOUTH, PlayerPosition_names
6+
from domino_data_types import DominoTile, PlayerPosition, GameState, PlayerPosition_SOUTH, PlayerPosition_names, move
7+
from get_best_move2 import get_best_move_alpha_beta, list_possible_moves
8+
from domino_utils import history_to_domino_tiles_history
9+
from domino_game_tracker import domino_game_state_our_perspective, generate_sample_from_game_state
10+
from domino_common_knowledge import CommonKnowledgeTracker
11+
from statistics import mean, median, stdev, mode
12+
import copy
13+
from tqdm import tqdm
14+
from concurrent.futures import ProcessPoolExecutor, as_completed
15+
16+
class AnalyticAgentPlayer(HumanPlayer):
17+
def __init__(self, position: int = 0) -> None:
18+
super().__init__()
19+
self.move_history: list[tuple[int, tuple[tuple[int, int], str]|None]] = []
20+
self.tile_count_history: dict[int, list[int]] = defaultdict(list)
21+
# self.round_scores: list[int] = []
22+
self.first_game = True
23+
self.position = position
24+
25+
def next_move(self, game_state: DominoGameState, player_hand: list[tuple[int,int]], verbose: bool = True) -> tuple[tuple[int,int], str] | None:
26+
if self.first_game:
27+
# Check if the move is forced
28+
if game_state.variant in {'international','venezuelan'} and len(game_state.history)==0:
29+
self.first_game = False
30+
print('Playing mandatory (6,6) opening on the first game.')
31+
return (6,6),'l'
32+
else:
33+
self.first_game = False
34+
35+
unplayed_tiles = self.get_unplayed_tiles(game_state, player_hand)
36+
_unplayed_tiles = DominoTile.loi_to_domino_tiles(unplayed_tiles)
37+
38+
_player_hand = DominoTile.loi_to_domino_tiles(player_hand)
39+
40+
_moves = history_to_domino_tiles_history(game_state.history)
41+
_remaining_tiles = set(_unplayed_tiles)
42+
# _initial_player_tiles = {p: 7 for p in PlayerPosition}
43+
_initial_player_tiles = {p: 7 for p in range(4)}
44+
# _starting_player = PlayerPosition((game_state.history[0][0] - self.position)%4) if len(game_state.history)>0 else PlayerPosition.SOUTH
45+
_starting_player = ((game_state.history[0][0] - self.position)%4) if len(game_state.history)>0 else PlayerPosition_SOUTH
46+
47+
current_player, _final_remaining_tiles, _board_ends, _player_tiles_count, _knowledge_tracker = domino_game_state_our_perspective(
48+
_remaining_tiles, _moves, _initial_player_tiles, current_player=_starting_player)
49+
50+
if verbose:
51+
self.print_verbose_info(_player_hand, _unplayed_tiles, _knowledge_tracker, _player_tiles_count, _starting_player)
52+
53+
num_samples = 1000 if len(game_state.history) > 8 else 100 if len(game_state.history) > 4 else 25 if len(game_state.history) > 0 else 1
54+
best_move = self.get_best_move(set(_player_hand), _remaining_tiles, _knowledge_tracker, _player_tiles_count, _board_ends, num_samples, verbose=verbose)
55+
56+
if best_move is None:
57+
return None
58+
else:
59+
tile, is_left = best_move
60+
side = 'l' if is_left else 'r'
61+
return (tile.top, tile.bottom), side
62+
63+
def print_verbose_info(self, player_hand: list[DominoTile], unplayed_tiles: list[DominoTile], knowledge_tracker: CommonKnowledgeTracker, player_tiles_count: dict[PlayerPosition, int], starting_player: PlayerPosition) -> None:
64+
print("\n--- Verbose Information ---")
65+
# print(f"Starting player: {starting_player.name}")
66+
print(f"Starting player: {PlayerPosition_names[starting_player]}")
67+
print(f"Player's hand: {player_hand}")
68+
print(f"Remaining tiles: {unplayed_tiles}")
69+
print("\nCommon knowledge of missing suits:")
70+
# for player in PlayerPosition:
71+
for player in range(4):
72+
# print(f" {player.name}: {knowledge_tracker.common_knowledge_missing_suits[player]}")
73+
print(f" {PlayerPosition_names[player]}: {knowledge_tracker.common_knowledge_missing_suits[player]}")
74+
print("\nRemaining tiles for each player:")
75+
for player, count in player_tiles_count.items():
76+
# print(f" {player.name}: {count}")
77+
print(f" {PlayerPosition_names[player]}: {count}")
78+
print("----------------------------\n")
79+
80+
def sample_search(self, final_south_hand: set[DominoTile], final_remaining_tiles_without_south_tiles: set[DominoTile], player_tiles_count: dict[PlayerPosition, int], inferred_knowledge_for_current_player: CommonKnowledgeTracker, board_ends: tuple[int|None,int|None]) -> tuple[move, float]:
81+
sample = generate_sample_from_game_state(
82+
# PlayerPosition.SOUTH,
83+
PlayerPosition_SOUTH,
84+
final_south_hand,
85+
final_remaining_tiles_without_south_tiles,
86+
player_tiles_count,
87+
inferred_knowledge_for_current_player
88+
)
89+
90+
sample_hands = (
91+
frozenset(final_south_hand),
92+
frozenset(sample['E']),
93+
frozenset(sample['N']),
94+
frozenset(sample['W'])
95+
)
96+
97+
sample_state = GameState(
98+
player_hands=sample_hands,
99+
# current_player=PlayerPosition.SOUTH,
100+
current_player=PlayerPosition_SOUTH,
101+
left_end=board_ends[0],
102+
right_end=board_ends[1],
103+
consecutive_passes=0
104+
)
105+
106+
depth = 24
107+
108+
# possible_moves = list_possible_moves(sample_state, include_stats=False)
109+
possible_moves = list_possible_moves(sample_state)
110+
111+
sample_cache: dict[GameState, tuple[int, int]] = {}
112+
for move in possible_moves:
113+
if move[0] is None:
114+
new_state = sample_state.pass_turn()
115+
else:
116+
tile, is_left = move[0]
117+
new_state = sample_state.play_hand(tile, is_left)
118+
119+
# _, best_score, _ = get_best_move_alpha_beta(new_state, depth, sample_cache, best_path_flag=False)
120+
_, best_score, _ = get_best_move_alpha_beta(new_state, depth, sample_cache, best_path_flag=False)
121+
return move[0], best_score
122+
123+
def get_best_move(self, final_south_hand: set[DominoTile], remaining_tiles: set[DominoTile],
124+
knowledge_tracker: CommonKnowledgeTracker, player_tiles_count: dict[PlayerPosition, int],
125+
board_ends: tuple[int|None,int|None], num_samples: int = 1000, verbose: bool = False) -> tuple[DominoTile, bool] | None:
126+
127+
inferred_knowledge: dict[PlayerPosition, set[DominoTile]] = {
128+
# player: set() for player in PlayerPosition
129+
player: set() for player in range(4)
130+
}
131+
132+
for tile in remaining_tiles:
133+
# for player in PlayerPosition:
134+
for player in range(4):
135+
if tile.top in knowledge_tracker.common_knowledge_missing_suits[player] or tile.bottom in knowledge_tracker.common_knowledge_missing_suits[player]:
136+
inferred_knowledge[player].add(tile)
137+
138+
final_remaining_tiles_without_south_tiles = remaining_tiles - final_south_hand
139+
# inferred_knowledge_for_current_player = copy.deepcopy(inferred_knowledge)
140+
inferred_knowledge_for_current_player = inferred_knowledge
141+
for player, tiles in inferred_knowledge_for_current_player.items():
142+
inferred_knowledge_for_current_player[player] = tiles - final_south_hand
143+
144+
move_scores = defaultdict(list)
145+
146+
# for _ in tqdm(range(num_samples), desc="Analyzing moves", leave=False):
147+
# sample = generate_sample_from_game_state(
148+
# # PlayerPosition.SOUTH,
149+
# PlayerPosition_SOUTH,
150+
# final_south_hand,
151+
# final_remaining_tiles_without_south_tiles,
152+
# player_tiles_count,
153+
# inferred_knowledge_for_current_player
154+
# )
155+
156+
# sample_hands = (
157+
# frozenset(final_south_hand),
158+
# frozenset(sample['E']),
159+
# frozenset(sample['N']),
160+
# frozenset(sample['W'])
161+
# )
162+
163+
# sample_state = GameState(
164+
# player_hands=sample_hands,
165+
# # current_player=PlayerPosition.SOUTH,
166+
# current_player=PlayerPosition_SOUTH,
167+
# left_end=board_ends[0],
168+
# right_end=board_ends[1],
169+
# consecutive_passes=0
170+
# )
171+
172+
# depth = 24
173+
174+
# # possible_moves = list_possible_moves(sample_state, include_stats=False)
175+
# possible_moves = list_possible_moves(sample_state)
176+
177+
# sample_cache: dict[GameState, tuple[int, int]] = {}
178+
# for move in possible_moves:
179+
# if move[0] is None:
180+
# new_state = sample_state.pass_turn()
181+
# else:
182+
# tile, is_left = move[0]
183+
# new_state = sample_state.play_hand(tile, is_left)
184+
185+
# # _, best_score, _ = get_best_move_alpha_beta(new_state, depth, sample_cache, best_path_flag=False)
186+
# _, best_score, _ = get_best_move_alpha_beta(new_state, depth, sample_cache, best_path_flag=False)
187+
188+
# move_scores[move[0]].append(best_score)
189+
190+
# move, best_score = self.sample_search(final_south_hand, final_remaining_tiles_without_south_tiles, player_tiles_count, inferred_knowledge_for_current_player, board_ends)
191+
# move_scores[move].append(best_score)
192+
193+
# def sample_search(self, final_south_hand: set[DominoTile], final_remaining_tiles_without_south_tiles: set[DominoTile], player_tiles_count: dict[PlayerPosition, int], inferred_knowledge_for_current_player: CommonKnowledgeTracker, board_ends: tuple[int|None,int|None]) -> tuple[move, float]:
194+
195+
# Use ProcessPoolExecutor to parallelize the execution
196+
with ProcessPoolExecutor() as executor:
197+
futures = [
198+
executor.submit(
199+
self.sample_search,
200+
final_south_hand,
201+
final_remaining_tiles_without_south_tiles,
202+
player_tiles_count,
203+
inferred_knowledge_for_current_player,
204+
board_ends
205+
)
206+
for _ in range(num_samples)
207+
# for _ in tqdm(range(num_samples), desc="Analyzing moves", leave=False)
208+
]
209+
for future in tqdm(as_completed(futures), total=num_samples, desc="Analyzing moves", leave=False):
210+
move, best_score = future.result()
211+
move_scores[move].append(best_score)
212+
213+
if not move_scores:
214+
if verbose:
215+
print("No legal moves available. Player must pass.")
216+
return None
217+
218+
if verbose:
219+
self.print_move_statistics(move_scores, num_samples)
220+
221+
best_move = max(move_scores, key=lambda x: mean(move_scores[x]))
222+
return best_move
223+
224+
def print_move_statistics(self, move_scores: dict[move, list[float]], num_samples: int) -> None:
225+
print(f"\nMove Statistics (based on {num_samples} samples):")
226+
227+
# Calculate statistics for each move
228+
move_statistics = {}
229+
for move, scores in move_scores.items():
230+
if len(scores) > 1:
231+
move_statistics[move] = {
232+
"count": len(scores),
233+
"mean": mean(scores),
234+
"std_dev": stdev(scores),
235+
"median": median(scores),
236+
"mode": mode(scores),
237+
"min": min(scores),
238+
"max": max(scores)
239+
}
240+
else:
241+
move_statistics[move] = {
242+
"count": len(scores),
243+
# "mean": scores[0] if scores else None,
244+
# "std_dev": 0,
245+
# "median": scores[0] if scores else None,
246+
# "mode": scores[0] if scores else None,
247+
# "min": scores[0] if scores else None,
248+
# "max": scores[0] if scores else None
249+
"mean": scores[0],
250+
"std_dev": 0,
251+
"median": scores[0],
252+
"mode": scores[0],
253+
"min": scores[0],
254+
"max": scores[0]
255+
}
256+
257+
# Sort moves by their mean score, descending order
258+
sorted_moves = sorted(move_statistics.items(), key=lambda x: x[1]["mean"], reverse=True)
259+
260+
# Print statistics for each move
261+
for move, stats in sorted_moves:
262+
if move is None:
263+
move_str = "Pass"
264+
else:
265+
tile, is_left = move
266+
direction = "left" if is_left else "right"
267+
move_str = f"Play {tile} on the {direction}"
268+
269+
print(f"\nMove: {move_str}")
270+
print(f" Count: {stats['count']}")
271+
print(f" Mean Score: {stats['mean']:.4f}")
272+
print(f" Standard Deviation: {stats['std_dev']:.4f}")
273+
print(f" Median Score: {stats['median']:.4f}")
274+
print(f" Mode Score: {stats['mode']:.4f}")
275+
print(f" Min Score: {stats['min']:.4f}")
276+
print(f" Max Score: {stats['max']:.4f}")
277+
278+
# Identify the best move based on the highest mean score
279+
best_move = max(move_statistics, key=lambda x: move_statistics[x]["mean"])
280+
best_stats = move_statistics[best_move]
281+
282+
print("\nBest Move Overall:")
283+
if best_move is None:
284+
print(f"Best move: Pass")
285+
else:
286+
tile, is_left = best_move
287+
direction = "left" if is_left else "right"
288+
print(f"Best move: Play {tile} on the {direction}")
289+
print(f"Mean Expected Score: {best_stats['mean']:.4f}")
290+
291+
def end_round(self, scores: list[int], team: int) -> None:
292+
super().end_round(scores, team)
293+
# self.record_round_score(scores)
294+
295+
def record_move(self, game_state: DominoGameState, move: tuple[tuple[int, int], str]|None) -> None:
296+
self.move_history.append((game_state.next_player, move))
297+
for i, count in enumerate(game_state.player_tile_counts):
298+
self.tile_count_history[i].append(count)
299+
300+
# def record_round_score(self, scores: list[int]) -> None:
301+
# self.round_scores.append(scores)
302+
303+
def get_move_history(self) -> list[tuple[int, tuple[tuple[int, int], str]|None]]:
304+
return self.move_history
305+
306+
def get_tile_count_history(self) -> dict[int, list[int]]:
307+
return dict(self.tile_count_history)
308+
309+
# def get_round_scores(self) -> list[int]:
310+
# return self.round_scores

get_best_move2.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import math
33
from domino_data_types import GameState, DominoTile, move, PlayerPosition, PlayerPosition_SOUTH, PlayerPosition_NORTH
44

5-
def min_max_alpha_beta(state: GameState, depth: int, alpha: float, beta: float, cache: dict[GameState, tuple[int, int]] = {}, best_path_flag: bool = True) -> tuple[tuple[DominoTile, bool]|None, float, list[tuple[PlayerPosition, tuple[DominoTile, bool]|None]]]:
5+
def min_max_alpha_beta(state: GameState, depth: int, alpha: float, beta: float, cache: dict[GameState, tuple[int, int]] = {}, best_path_flag: bool = True) -> tuple[move, float, list[tuple[PlayerPosition, move]]]:
66
"""
77
Implement the min-max algorithm with alpha-beta pruning for the domino game, including the optimal path.
88
@@ -182,7 +182,7 @@ def count_game_stats(initial_state: GameState, print_stats: bool = True, cache:
182182

183183
return total_games, exp_score
184184

185-
def list_possible_moves(state: GameState) -> list[tuple[tuple[DominoTile, bool]|None, int|None, float|None]]:
185+
def list_possible_moves(state: GameState) -> list[tuple[move, int|None, float|None]]:
186186
"""
187187
List all possible moves for the current player in the given game state,
188188
optionally including the number of possible outcomes and expected score for each move.

0 commit comments

Comments
 (0)