Skip to content

Commit 8951885

Browse files
author
miskibin
committed
feat: Add PDN copy and load buttons to the UI, update version to 1.5.0
- Added buttons for copying and loading PDN in the index.html template. - Updated version number in pyproject.toml to 1.5.0. - Introduced random PDNs for testing in a new JSON file. - Enhanced test coverage for engine evaluation consistency. - Added edge case tests for king captures in standard board tests. - Implemented a script to fetch random PDNs from Lidraughts API.
1 parent 2653690 commit 8951885

File tree

12 files changed

+257
-26
lines changed

12 files changed

+257
-26
lines changed

.github/workflows/python-app.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
- name: Set up Python
1919
uses: actions/setup-python@v5
2020
with:
21-
python-version: "3.11"
21+
python-version: "3.12"
2222
- name: Install dependencies
2323
run: |
2424
python -m pip install --upgrade pip
@@ -34,7 +34,7 @@ jobs:
3434
- name: Set up Python
3535
uses: actions/setup-python@v5
3636
with:
37-
python-version: "3.11"
37+
python-version: "3.12"
3838
- name: Install dependencies
3939
run: |
4040
python -m pip install --upgrade pip

draughts/boards/base.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -395,11 +395,43 @@ def pdn(self) -> str:
395395
history.append([(idx // 2) + 1, str(move)])
396396
else:
397397
history[-1].append(str(move))
398-
return (
399-
data
400-
+ " ".join(f"{h[0]}. {' '.join(str(x) for x in h[1:])}" for h in history)
401-
+ self.result * (len(self.result) - 1)
402-
)
398+
moves_str = " ".join(f"{h[0]}. {' '.join(str(x) for x in h[1:])}" for h in history)
399+
result_str = "" if self.result == "-" else f" {self.result}"
400+
return data + moves_str + result_str
401+
402+
403+
@classmethod
404+
def from_pdn(cls: Type[BaseBoard], pdn: str) -> BaseBoard:
405+
"""
406+
Creates a board from a PDN string.
407+
"""
408+
logger.debug(f"Initializing board from PDN:\n{pdn}")
409+
re_gametype = re.compile(r'\[GameType\s*"(\d+)"\]')
410+
# Match move numbers followed by actual moves (must contain - or x)
411+
re_moves = re.compile(r"(\d+)\.\s*(\d+[-x]\d+(?:[-x]\d+)*)(?:\s+(\d+[-x]\d+(?:[-x]\d+)*))?")
412+
# Game results that should not be parsed as moves
413+
game_results = {"2-0", "0-2", "1-1", "1-0", "0-1", "*"}
414+
gametype_match = re_gametype.search(pdn)
415+
if not gametype_match:
416+
raise ValueError("Invalid PDN: missing GameType")
417+
gametype = int(gametype_match.group(1))
418+
if gametype != cls.GAME_TYPE:
419+
raise ValueError(
420+
f"Invalid PDN: expected GameType {cls.GAME_TYPE}, got {gametype}"
421+
)
422+
board = cls()
423+
moves_matches = re_moves.findall(pdn)
424+
for match in moves_matches:
425+
white_move_str = match[1].strip()
426+
if white_move_str in game_results:
427+
break
428+
board.push_uci(white_move_str)
429+
black_move_str = match[2].strip() if match[2] else None
430+
if black_move_str:
431+
if black_move_str in game_results:
432+
break
433+
board.push_uci(black_move_str)
434+
return board
403435

404436
@staticmethod
405437
def is_capture(move: Move) -> bool:

draughts/boards/frisian.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,14 @@ def _legal_moves_from(self, square: int | np.intp, is_capture_mandatory=False) -
127127
return moves
128128

129129
def _get_man_legal_moves_from(
130-
self, square: int, is_captrue_mandatory: bool
130+
self, square: int | np.intp, is_captrue_mandatory: bool
131131
) -> list[Move]:
132132
# legal_moves = self.DIAGONAL_SHORT_MOVES +
133133

134134
raise NotImplementedError
135135

136136
def _get_king_legal_moves_from(
137-
self, square: int, is_captrue_mandatory: bool
137+
self, square: int | np.intp, is_captrue_mandatory: bool
138138
) -> list[Move]:
139139
raise NotImplementedError
140140

draughts/boards/standard.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,13 @@ def _get_king_legal_moves_from(
183183
moves = [m for m in moves if len(m) == max_len]
184184
i += 1
185185
break
186-
if (
187-
self._pos[target] == Figure.EMPTY.value and not is_capture_mandatory
188-
): # casual move
189-
moves.append(Move([square, target]))
186+
if self._pos[target] == Figure.EMPTY.value:
187+
if not is_capture_mandatory:
188+
# casual move - only when not in capture chain
189+
moves.append(Move([square, target]))
190+
# Continue searching for capturable pieces further along diagonal
190191
else:
192+
# Any piece (own or enemy without landing space) blocks the path
191193
break
192194
return moves
193195

@@ -203,10 +205,17 @@ def _legal_moves_from(self, square: int, is_capture_mandatory=False) -> list[Mov
203205

204206

205207
if __name__ == "__main__":
206-
board = Board()
207-
for i in range(10):
208-
# random move
209-
move = np.random.choice(list(board.legal_moves))
210-
board.push(move)
211-
212-
print(board.pdn)
208+
# board = Board()
209+
# for i in range(10):
210+
# # random move
211+
# move = np.random.choice(list(board.legal_moves))
212+
# board.push(move)
213+
# pdn = """
214+
# [GameType \"20\"]\n1. 33-28 18-23 2. 39-33 13-18 3. 44-39 18-22 4. 31-27 22x31 5. 36x27 8-13 6. 50-44 2-8 7. 41-36 20-24 8. 34-29 23x34 9. 40x20 14x25 10. 46-41 10-14 11. 44-40 5-10 12. 39-34 17-21 13. 43-39 21-26 14. 49-43 12-17 15. 27-21 16x27 16. 32x12 7x18 17. 37-32 11-16 18. 41-37 1-7 19. 37-31 26x37 20. 42x31 7-11 21. 31-27 14-20 22. 47-42 10-14 23. 42-37 20-24 24. 27-21 16x27 25. 32x21 8-12 26. 21-16 3-8 27. 16x7 12x1 28. 37-32 8-12 29. 28-23 19x37 30. 34-30 25x34 31. 39x17 14-19 32. 43-39 1-7 33. 38-32 37x28 34. 33x24 9-13 35. 40-34 4-9 36. 34-30 9-14 37. 30-25 13-18 38. 17-12 18-22 39. 12x1 22-27 40. 48-42 6-11 41. 1-23 11-17 42. 23x5 17-21 43. 5-37 21-26 44. 39-34 15-20 45. 24x15 26-31 46. 37x26 27-32 47. 42-37 32x41 48. 36x47 2-0
215+
# """
216+
# board = Board.from_pdn(pdn)
217+
218+
# print(board.pdn)
219+
board = Board.from_fen('[FEN "W:B:WK2,28,31,44:B20,K50"])')
220+
print(board)
221+
print(board.legal_moves)

draughts/server/server.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,21 @@ def __init__(
5151
"/move/{source}/{target}", self.move, methods=["POST"]
5252
)
5353
self.router.add_api_route("/pop", self.pop, methods=["GET"])
54+
self.router.add_api_route("/pdn", self.get_pdn, methods=["GET"])
55+
self.router.add_api_route("/load_pdn", self.load_pdn, methods=["POST"])
5456
self.APP.include_router(self.router)
5557

5658
def get_fen(self):
5759
return {"fen": self.board.fen}
5860

61+
def get_pdn(self):
62+
return {"pdn": self.board.pdn}
63+
64+
async def load_pdn(self, request: Request) -> PositionResponse:
65+
data = await request.json()
66+
self.board = type(self.board).from_pdn(data["pdn"])
67+
return self.position_json
68+
5969
def set_board(self, request: Request, board_type: Literal["standard", "american"]):
6070
if board_type == "standard":
6171
from draughts import StandardBoard

draughts/server/static/js/script.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
// Piece colors
66
const COLORS = {
7-
1: '#f0f0f0', '-1': '#1a1a1a',
8-
2: '#f0f0f0', '-2': '#1a1a1a'
7+
1: '#1a1a1a', '-1': '#f0f0f0',
8+
2: '#1a1a1a', '-2': '#f0f0f0'
99
};
1010

1111
// Game state
@@ -46,8 +46,8 @@ function updateBoard() {
4646
if (piece !== 0) {
4747
$tile.append(`<div class="piece" style="background:${COLORS[piece]}"></div>`);
4848
if (Math.abs(piece) > 1) {
49-
const isBlack = piece < 0;
50-
$tile.append(`<img src="${crownIcon}" class="crown ${isBlack ? 'crown-light' : ''}" alt="K">`);
49+
const isWhite = piece > 0;
50+
$tile.append(`<img src="${crownIcon}" class="crown ${isWhite ? 'crown-light' : ''}" alt="K">`);
5151
}
5252
}
5353
});
@@ -149,6 +149,23 @@ async function copyFen() {
149149
notify('Copied', data.fen, 'success');
150150
}
151151

152+
async function copyPdn() {
153+
const data = await api.get('/pdn');
154+
navigator.clipboard.writeText(data.pdn);
155+
notify('Copied', 'PDN copied to clipboard', 'success');
156+
}
157+
158+
async function loadPdn() {
159+
const pdn = prompt('Paste PDN:');
160+
if (!pdn) return;
161+
try {
162+
const data = await $.ajax({ url: '/load_pdn', method: 'POST', contentType: 'application/json', data: JSON.stringify({ pdn }) });
163+
board = data.position; history = data.history; turn = data.turn;
164+
updateBoard();
165+
notify('Loaded', 'PDN loaded', 'success');
166+
} catch { notify('Error', 'Invalid PDN', 'error'); }
167+
}
168+
152169
function toggleAutoPlay() {
153170
const $btn = $('#autoPlay');
154171
if (autoPlayId) {
@@ -171,5 +188,7 @@ $(async () => {
171188
$('#popBtn').on('click', undo);
172189
$('#randomPos').on('click', randomPosition);
173190
$('#copyFen').on('click', copyFen);
191+
$('#copyPdn').on('click', copyPdn);
192+
$('#loadPdn').on('click', loadPdn);
174193
$('#autoPlay').on('click', toggleAutoPlay);
175194
});

draughts/server/templates/index.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@
4747
<i class="bi bi-clipboard"></i>
4848
Copy FEN
4949
</button>
50+
<button id="copyPdn" class="btn btn-game" type="button">
51+
<i class="bi bi-file-text"></i>
52+
Copy PDN
53+
</button>
54+
<button id="loadPdn" class="btn btn-game" type="button">
55+
<i class="bi bi-upload"></i>
56+
Load PDN
57+
</button>
5058
<button id="autoPlay" class="btn btn-game" type="button">
5159
<i class="bi bi-play-fill"></i>
5260
Auto Play

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "py-draughts"
7-
version = "1.4.2"
7+
version = "1.5.0"
88
description = "A draughts library with advanced (customizable) WEB UI move generation and validation, PDN parsing and writing. Supports multiple variants of game."
99
readme = "readme.md"
1010
license = {file = "LICENSE"}

0 commit comments

Comments
 (0)