Skip to content

Commit d3618e5

Browse files
committed
feat: Update AlphaBetaEngine to use depth_limit parameter consistently across documentation and code
1 parent d20b9f0 commit d3618e5

File tree

12 files changed

+77
-54
lines changed

12 files changed

+77
-54
lines changed

docs/source/engine.rst

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ Basic usage::
105105
from draughts.engine import AlphaBetaEngine
106106
107107
board = get_board('standard')
108-
engine = AlphaBetaEngine(depth=5)
108+
engine = AlphaBetaEngine(depth_limit=5)
109109
110110
# Get best move
111111
best_move = engine.get_best_move(board)
@@ -118,9 +118,9 @@ Basic usage::
118118
With time limits::
119119

120120
# Search with 1-second time limit instead of fixed depth
121-
engine = AlphaBetaEngine(depth=20, time_limit=1.0)
121+
engine = AlphaBetaEngine(depth_limit=20, time_limit=1.0)
122122
move, score = engine.get_best_move(board, with_evaluation=True)
123-
# Engine will iteratively deepen up to depth 20 or until time expires
123+
# Engine will iteratively deepen up to depth_limit or until time expires
124124

125125
Custom Engine Implementation
126126
-----------------------------
@@ -183,7 +183,7 @@ Single Engine for Both Sides::
183183
from draughts.server import Server
184184
185185
board = StandardBoard()
186-
engine = AlphaBetaEngine(depth=6)
186+
engine = AlphaBetaEngine(depth_limit=6)
187187
188188
# Same engine plays for both colors
189189
server = Server(
@@ -202,8 +202,8 @@ Engine vs Engine Match::
202202
board = StandardBoard()
203203
204204
# Different engines/configurations for each side
205-
white_engine = AlphaBetaEngine(depth=6)
206-
black_engine = AlphaBetaEngine(depth=4)
205+
white_engine = AlphaBetaEngine(depth_limit=6)
206+
black_engine = AlphaBetaEngine(depth_limit=4)
207207
208208
server = Server(
209209
board=board,
@@ -269,12 +269,12 @@ Search Options
269269
Time-based search (default)::
270270

271271
# 2 seconds per move
272-
engine = HubEngine("scan.exe", time_per_move=2.0)
272+
engine = HubEngine("scan.exe", time_limit=2.0)
273273

274274
Fixed depth search::
275275

276276
# Search to depth 15
277-
engine = HubEngine("scan.exe", depth=15)
277+
engine = HubEngine("scan.exe", depth_limit=15)
278278

279279
Engine vs HubEngine Match
280280
~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -285,9 +285,9 @@ You can pit the built-in AlphaBetaEngine against an external Hub engine::
285285
from draughts.engine import AlphaBetaEngine
286286
287287
board = StandardBoard()
288-
alphabeta = AlphaBetaEngine(depth=6)
288+
alphabeta = AlphaBetaEngine(depth_limit=6)
289289
290-
with HubEngine("scan.exe", time_per_move=1.0) as scan:
290+
with HubEngine("scan.exe", time_limit=1.0) as scan:
291291
while not board.is_over():
292292
if board.turn.value == 0: # White
293293
move = alphabeta.get_best_move(board)

docs/source/server.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ To enable the "Engine Move" button in the UI, pass an engine to the server::
3434
from draughts.server import Server
3535
3636
board = get_board('standard')
37-
engine = AlphaBetaEngine(depth=6)
37+
engine = AlphaBetaEngine(depth_limit=6)
3838
3939
# Engine plays for both sides when you click "Engine Move"
4040
server = Server(
@@ -57,8 +57,8 @@ for testing and comparing engine implementations::
5757
board = get_board('standard')
5858
5959
# Create two engines with different configurations
60-
white_engine = AlphaBetaEngine(depth=6)
61-
black_engine = AlphaBetaEngine(depth=4)
60+
white_engine = AlphaBetaEngine(depth_limit=6)
61+
black_engine = AlphaBetaEngine(depth_limit=4)
6262
6363
server = Server(
6464
board=board,

draughts/engine.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def get_best_move(
7676
Either a Move object, or tuple of (Move, float) if with_evaluation=True
7777
7878
Example:
79-
>>> engine = AlphaBetaEngine(depth=3)
79+
>>> engine = AlphaBetaEngine(depth_limit=3)
8080
>>> move = engine.get_best_move(board)
8181
>>> move, score = engine.get_best_move(board, with_evaluation=True)
8282
@@ -99,8 +99,8 @@ class AlphaBetaEngine(Engine):
9999
- Advanced Evaluation (PST)
100100
"""
101101

102-
def __init__(self, depth: int, time_limit: float | None = None):
103-
self.depth = depth
102+
def __init__(self, depth_limit: int = 6, time_limit: float | None = None):
103+
self.depth_limit = depth_limit
104104
self.time_limit = time_limit
105105
self.nodes: int = 0
106106
self.tt: dict[int, tuple[int, int, float, Move | None]] = {} # {hash: (depth, flag, score, best_move)}
@@ -197,7 +197,7 @@ def get_best_move(self, board: Board, with_evaluation: bool = False) -> Move | t
197197
best_score = -INF
198198

199199
# Iterative Deepening
200-
max_depth = self.depth
200+
max_depth = self.depth_limit
201201

202202
for d in range(1, max_depth + 1):
203203
try:
@@ -272,7 +272,7 @@ def negamax(self, board: Board, depth: int, alpha: float, beta: float, h: int) -
272272

273273
legal_moves = list(board.legal_moves)
274274
if not legal_moves:
275-
return -CHECKMATE + (self.depth - depth)
275+
return -CHECKMATE + (self.depth_limit - depth)
276276

277277
# Check for draw
278278
if board.is_draw:

draughts/server/server.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from draughts.boards.base import BaseBoard, Color
2222
from draughts.engine import Engine
23+
from draughts.hub import HubEngine
2324

2425

2526
class PositionResponse(BaseModel):
@@ -74,6 +75,11 @@ def __init__(
7475
self._lock = threading.RLock()
7576
self.engine_depth = 6
7677

78+
# Start any HubEngine instances
79+
for engine in [self.white_engine, self.black_engine]:
80+
if isinstance(engine, HubEngine) and not engine._started:
81+
engine.start()
82+
7783
self._setup_routes()
7884

7985
def _setup_routes(self) -> None:
@@ -284,10 +290,10 @@ def set_depth(self, depth: int) -> dict:
284290
with self._lock:
285291
self.engine_depth = depth
286292

287-
# Update depth on both engines if they have the attribute
293+
# Update depth_limit on both engines if they have the attribute
288294
for engine in [self.white_engine, self.black_engine]:
289-
if engine is not None and hasattr(engine, 'depth'):
290-
engine.depth = depth
295+
if engine is not None and hasattr(engine, 'depth_limit'):
296+
engine.depth_limit = depth
291297

292298
return {"depth": self.engine_depth}
293299

@@ -304,7 +310,19 @@ def _get_engine_name(engine: Optional[Engine]) -> Optional[str]:
304310

305311
def run(self, **kwargs):
306312
"""Start the server."""
307-
uvicorn.run(self.APP, **kwargs)
313+
try:
314+
uvicorn.run(self.APP, **kwargs)
315+
finally:
316+
self._cleanup_engines()
317+
318+
def _cleanup_engines(self) -> None:
319+
"""Quit any HubEngine instances."""
320+
for engine in [self.white_engine, self.black_engine]:
321+
if isinstance(engine, HubEngine):
322+
try:
323+
engine.quit()
324+
except Exception:
325+
pass
308326

309327

310328
if __name__ == "__main__":
@@ -314,11 +332,11 @@ def run(self, **kwargs):
314332
logger.add(sys.stderr, level="DEBUG")
315333

316334
from draughts.engine import AlphaBetaEngine
317-
from draughts import get_board
335+
from draughts import get_board , HubEngine
318336

319337
# Example: Two engines playing against each other
320-
white_engine = AlphaBetaEngine(depth=6)
321-
black_engine = AlphaBetaEngine(depth=6)
338+
white_engine = AlphaBetaEngine(depth_limit=6)
339+
black_engine = HubEngine('./scan_engine/scan.exe', depth_limit=6)
322340

323341
board = get_board("standard")
324342
server = Server(

examples/alphabeta_vs_scan.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def play_game(scan_path: str, max_moves: int = 100) -> None:
3434
board = StandardBoard()
3535

3636
# AlphaBeta plays White
37-
alphabeta = AlphaBetaEngine(depth=9)
37+
alphabeta = AlphaBetaEngine(depth_limit=9)
3838

3939
# Scan plays Black
4040
print(f"Starting Scan engine from: {scan_path}")
@@ -43,7 +43,7 @@ def play_game(scan_path: str, max_moves: int = 100) -> None:
4343
scan.new_game()
4444

4545
print("\n=== Game Start ===")
46-
print(f"White: AlphaBeta (depth=9)")
46+
print(f"White: AlphaBeta (depth_limit=9)")
4747
print(f"Black: {scan.info.name} {scan.info.version}")
4848
print()
4949

readme.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ Legal moves are: ['31-27', '31-26', '32-28', '32-27', '33-29', '33-28', '34-30',
150150

151151
```python
152152
>>> from draughts.engine import AlphaBetaEngine
153-
>>> engine = AlphaBetaEngine(depth=5)
153+
>>> engine = AlphaBetaEngine(depth_limit=5)
154154
>>> engine.get_best_move(board, with_evaluation=True)
155155
Move: 28->37, 3.0
156156
```
@@ -172,7 +172,7 @@ via the Hub protocol:
172172

173173
```python
174174
>>> from draughts import HubEngine, StandardBoard
175-
>>> with HubEngine("path/to/scan.exe", time_per_move=1.0) as engine:
175+
>>> with HubEngine("path/to/scan.exe", time_limit=1.0) as engine:
176176
... board = StandardBoard()
177177
... move, score = engine.get_best_move(board, with_evaluation=True)
178178
... print(f"Best move: {move}, Score: {score}")
@@ -182,7 +182,7 @@ Best move: 32-28, Score: 0.15
182182
The `HubEngine` class:
183183
- Manages the external engine subprocess lifecycle
184184
- Auto-detects variant from board type (Standard, Frisian)
185-
- Supports time-based or depth-based search
185+
- Supports time-based (`time_limit`) or depth-based (`depth_limit`) search
186186
- Works as a drop-in replacement for `AlphaBetaEngine`
187187

188188
## Web UI
@@ -209,8 +209,8 @@ from draughts.server import Server
209209
board = get_board('standard')
210210

211211
# Create engines with different configurations
212-
white_engine = AlphaBetaEngine(depth=6)
213-
black_engine = AlphaBetaEngine(depth=4)
212+
white_engine = AlphaBetaEngine(depth_limit=6)
213+
black_engine = AlphaBetaEngine(depth_limit=4)
214214

215215
server = Server(
216216
board=board,
@@ -239,7 +239,7 @@ board = get_board('standard')
239239
server = Server(
240240
board=board,
241241
white_engine=MyEngine(),
242-
black_engine=AlphaBetaEngine(depth=5)
242+
black_engine=AlphaBetaEngine(depth_limit=5)
243243
)
244244
server.run()
245245
```

test/test_engine.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class TestAlphaBetaEngine:
1212
@pytest.fixture(autouse=True)
1313
def setup(self):
1414
self.board = get_board("standard")
15-
self.engine = AlphaBetaEngine(depth=3)
15+
self.engine = AlphaBetaEngine(depth_limit=3)
1616
yield
1717
del self.board
1818
del self.engine
@@ -98,7 +98,7 @@ def _snapshot(board: StandardBoard):
9898
@pytest.mark.parametrize("seed", list(seeded_range(15)))
9999
def test_engine_get_best_move_does_not_mutate_board(seed):
100100
board = standard_board_after_random_play(seed=seed, plies=30)
101-
engine = AlphaBetaEngine(depth=2)
101+
engine = AlphaBetaEngine(depth_limit=2)
102102

103103
if board.game_over:
104104
return
@@ -118,7 +118,7 @@ def test_engine_get_best_move_does_not_mutate_board(seed):
118118
@pytest.mark.parametrize("seed", list(seeded_range(15)))
119119
def test_engine_hash_is_stable_across_push_pop(seed):
120120
board = standard_board_after_random_play(seed=seed, plies=25)
121-
engine = AlphaBetaEngine(depth=1)
121+
engine = AlphaBetaEngine(depth_limit=1)
122122

123123
legal = list(board.legal_moves)
124124
if not legal:
@@ -134,7 +134,7 @@ def test_engine_hash_is_stable_across_push_pop(seed):
134134
@pytest.mark.parametrize("seed", list(seeded_range(10)))
135135
def test_engine_populates_transposition_table_for_root(seed):
136136
board = standard_board_after_random_play(seed=seed, plies=20)
137-
engine = AlphaBetaEngine(depth=2)
137+
engine = AlphaBetaEngine(depth_limit=2)
138138

139139
if board.game_over:
140140
return

test/test_server.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
from fastapi.testclient import TestClient
44

55
from draughts import get_board
6-
from draughts.engine import AlphaBetaEngine
6+
from draughts.engine import AlphaBetaEngine, Engine
77
from draughts.server.server import Server
88
from draughts.move import Move
99

1010

11-
class _SlowStaleEngine:
11+
class _SlowStaleEngine(Engine):
1212
"""Engine stub that always returns the same *initial* move after a delay.
1313
1414
This simulates autoplay overlap: a long search returns a move that was legal
@@ -22,8 +22,10 @@ def __init__(self, stale_move, delay_s: float = 0.15):
2222
self._delay_s = delay_s
2323
self._time = time
2424

25-
def get_best_move(self, _board):
25+
def get_best_move(self, _board, with_evaluation=False):
2626
self._time.sleep(self._delay_s)
27+
if with_evaluation:
28+
return self._stale_move, 0.0
2729
return self._stale_move
2830

2931

@@ -122,21 +124,21 @@ def test_set_depth_clamps_and_updates_engine():
122124
try:
123125
Server.APP = _new_test_app()
124126
board = get_board("standard")
125-
engine = AlphaBetaEngine(depth=6)
127+
engine = AlphaBetaEngine(depth_limit=6)
126128
server = Server(
127-
board=board, get_best_move_method=engine.get_best_move, engine=engine
129+
board=board, white_engine=engine, black_engine=engine
128130
)
129131
client = TestClient(server.APP)
130132

131133
r = client.get("/set_depth/0")
132134
assert r.status_code == 200
133135
assert r.json()["depth"] == 1
134-
assert engine.depth == 1
136+
assert engine.depth_limit == 1
135137

136138
r = client.get("/set_depth/999")
137139
assert r.status_code == 200
138140
assert r.json()["depth"] == 10
139-
assert engine.depth == 10
141+
assert engine.depth_limit == 10
140142
finally:
141143
Server.APP = old_app
142144

@@ -158,7 +160,7 @@ def test_overlapping_best_move_requests_do_not_corrupt_board():
158160
stale_move = list(board.legal_moves)[0]
159161

160162
engine = _SlowStaleEngine(stale_move=stale_move, delay_s=0.15)
161-
server = Server(board=board, get_best_move_method=engine.get_best_move, engine=engine)
163+
server = Server(board=board, white_engine=engine, black_engine=engine)
162164
client = TestClient(server.APP)
163165

164166
barrier = threading.Barrier(3)
@@ -200,11 +202,14 @@ def test_best_move_falls_back_if_engine_returns_illegal_move():
200202
Server.APP = _new_test_app()
201203
board = get_board("standard")
202204

203-
def bad_engine(_board):
204-
# Definitely illegal: no-op move
205-
return Move([0, 0])
205+
class BadEngine(Engine):
206+
def get_best_move(self, _board, with_evaluation=False):
207+
# Definitely illegal: no-op move
208+
move = Move([0, 0])
209+
return (move, 0.0) if with_evaluation else move
206210

207-
server = Server(board=board, get_best_move_method=bad_engine)
211+
bad_engine = BadEngine()
212+
server = Server(board=board, white_engine=bad_engine, black_engine=bad_engine)
208213
client = TestClient(server.APP)
209214

210215
r = client.get("/best_move")

tools/depth_benchmark.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
def benchmark_depth(depth: int) -> dict:
3232
"""Benchmark engine at given depth, return stats."""
33-
engine = AlphaBetaEngine(depth=depth)
33+
engine = AlphaBetaEngine(depth_limit=depth)
3434

3535
total_time = 0.0
3636
total_moves = 0

0 commit comments

Comments
 (0)