Skip to content

Commit d20b9f0

Browse files
committed
feat: Enhance web UI and integrate external engine support
- Updated index.html for a more compact and user-friendly layout, including controls for board type and engine depth. - Added new example script `alphabeta_vs_scan.py` to demonstrate playing between AlphaBeta and Scan engines using the Hub protocol. - Expanded documentation in readme.md to include details on external engine support and usage examples. - Introduced comprehensive tests for Hub protocol implementation in `test/test_hub.py`, covering board position conversions, move notations, and engine interactions.
1 parent 3cdc37d commit d20b9f0

File tree

13 files changed

+2297
-431
lines changed

13 files changed

+2297
-431
lines changed

.gitignore

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
__pycache__/
33
*.py[cod]
44
*$py.class
5-
5+
*.exe
66
# C extensions
77
*.so
88
dist/
@@ -12,7 +12,10 @@ snapshots/
1212
.Python
1313
build/
1414
develop-eggs/
15-
15+
scan_engine/data/
16+
*.ini
17+
/data/
18+
scan_engine/
1619
# dist/ # we need this to publish
1720
downloads/
1821
eggs/

docs/source/engine.rst

Lines changed: 127 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -173,43 +173,151 @@ Advanced Example - Greedy Capture Engine::
173173
Running Custom Engines with the Server
174174
---------------------------------------
175175

176-
The ``Server`` class accepts any engine implementation through the ``engine`` parameter
177-
or via the ``get_best_move_method`` callback.
176+
The ``Server`` class accepts engine implementations for white and black sides.
177+
You can use the same engine for both sides, or pit different engines against each other.
178178

179-
Method 1: Using the ``engine`` Parameter (Recommended)::
179+
Single Engine for Both Sides::
180180

181181
from draughts import StandardBoard
182182
from draughts.engine import AlphaBetaEngine
183183
from draughts.server import Server
184-
import uvicorn
185184
186-
# Create board and engine
187185
board = StandardBoard()
188186
engine = AlphaBetaEngine(depth=6)
189187
190-
# Create server with engine
191-
server = Server(board=board, engine=engine)
192-
193-
# Run server on http://localhost:8000
194-
uvicorn.run(server.APP, host="127.0.0.1", port=8000)
188+
# Same engine plays for both colors
189+
server = Server(
190+
board=board,
191+
white_engine=engine,
192+
black_engine=engine
193+
)
194+
server.run(host="127.0.0.1", port=8000)
195195

196-
Method 2: Using ``get_best_move_method`` Callback::
196+
Engine vs Engine Match::
197197

198198
from draughts import StandardBoard
199+
from draughts.engine import AlphaBetaEngine
199200
from draughts.server import Server
200-
import uvicorn
201201
202202
board = StandardBoard()
203203
204-
# Define custom move selection function
205-
def my_move_strategy(board):
206-
# Your custom logic here
207-
legal_moves = list(board.legal_moves)
208-
return legal_moves[0] # Simple example
204+
# Different engines/configurations for each side
205+
white_engine = AlphaBetaEngine(depth=6)
206+
black_engine = AlphaBetaEngine(depth=4)
207+
208+
server = Server(
209+
board=board,
210+
white_engine=white_engine,
211+
black_engine=black_engine
212+
)
213+
server.run(host="127.0.0.1", port=8000)
214+
215+
See the :doc:`server` documentation for more details on the web interface.
216+
217+
218+
HubEngine - External Engine Support
219+
------------------------------------
220+
221+
The ``HubEngine`` class allows you to use external draughts engines that implement
222+
the Hub protocol (like `Scan <https://hjetten.home.xs4all.nl/scan/scan.html>`_).
223+
224+
.. autoclass:: draughts.hub.HubEngine
225+
:members: __init__, start, quit, get_best_move
226+
227+
Hub Protocol Overview
228+
~~~~~~~~~~~~~~~~~~~~~
229+
230+
The Hub protocol is a text-based communication protocol for draughts engines,
231+
similar to UCI for chess. It supports:
232+
233+
- **Position format**: 51-character string (side-to-move + 50 squares)
234+
- **Move format**: ``32-28`` (quiet moves) or ``28x19x23`` (captures with intermediate squares)
235+
- **Search control**: Time-per-move or fixed depth
236+
- **Variants**: Standard (international 10x10), Frisian, and others
237+
238+
Basic Usage
239+
~~~~~~~~~~~
240+
241+
Using HubEngine with a context manager (recommended)::
242+
243+
from draughts import HubEngine, StandardBoard
244+
245+
with HubEngine("path/to/scan.exe") as engine:
246+
board = StandardBoard()
247+
248+
# Get best move with 1 second think time
249+
move, score = engine.get_best_move(board, with_evaluation=True)
250+
print(f"Best move: {move}, Score: {score}")
251+
252+
Manual lifecycle management::
253+
254+
from draughts import HubEngine, StandardBoard
209255
210-
server = Server(board=board, get_best_move_method=my_move_strategy)
211-
uvicorn.run(server.APP, host="127.0.0.1", port=8000)
256+
engine = HubEngine("path/to/scan.exe")
257+
engine.start()
258+
259+
try:
260+
board = StandardBoard()
261+
move = engine.get_best_move(board)
262+
board.push(move)
263+
finally:
264+
engine.quit()
265+
266+
Search Options
267+
~~~~~~~~~~~~~~
212268

269+
Time-based search (default)::
270+
271+
# 2 seconds per move
272+
engine = HubEngine("scan.exe", time_per_move=2.0)
273+
274+
Fixed depth search::
275+
276+
# Search to depth 15
277+
engine = HubEngine("scan.exe", depth=15)
278+
279+
Engine vs HubEngine Match
280+
~~~~~~~~~~~~~~~~~~~~~~~~~
281+
282+
You can pit the built-in AlphaBetaEngine against an external Hub engine::
283+
284+
from draughts import StandardBoard, HubEngine
285+
from draughts.engine import AlphaBetaEngine
286+
287+
board = StandardBoard()
288+
alphabeta = AlphaBetaEngine(depth=6)
289+
290+
with HubEngine("scan.exe", time_per_move=1.0) as scan:
291+
while not board.is_over():
292+
if board.turn.value == 0: # White
293+
move = alphabeta.get_best_move(board)
294+
else: # Black
295+
move = scan.get_best_move(board)
296+
board.push(move)
297+
298+
print(f"Result: {board.result()}")
299+
300+
Variant Auto-Detection
301+
~~~~~~~~~~~~~~~~~~~~~~
302+
303+
HubEngine automatically detects the variant from the board type:
304+
305+
- ``StandardBoard`` → ``"normal"`` (International 10x10 draughts)
306+
- ``FrisianBoard`` → ``"frisian"`` (Frisian draughts with extra capture rules)
307+
308+
Logging
309+
~~~~~~~
310+
311+
HubEngine uses the ``loguru`` logger for debugging. Enable debug output::
312+
313+
from loguru import logger
314+
import sys
315+
316+
logger.add(sys.stderr, level="DEBUG")
317+
318+
with HubEngine("scan.exe") as engine:
319+
move = engine.get_best_move(board)
320+
# Will show all Hub protocol communication
213321

214322
Benchmarking the Engine
215323
-----------------------

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Contents
1111
base
1212
move
1313
engine
14+
server
1415
svg
1516
standard
1617
american

docs/source/server.rst

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
Server
2+
==========================================
3+
4+
The server module provides a web-based UI for playing draughts games. It supports
5+
human play, single engine play, and engine vs engine matches.
6+
7+
Server Class
8+
------------
9+
10+
.. autoclass:: draughts.server.Server
11+
:members: __init__, run
12+
13+
Basic Usage
14+
-----------
15+
16+
Starting a simple server::
17+
18+
from draughts import get_board
19+
from draughts.server import Server
20+
21+
board = get_board('standard')
22+
server = Server(board=board)
23+
server.run(host="127.0.0.1", port=8000)
24+
25+
Open http://localhost:8000 in your browser to access the web interface.
26+
27+
Playing with an Engine
28+
----------------------
29+
30+
To enable the "Engine Move" button in the UI, pass an engine to the server::
31+
32+
from draughts import get_board
33+
from draughts.engine import AlphaBetaEngine
34+
from draughts.server import Server
35+
36+
board = get_board('standard')
37+
engine = AlphaBetaEngine(depth=6)
38+
39+
# Engine plays for both sides when you click "Engine Move"
40+
server = Server(
41+
board=board,
42+
white_engine=engine,
43+
black_engine=engine
44+
)
45+
server.run()
46+
47+
Engine vs Engine Matches
48+
------------------------
49+
50+
The server supports running matches between two different engines. This is useful
51+
for testing and comparing engine implementations::
52+
53+
from draughts import get_board
54+
from draughts.engine import AlphaBetaEngine
55+
from draughts.server import Server
56+
57+
board = get_board('standard')
58+
59+
# Create two engines with different configurations
60+
white_engine = AlphaBetaEngine(depth=6)
61+
black_engine = AlphaBetaEngine(depth=4)
62+
63+
server = Server(
64+
board=board,
65+
white_engine=white_engine,
66+
black_engine=black_engine
67+
)
68+
server.run()
69+
70+
When two engines are configured:
71+
72+
- The UI displays the engine names for each side
73+
- Click "Engine Move" to make the current side's engine play
74+
- Use "Auto Play" to watch the engines play against each other automatically
75+
- The depth slider affects both engines
76+
77+
Custom Engine Integration
78+
-------------------------
79+
80+
You can integrate any custom engine that implements the ``Engine`` interface::
81+
82+
from draughts import get_board
83+
from draughts.engine import Engine
84+
from draughts.server import Server
85+
import random
86+
87+
class RandomEngine(Engine):
88+
"""A simple random move engine."""
89+
90+
def get_best_move(self, board, with_evaluation=False):
91+
move = random.choice(list(board.legal_moves))
92+
return (move, 0.0) if with_evaluation else move
93+
94+
class GreedyEngine(Engine):
95+
"""Prefers captures over quiet moves."""
96+
97+
def get_best_move(self, board, with_evaluation=False):
98+
moves = list(board.legal_moves)
99+
# Sort by capture length (most captures first)
100+
moves.sort(key=lambda m: len(m.captured_list), reverse=True)
101+
move = moves[0]
102+
return (move, 0.0) if with_evaluation else move
103+
104+
# Pit the two engines against each other
105+
board = get_board('standard')
106+
server = Server(
107+
board=board,
108+
white_engine=GreedyEngine(),
109+
black_engine=RandomEngine()
110+
)
111+
server.run()
112+
113+
Web Interface Features
114+
----------------------
115+
116+
The web UI provides the following controls:
117+
118+
**Board Selection**
119+
Switch between American (8x8) and Standard (10x10) board variants.
120+
121+
**Engine Settings**
122+
- **Depth slider**: Adjust search depth (1-10) for both engines
123+
124+
**Controls**
125+
- **Engine Move**: Make the current engine play its best move
126+
- **Auto Play**: Start/stop automatic engine-vs-engine play
127+
- **Undo**: Take back the last move
128+
129+
**Import/Export**
130+
- **Copy FEN**: Copy current position as FEN string
131+
- **Load FEN**: Load a position from FEN string
132+
- **Copy PDN**: Copy game notation as PDN
133+
- **Load PDN**: Load a game from PDN notation
134+
135+
**Info Panel**
136+
- Turn indicator showing whose move it is
137+
- Move history with clickable moves for navigation
138+
- Engine indicator (when engines are configured)
139+
140+
API Endpoints
141+
-------------
142+
143+
The server exposes the following REST API endpoints:
144+
145+
=================== ======= ===============================================
146+
Endpoint Method Description
147+
=================== ======= ===============================================
148+
``/`` GET Main game page
149+
``/position`` GET Get current board position
150+
``/legal_moves`` GET Get legal moves for current position
151+
``/fen`` GET Get FEN string
152+
``/pdn`` GET Get PDN string
153+
``/engine_info`` GET Get configured engine information
154+
``/move/{src}/{tgt}`` POST Make a move
155+
``/best_move`` GET Get and play engine's best move
156+
``/pop`` GET Undo last move
157+
``/goto/{ply}`` GET Jump to specific ply in history
158+
``/load_fen`` POST Load position from FEN
159+
``/load_pdn`` POST Load game from PDN
160+
``/set_depth/{n}`` GET Set engine search depth
161+
``/set_board/{type}`` GET Switch board type (standard/american)
162+
=================== ======= ===============================================
163+
164+
Running from Command Line
165+
-------------------------
166+
167+
Start the server directly from the command line::
168+
169+
python -m draughts.server.server
170+
171+
This starts a server with default settings (AlphaBetaEngine at depth 6).

draughts/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from draughts import svg
3838
from draughts.models import Color, Figure
3939
from draughts.move import Move
40+
from draughts.hub import HubEngine
4041

4142

4243
def get_board(

0 commit comments

Comments
 (0)