Skip to content

Commit e6338b4

Browse files
author
miskibin
committed
feat: Update king movement logic to prevent crossing captured squares and enhance test coverage
1 parent 020e228 commit e6338b4

File tree

3 files changed

+62
-4
lines changed

3 files changed

+62
-4
lines changed

draughts/boards/standard.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,28 +169,51 @@ def _get_man_legal_moves_from(
169169
return moves
170170

171171
def _get_king_legal_moves_from(
172-
self, square: int, is_capture_mandatory: bool
172+
self, square: int, is_capture_mandatory: bool, forbidden_squares: set[int] | None = None
173173
) -> list[Move]:
174+
"""
175+
Generate legal moves for a king from a given square.
176+
177+
Args:
178+
square: The current square of the king
179+
is_capture_mandatory: Whether we're in a capture sequence
180+
forbidden_squares: Squares of previously captured pieces that cannot be crossed
181+
"""
182+
if forbidden_squares is None:
183+
forbidden_squares = set()
184+
174185
moves = []
175186
pos = self._pos # Local reference for faster access
176187
turn_val = self.turn.value
188+
max_len = 0 # Track max length across ALL directions
189+
177190
for direction in self.DIAGONAL_SHORT_MOVES[square]:
178191
dir_len = len(direction)
179192
for idx, target in enumerate(direction):
193+
# Check if path crosses a forbidden square (previously captured piece)
194+
if target in forbidden_squares:
195+
break
196+
180197
target_val = pos[target]
181198
if (
182199
dir_len > idx + 1
183200
and target_val * turn_val < 0
184201
and pos[direction[idx + 1]] == EMPTY
185202
):
186203
i = idx + 1
187-
max_len = 0 # Track max length locally
188204
while i < dir_len and pos[direction[i]] == EMPTY:
205+
# Check landing square is not forbidden
206+
if direction[i] in forbidden_squares:
207+
i += 1
208+
continue
209+
189210
move = Move(
190211
[square, direction[i]], [target], [target_val]
191212
)
192213
self.push(move, False)
193-
sub_moves = self._get_king_legal_moves_from(direction[i], True)
214+
# Add the captured square to forbidden set for subsequent captures
215+
new_forbidden = forbidden_squares | {target}
216+
sub_moves = self._get_king_legal_moves_from(direction[i], True, new_forbidden)
194217
self.pop(False)
195218

196219
if sub_moves:

test/test_standard_board.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ def test_legal_moves(self):
4848
('[FEN "W:B:WK2,28,31,44:B20,K50"]', ["50x36"], []),
4949
# King cannot jump adjacent pieces (28 and 33 have no empty square between)
5050
('[FEN "W:B:W7,28,31,33:B20,30,K50"]', [], ["50x50"]),
51+
# King cannot hop over same piece twice during capture sequence
52+
# Black king on 49 should capture 38, 28, 24 (path: 49->32->19->30/35)
53+
# It should NOT capture 41 because to reach it after capturing 38,
54+
# and then continue to 28, would require crossing 41's square twice
55+
('[FEN "W:B:W6,24,28,38,41:B13,K49"]', ["49x30", "49x35"], []),
5156
],
5257
)
5358
def test_king_capture_edge_cases(self, fen, valid_moves, invalid_moves):
@@ -62,6 +67,36 @@ def test_king_capture_edge_cases(self, fen, valid_moves, invalid_moves):
6267
for move in invalid_moves:
6368
assert move not in move_strs, f"Invalid move {move} found in: {move_strs}"
6469

70+
def test_king_cannot_cross_captured_square(self):
71+
"""
72+
Test that king cannot cross a square where a piece was already captured.
73+
74+
FEN "W:B:W6,24,28,38,41:B13,K49" - Black king on 49
75+
Valid path: 49 -> capture 38 -> land on 32 -> capture 28 -> land on 19 -> capture 24 -> land on 30/35
76+
Invalid: capturing 41 then trying to capture 28 would cross the 41 square again
77+
"""
78+
import numpy as np
79+
80+
board = Board.from_fen('[FEN "W:B:W6,24,28,38,41:B13,K49"]')
81+
legal_moves = board.legal_moves
82+
83+
# Should have exactly 2 moves (49x30 and 49x35)
84+
assert len(legal_moves) == 2, f"Expected 2 moves, got {len(legal_moves)}: {[str(m) for m in legal_moves]}"
85+
86+
# Both moves should capture exactly 3 pieces: 38, 28, 24
87+
for move in legal_moves:
88+
captured_1idx = sorted([c + 1 for c in move.captured_list])
89+
assert captured_1idx == [24, 28, 38], f"Expected captures [24, 28, 38], got {captured_1idx}"
90+
91+
# Piece on 41 should NOT be captured (it's on a different branch)
92+
test_board = Board.from_fen('[FEN "W:B:W6,24,28,38,41:B13,K49"]')
93+
test_board.push(legal_moves[0])
94+
95+
# Square 41 (0-indexed: 40) should still have a white piece
96+
assert test_board.position[40] == -1, "White piece on square 41 was incorrectly captured"
97+
# Square 6 (0-indexed: 5) should still have a white piece
98+
assert test_board.position[5] == -1, "White piece on square 6 was incorrectly captured"
99+
65100
def test_games_from_pdns(self):
66101
import re
67102
with open(self.random_pdns, "r") as f:

tools/compare_versions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
# === Configuration ===
2626
WARMUP_ROUNDS = 5
2727
BENCHMARK_ROUNDS = 10
28-
BENCHMARK_ITERATIONS = 1 # Number of times to run the legal moves benchmark
28+
BENCHMARK_ITERATIONS = 10 # Number of times to run the legal moves benchmark
2929
ENGINE_DEPTH = 4
3030

3131
# === Paths ===

0 commit comments

Comments
 (0)