|
27 | 27 | __author__ = "CoolCat467" |
28 | 28 | __version__ = "0.0.0" |
29 | 29 |
|
30 | | -import math |
31 | | -import random |
32 | | -import time |
33 | 30 | import traceback |
34 | | -from collections import Counter |
35 | | -from math import inf as infinity |
36 | | -from typing import TYPE_CHECKING, Any, ClassVar, TypeVar |
| 31 | +from typing import TYPE_CHECKING, TypeVar |
37 | 32 |
|
38 | | -from checkers.state import Action, State |
39 | 33 | from checkers_computer_players.checkers_minimax import CheckersMinimax |
40 | 34 | from checkers_computer_players.machine_client import ( |
41 | 35 | RemoteState, |
42 | 36 | run_clients_in_local_servers_sync, |
43 | 37 | ) |
44 | | -from checkers_computer_players.minimax import Minimax, MinimaxResult, Player |
45 | 38 |
|
46 | 39 | if TYPE_CHECKING: |
47 | | - from collections.abc import Iterable |
48 | | - |
| 40 | + from checkers.state import Action |
49 | 41 |
|
50 | 42 | T = TypeVar("T") |
51 | 43 |
|
|
54 | 46 | # 1 = True = AI (Us) = MAX = 1, 3 |
55 | 47 |
|
56 | 48 |
|
57 | | -class MinimaxWithID(Minimax[State, Action]): |
58 | | - """Minimax with ID.""" |
59 | | - |
60 | | - __slots__ = () |
61 | | - |
62 | | - # Simple Transposition Table: |
63 | | - # key → (stored_depth, value, action, flag) |
64 | | - # flag: 'EXACT', 'LOWERBOUND', 'UPPERBOUND' |
65 | | - TRANSPOSITION_TABLE: ClassVar[ |
66 | | - dict[int, tuple[int, MinimaxResult[Any], str]] |
67 | | - ] = {} |
68 | | - |
69 | | - @classmethod |
70 | | - def _transposition_table_lookup( |
71 | | - cls, |
72 | | - state_hash: int, |
73 | | - depth: int, |
74 | | - alpha: float, |
75 | | - beta: float, |
76 | | - ) -> MinimaxResult[Action] | None: |
77 | | - """Lookup in transposition_table. Return (value, action) or None.""" |
78 | | - entry = cls.TRANSPOSITION_TABLE.get(state_hash) |
79 | | - if entry is None: |
80 | | - return None |
81 | | - |
82 | | - stored_depth, result, flag = entry |
83 | | - # only use if stored depth is deep enough |
84 | | - if stored_depth >= depth and ( |
85 | | - (flag == "EXACT") |
86 | | - or (flag == "LOWERBOUND" and result.value > alpha) |
87 | | - or (flag == "UPPERBOUND" and result.value < beta) |
88 | | - ): |
89 | | - return result |
90 | | - return None |
91 | | - |
92 | | - @classmethod |
93 | | - def _transposition_table_store( |
94 | | - cls, |
95 | | - state_hash: int, |
96 | | - depth: int, |
97 | | - result: MinimaxResult[Action], |
98 | | - alpha: float, |
99 | | - beta: float, |
100 | | - ) -> None: |
101 | | - """Store in transposition_table with proper flag.""" |
102 | | - if result.value <= alpha: |
103 | | - flag = "UPPERBOUND" |
104 | | - elif result.value >= beta: |
105 | | - flag = "LOWERBOUND" |
106 | | - else: |
107 | | - flag = "EXACT" |
108 | | - cls.TRANSPOSITION_TABLE[state_hash] = (depth, result, flag) |
109 | | - |
110 | | - @classmethod |
111 | | - def hash_state(cls, state: State) -> int: |
112 | | - """Your state-to-hash function. Must be consistent.""" |
113 | | - # For small games you might do: return hash(state) |
114 | | - # For larger, use Zobrist or custom. |
115 | | - return hash(state) |
116 | | - |
117 | | - @classmethod |
118 | | - def alphabeta_transposition_table( |
119 | | - cls, |
120 | | - state: State, |
121 | | - depth: int = 5, |
122 | | - a: int | float = -infinity, |
123 | | - b: int | float = infinity, |
124 | | - ) -> MinimaxResult[Action]: |
125 | | - """AlphaBeta with transposition table.""" |
126 | | - if cls.terminal(state): |
127 | | - return MinimaxResult(cls.value(state), None) |
128 | | - if depth <= 0: |
129 | | - # Choose a random action |
130 | | - # No need for cryptographic secure random |
131 | | - return MinimaxResult( |
132 | | - cls.value(state), |
133 | | - random.choice(tuple(cls.actions(state))), |
134 | | - ) |
135 | | - next_down = depth - 1 |
136 | | - |
137 | | - state_h = cls.hash_state(state) |
138 | | - # 1) Try transposition_table lookup |
139 | | - transposition_table_hit = cls._transposition_table_lookup( |
140 | | - state_h, |
141 | | - depth, |
142 | | - a, |
143 | | - b, |
144 | | - ) |
145 | | - if transposition_table_hit is not None: |
146 | | - return transposition_table_hit |
147 | | - next_down = None if depth is None else depth - 1 |
148 | | - |
149 | | - current_player = cls.player(state) |
150 | | - value: int | float |
151 | | - |
152 | | - best_action: Action | None = None |
153 | | - |
154 | | - if current_player == Player.MAX: |
155 | | - value = -infinity |
156 | | - for action in cls.actions(state): |
157 | | - child = cls.alphabeta_transposition_table( |
158 | | - cls.result(state, action), |
159 | | - next_down, |
160 | | - a, |
161 | | - b, |
162 | | - ) |
163 | | - if child.value > value: |
164 | | - value = child.value |
165 | | - best_action = action |
166 | | - a = max(a, value) |
167 | | - if a >= b: |
168 | | - break |
169 | | - |
170 | | - elif current_player == Player.MIN: |
171 | | - value = infinity |
172 | | - for action in cls.actions(state): |
173 | | - child = cls.alphabeta_transposition_table( |
174 | | - cls.result(state, action), |
175 | | - next_down, |
176 | | - a, |
177 | | - b, |
178 | | - ) |
179 | | - if child.value < value: |
180 | | - value = child.value |
181 | | - best_action = action |
182 | | - b = min(b, value) |
183 | | - if b <= a: |
184 | | - break |
185 | | - else: |
186 | | - raise NotImplementedError(f"{current_player = }") |
187 | | - |
188 | | - # 2) Store in transposition_table |
189 | | - result = MinimaxResult(value, best_action) |
190 | | - cls._transposition_table_store(state_h, depth, result, a, b) |
191 | | - return result |
192 | | - |
193 | | - @classmethod |
194 | | - def iterative_deepening( |
195 | | - cls, |
196 | | - state: State, |
197 | | - start_depth: int = 5, |
198 | | - max_depth: int = 7, |
199 | | - time_limit_ns: int | float | None = None, |
200 | | - ) -> MinimaxResult[Action]: |
201 | | - """Run alpha-beta with increasing depth up to max_depth. |
202 | | -
|
203 | | - If time_limit_ns is None, do all depths. Otherwise stop early. |
204 | | - """ |
205 | | - best_result: MinimaxResult[Action] = MinimaxResult(0, None) |
206 | | - start_t = time.perf_counter_ns() |
207 | | - |
208 | | - for depth in range(start_depth, max_depth + 1): |
209 | | - # clear or keep transposition_table between depths? often you keep it |
210 | | - # cls.TRANSPOSITION_TABLE.clear() |
211 | | - |
212 | | - result = cls.alphabeta_transposition_table( |
213 | | - state, |
214 | | - depth, |
215 | | - ) |
216 | | - best_result = result |
217 | | - |
218 | | - # Optional: if you find a forced win/loss you can stop |
219 | | - if abs(result.value) == cls.HIGHEST: |
220 | | - print(f"reached terminal state stop {depth=}") |
221 | | - break |
222 | | - |
223 | | - # optional time check |
224 | | - if ( |
225 | | - time_limit_ns |
226 | | - and (time.perf_counter_ns() - start_t) > time_limit_ns |
227 | | - ): |
228 | | - print( |
229 | | - f"break from time expired {depth=} ({(time.perf_counter_ns() - start_t) / 1e9} seconds elaped)", |
230 | | - ) |
231 | | - break |
232 | | - print( |
233 | | - f"{depth=} ({(time.perf_counter_ns() - start_t) / 1e9} seconds elaped)", |
234 | | - ) |
235 | | - |
236 | | - return best_result |
237 | | - |
238 | | - |
239 | | -# Minimax[State, Action] |
240 | | -class CheckersMinimax(MinimaxWithID): |
241 | | - """Minimax Algorithm for Checkers.""" |
242 | | - |
243 | | - __slots__ = () |
244 | | - |
245 | | - @classmethod |
246 | | - def hash_state(cls, state: State) -> int: |
247 | | - """Return state hash value.""" |
248 | | - # For small games you might do: return hash(state) |
249 | | - # For larger, use Zobrist or custom. |
250 | | - return hash((state.size, tuple(state.pieces.items()), state.turn)) |
251 | | - |
252 | | - @staticmethod |
253 | | - def value(state: State) -> int | float: |
254 | | - """Return value of given game state.""" |
255 | | - # Return winner if possible |
256 | | - win = state.check_for_win() |
257 | | - # If no winner, we have to predict the value |
258 | | - if win is None: |
259 | | - # We'll estimate the value by the pieces in play |
260 | | - counts = Counter(state.pieces.values()) |
261 | | - # Score is pawns plus 3 times kings |
262 | | - min_ = counts[0] + 3 * counts[2] |
263 | | - max_ = counts[1] + 3 * counts[3] |
264 | | - # More max will make score higher, |
265 | | - # more min will make score lower |
266 | | - # Plus one in divisor makes so never / 0 |
267 | | - return (max_ - min_) / (max_ + min_ + 1) |
268 | | - return win * 2 - 1 |
269 | | - |
270 | | - @staticmethod |
271 | | - def terminal(state: State) -> bool: |
272 | | - """Return if game state is terminal.""" |
273 | | - return state.check_for_win() is not None |
274 | | - |
275 | | - @staticmethod |
276 | | - def player(state: State) -> Player: |
277 | | - """Return Player enum from current state's turn.""" |
278 | | - return Player.MAX if state.get_turn() else Player.MIN |
279 | | - |
280 | | - @staticmethod |
281 | | - def actions(state: State) -> Iterable[Action]: |
282 | | - """Return all actions that are able to be performed for the current player in the given state.""" |
283 | | - return state.get_all_actions(int(state.get_turn())) |
284 | | - |
285 | | - @staticmethod |
286 | | - def result(state: State, action: Action) -> State: |
287 | | - """Return new state after performing given action on given current state.""" |
288 | | - return state.perform_action(action) |
289 | | - |
290 | | - @classmethod |
291 | | - def adaptive_depth_minimax( |
292 | | - cls, |
293 | | - state: State, |
294 | | - minimum: int, |
295 | | - maximum: int, |
296 | | - ) -> MinimaxResult[Action]: |
297 | | - """Return minimax result from adaptive max depth.""" |
298 | | - ## types = state.pieces.values() |
299 | | - ## current = len(types) |
300 | | - ## w, h = state.size |
301 | | - ## max_count = w * h // 6 << 1 |
302 | | - ## old_depth = (1 - (current / max_count)) * math.floor( |
303 | | - ## math.sqrt(w**2 + h**2) |
304 | | - ## ) |
305 | | - |
306 | | - depth = cls.value(state) * maximum + minimum |
307 | | - final_depth = min(maximum, max(minimum, math.floor(depth))) |
308 | | - print(f"{depth = } {final_depth = }") |
309 | | - return cls.minimax(state, final_depth) |
310 | | - |
311 | | - |
312 | 49 | class MinimaxPlayer(RemoteState): |
313 | 50 | """Minimax Player.""" |
314 | 51 |
|
|
0 commit comments