Skip to content

Commit c88bfa9

Browse files
committed
Remove duplciated code and update mypy require to 1.18.2
1 parent 4e90b6f commit c88bfa9

3 files changed

Lines changed: 5 additions & 268 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ tests = [
6767
"uv>=0.5.21",
6868
]
6969
tools = [
70-
"mypy>=1.17.0",
70+
"mypy>=1.18.2",
7171
"ruff>=0.9.2",
7272
"codespell>=2.3.0",
7373
"pre-commit>=4.2.0",
@@ -80,7 +80,7 @@ packages = ["src/checkers", "src/checkers_computer_players"]
8080
[tool.hatch.build.targets.wheel.hooks.mypyc]
8181
dependencies = [
8282
"hatch-mypyc>=0.16.0",
83-
"mypy>=1.17.0",
83+
"mypy>=1.18.2",
8484
]
8585
require-runtime-dependencies = true
8686
include = [

src/checkers/state.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def __str__(self) -> str:
123123
for y in range(h):
124124
line = []
125125
for x in range(w):
126-
if (x + y + 1) % 2 != 0:
126+
if (x + y + 1) & 1 != 0:
127127
# line.append("_")
128128
line.append(" ")
129129
continue

src/checkers_computer_players/minimax_ai.py

Lines changed: 2 additions & 265 deletions
Original file line numberDiff line numberDiff line change
@@ -27,25 +27,17 @@
2727
__author__ = "CoolCat467"
2828
__version__ = "0.0.0"
2929

30-
import math
31-
import random
32-
import time
3330
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
3732

38-
from checkers.state import Action, State
3933
from checkers_computer_players.checkers_minimax import CheckersMinimax
4034
from checkers_computer_players.machine_client import (
4135
RemoteState,
4236
run_clients_in_local_servers_sync,
4337
)
44-
from checkers_computer_players.minimax import Minimax, MinimaxResult, Player
4538

4639
if TYPE_CHECKING:
47-
from collections.abc import Iterable
48-
40+
from checkers.state import Action
4941

5042
T = TypeVar("T")
5143

@@ -54,261 +46,6 @@
5446
# 1 = True = AI (Us) = MAX = 1, 3
5547

5648

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-
31249
class MinimaxPlayer(RemoteState):
31350
"""Minimax Player."""
31451

0 commit comments

Comments
 (0)