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 ("\n Common 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 ("\n Remaining 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"\n Move 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"\n Move: { 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 ("\n Best 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
0 commit comments