Xiangiqgame
AI engine for Xiangqi
Loading...
Searching...
No Matches
player_summary.py
Go to the documentation of this file.
1"""
2Contains PlayerSummary class.
3"""
4
5from dataclasses import dataclass
6from typing import Dict, List
7
8import numpy as np
9import pandas as pd
10import xiangqi_bindings as xb
11
13from xiangqipy.enums import PlayerType, EvaluatorType
14from xiangqipy.core_dataclass_mirrors import PointsT
15
16
17@dataclass
19 """
20 Data container for data from one xiangqipy.game_interfaces.Player in a Game.
21
22 @param color xiangi_xb.PieceColoar: color of Player the summary belongs to.
23 @param player_type xiangipy.enums.PlayerType: type of the Player.
24 @param move_evaluator_type xiangqipy.enums.EvaluatorType: type of Player's move evaluator.
25 @param max_search_depth int: maximum depth that a Minimax move evaluator will search.
27 container with summary data of each Move selected by Player.
28 """
29
30 color: xb.PieceColor
31 player_type: PlayerType
32 move_evaluator_type: EvaluatorType = EvaluatorType.NULL
33 max_search_depth: int = None
34 zobrist_key_size: int = None
35 zkeys_seed: int = None
36 search_summaries: cdm.SearchSummaries = None
37
38 @property
39 def has_search_summaries(self) -> bool:
40 """
41 Property indicating if PlayerSummary object has search summaries.
42
43 Expect to be True for MinimaxMoveEvaluators, False otherwise.
44 """
45 return self.search_summaries is not None
46
47 @property
48 def player_move_count(self) -> int:
49 """
50 Number of moves selected by Player during Game.
51 """
52 return len(self.search_summaries.first_searches)
53
54 @property
55 def game_move_numbers(self) -> List[int]:
56 """
57 Converts Players' move numbers in to overall Game Move numbers
58 (red = odd ints, black = even ints).
59 """
60 if self.colorcolor == xb.PieceColor.kRed:
61 return list(range(1, 2 * self.player_move_count, 2))
62 if self.colorcolor == xb.PieceColor.kBlk:
63 return list(range(2, 2 * self.player_move_count + 1, 2))
64
65 @property
67 self,
68 ) -> Dict[str, pd.DataFrame] | None:
69 """
70 Dictionary with strings corresponding to
71 xiangqi_xb.MinimaxResultType values as keys, and DataFrame of
72 result type counts as values.
73
74 Each row of data frame -> move number, each col -> a value of
75 remaining search depth when result was obtained.
76 """
77 if not self.has_search_summaries:
78 return None
79 result = {}
80 for name, value in xb.MinimaxResultType.__members__.items():
81 new_df = pd.DataFrame(
82 [
83 search_summary.result_depth_counts[value, :]
84 for search_summary in self.search_summaries.first_searches
85 ]
86 )
87 new_df.index = self.game_move_numbers
88 new_df.index.name = "game_move_numbers"
89 new_df.columns = [
90 f"remaining_depth={col_idx}"
91 for col_idx in range(new_df.shape[1])
92 ]
93 result[name] = new_df
94
95 return result
96
97 @property
98 def first_searches_by_type(self) -> pd.DataFrame | None:
99 """
100 Dataframe with row -> move number, col -> xiangqi_xb.MinimaxResultType.
101
102 Named 'first_searches...' in constrast to 'extra_searches...' or
103 'second_searches...' which would occur if first_search of
104 MinimaxMoveEvaluator.select_move returns an illegal move. After fixing bug
105 that was allowing moves that violated repeated move rule to be returned
106 by first search, have never recorded any second searches.
107 """
108 if not self.has_search_summaries:
109 return None
110
111 # Get the list of MinimaxResultType names
112 result_type_names = list(xb.MinimaxResultType.__members__.keys())
113 num_result_types = len(result_type_names)
114
115 # Preallocate the DataFrame with zeros (dtype=int64)
116 num_first_searches = len(self.search_summaries.first_searches)
117 df = pd.DataFrame(
118 np.zeros(
119 (num_first_searches, num_result_types), dtype=np.int64
120 ), # Preallocate with zeros as integers
121 index=self.game_move_numbers,
122 columns=result_type_names,
123 )
124 df.index.name = "game_move_numbers"
125
126 # Populate the DataFrame by itescore over result types
127 for idx, (name, value) in enumerate(
128 xb.MinimaxResultType.__members__.items()
129 ):
130 for search_idx, search_summary in enumerate(
131 self.search_summaries.first_searches
132 ):
133 # Sum the result_depth_counts and set the value in the DataFrame
134 df.iloc[search_idx, idx] = sum(
135 search_summary.result_depth_counts[value][:]
136 )
137
138 assert df["Unknown"].sum() == 0
139 df.drop(columns=["Unknown"], inplace=True)
140
141 return df
142
143 @property
145 self,
146 ) -> int | None:
147 """
148 Transposition table size the first time any illegal move was returned
149 by first search. This has been NaN after fixing first search's repeat
150 move rule bug.
151 """
152 if self.has_search_summaries and self.search_summaries.extra_searches:
153 return self.search_summaries.extra_searches[
154 min(self.search_summaries.extra_searches)
155 ].tr_table_size_initial
156
157 @property
158 def tr_table_size_end_game(self) -> int | None:
159 """
160 Size of transposition table at end of game.
161 """
162 if self.has_search_summaries:
163 return self.search_summaries.first_searches[-1].tr_table_size_final
164
165 @property
166 def tr_table_sizes_at_events(self) -> cdm.TranspositionTableSizesAtEvents:
167 """
168 Transposition table size at first illegal move and end of game wrapped
169 into single convenience property.
170 """
171 return cdm.TranspositionTableSizesAtEvents(
172 first_illegal_move_request=self.tr_table_size_first_illegal_move_request,
173 end_game=self.tr_table_size_end_game,
174 )
175
176 @property
177 def first_search_stats(self) -> pd.DataFrame | None:
178 """
179 Dataframe with row -> move number; cols -> number of nodes explored,
180 total time for move selection, average time per node, and minimax
181 evaluation score of the selected move.
182 """
183 if not self.has_search_summaries:
184 return None
185
186 num_nodes = np.array(
187 [
188 search_summary.num_nodes
189 for search_summary in self.search_summaries.first_searches
190 ]
191 )
192
193 search_time_s = np.array(
194 [
195 search_summary.time.total_seconds()
196 for search_summary in self.search_summaries.first_searches
197 ]
198 )
199
200 mean_time_per_node_ns = np.array(
201 [
202 search_summary.mean_time_per_node_ns
203 for search_summary in self.search_summaries.first_searches
204 ]
205 )
206
207 eval_score = np.array(
208 [
209 search_summary.equal_score_moves.shared_score
210 for search_summary in self.search_summaries.first_searches
211 ],
212 dtype=PointsT,
213 )
214
215 tr_table_size = [
216 search_summary.tr_table_size_final
217 for search_summary in self.search_summaries.first_searches
218 ]
219
220 returned_illegal_move = np.array(
221 [
222 search_summary.returned_illegal_move
223 for search_summary in self.search_summaries.first_searches
224 ]
225 )
226
227 num_collisions = np.array(
228 [
229 search_summary.num_collisions
230 for search_summary in self.search_summaries.first_searches
231 ]
232 )
233
234 # cumulative_illegal_moves = np.cumsum(returned_illegal_move)
235
236 df = pd.DataFrame(
237 {
238 "num_nodes": num_nodes,
239 "search_time_s": search_time_s,
240 "mean_time_per_node_ns": mean_time_per_node_ns,
241 "eval_score": eval_score,
242 "tr_table_size": tr_table_size,
243 # "tr_table_num_entries": tr_table_num_entries,
244 "returned_illegal_move": returned_illegal_move,
245 "num_collisions": num_collisions,
246 # "cumulative_illegal_moves": cumulative_illegal_moves,
247 },
248 index=self.game_move_numbers,
249 )
250 df.index.name = "game_move_numbers"
251
252 return df
253
254 @property
255 def selection_stats(self) -> pd.Series | None:
256 """
257 Pandas Series with mean & max nodes per move, mean & max time per move,
258 and number of illegal move requests.
259 """
260
261 if self.has_search_summaries:
262 nodes_per_move = self.first_search_stats["num_nodes"].mean()
263 time_per_move_s = self.first_search_stats["search_time_s"].mean()
264 time_per_node_ns = self.first_search_stats[
265 "mean_time_per_node_ns"
266 ].mean()
267 num_illegal_move_requests = len(
268 self.search_summaries.extra_searches
269 )
270 num_collisions = self.first_search_stats["num_collisions"].sum()
271 collisions_per_move = (
272 self.first_search_stats["num_collisions"]
273 ).sum() / self.player_move_count
274 collisions_per_node = (
275 self.first_search_stats["num_collisions"].sum()
276 / self.first_search_stats["num_nodes"].sum()
277 )
278
279 return pd.Series(
280 [
281 self.max_search_depth,
282 self.zobrist_key_size,
283 self.zkeys_seed,
284 nodes_per_move,
285 time_per_move_s,
286 time_per_node_ns,
287 num_illegal_move_requests,
288 num_collisions,
289 collisions_per_move,
290 collisions_per_node,
291 # self.tr_table_size_first_illegal_move_request,
293 # self.tr_table_size_end_game,
295 ],
296 index=[
297 "search_depth",
298 "zobrist_key_size",
299 "zkeys_seed",
300 "nodes_per_move",
301 "time_per_move_s",
302 "time_per_node_ns",
303 "num_illegal_move_requests",
304 "num_collisions",
305 "collisions_per_move",
306 "collisions_per_node",
307 # "tr_table_num_states_first_illegal_move_request",
308 "tr_table_size_first_illegal_move_request",
309 # "tr_table_num_states_end_game",
310 "tr_table_size_end_game",
311 ],
312 )
Enum indicating type of core MoveEvaluator used for a Player.
Definition: enums.py:28
Can take a turn in a Game.
Data container for data from one xiangqipy.game_interfaces.Player in a Game.
cdm.TranspositionTableSizesAtEvents tr_table_sizes_at_events(self)
Transposition table size at first illegal move and end of game wrapped into single convenience proper...
int|None tr_table_size_first_illegal_move_request(self)
Transposition table size the first time any illegal move was returned by first search.
pd.Series|None selection_stats(self)
Pandas Series with mean & max nodes per move, mean & max time per move, and number of illegal move re...
int|None tr_table_size_end_game(self)
Size of transposition table at end of game.
pd.DataFrame|None first_search_stats(self)
Dataframe with row -> move number; cols -> number of nodes explored, total time for move selection,...
pd.DataFrame|None first_searches_by_type(self)
Dataframe with row -> move number, col -> xiangqi_xb.MinimaxResultType.
int player_move_count(self)
Number of moves selected by Player during Game.
bool has_search_summaries(self)
Property indicating if PlayerSummary object has search summaries.
List[int] game_move_numbers(self)
Converts Players' move numbers in to overall Game Move numbers (red = odd ints, black = even ints).
Dict[str, pd.DataFrame]|None first_searches_by_type_and_depth(self)
Dictionary with strings corresponding to xiangqi_xb.MinimaxResultType values as keys,...
Contains classes that mirror the structure of some core C++ classes, primarily to facilitate easy IO ...
Enums that are only used on the Python side of the app.
Definition: enums.py:1