|
5 | 5 | import numpy as np |
6 | 6 |
|
7 | 7 | from draughts.boards.base import BaseBoard |
8 | | -from draughts.models import Color, Figure |
| 8 | +from draughts.models import Color, Figure, EMPTY, MAN, KING |
9 | 9 | from draughts.move import Move |
10 | 10 |
|
11 | 11 | # fmt: off |
@@ -46,58 +46,77 @@ def is_draw(self) -> bool: |
46 | 46 |
|
47 | 47 | @property |
48 | 48 | def legal_moves(self) -> Generator[Move, None, None]: |
49 | | - if self.turn == Color.BLACK: |
50 | | - squares_list = np.transpose(np.nonzero(self._pos > 0)) |
51 | | - else: |
52 | | - squares_list = np.transpose(np.nonzero(self._pos < 0)) |
53 | | - for square in squares_list.flatten(): |
| 49 | + # Use flatnonzero for faster piece lookup |
| 50 | + turn_val = self.turn.value |
| 51 | + squares_list = np.flatnonzero(self._pos * turn_val > 0) |
| 52 | + for square in squares_list: |
54 | 53 | moves = self._legal_moves_from(square) |
55 | 54 | for move in moves: |
56 | 55 | yield move |
57 | 56 |
|
58 | 57 | def _legal_moves_from( |
59 | 58 | self, square: int, is_after_capture=False |
60 | | - ) -> Generator[Move, None, None]: |
61 | | - row = self.ROW_IDX[square] |
| 59 | + ) -> list[Move]: |
| 60 | + """ |
| 61 | + Generate legal moves using pre-computed attack tables. |
| 62 | + American checkers: men can only capture forward, kings can capture in all directions. |
| 63 | + """ |
62 | 64 | moves = [] |
63 | | - odd = bool(row % 2 != 0 and self.turn == Color.BLACK) or ( |
64 | | - row % 2 == 0 and self.turn == Color.WHITE |
65 | | - ) |
66 | | - is_king = bool(self[square] == self.turn.value * Figure.KING) |
67 | | - # is_king = False # DEBUG |
68 | | - for mv_offset, cap_offset, dir in [ |
69 | | - (4 - odd, 7, self.turn.value), |
70 | | - (5 - odd, 9, self.turn.value), |
71 | | - ] + is_king * [ |
72 | | - (4 - (not odd), 7, -self.turn.value), |
73 | | - (5 - (not odd), 9, -self.turn.value), |
74 | | - ]: |
75 | | - move_sq = square + mv_offset * (dir) |
76 | | - capture_sq = square + cap_offset * (dir) |
77 | | - |
78 | | - if ( |
79 | | - 0 <= move_sq < len(self._pos) |
80 | | - and row + 1 * (dir) == self.ROW_IDX[move_sq] |
81 | | - and self[move_sq] == 0 |
82 | | - and not is_after_capture |
83 | | - ): |
84 | | - moves.append(Move([square, move_sq])) |
85 | | - elif ( |
86 | | - 0 <= capture_sq < len(self._pos) |
87 | | - and row + 2 * (dir) == self.ROW_IDX[capture_sq] |
88 | | - and self[capture_sq] == 0 |
89 | | - and self[move_sq] * self.turn.value < 0 |
90 | | - ): |
91 | | - move = Move( |
92 | | - [square, capture_sq], |
93 | | - captured_list=[move_sq], |
94 | | - captured_entities=[self[move_sq]], |
95 | | - ) |
96 | | - moves.append(move) |
97 | | - self.push(move, False) |
98 | | - moves += [move + m for m in self._legal_moves_from(capture_sq, True)] |
99 | | - self.pop(False) |
100 | | - |
| 65 | + pos = self._pos |
| 66 | + turn_val = self.turn.value |
| 67 | + piece_val = abs(pos[square]) |
| 68 | + is_king = piece_val == KING |
| 69 | + |
| 70 | + # Use pre-computed attack tables |
| 71 | + attack_table = self.WHITE_MAN_ATTACKS if turn_val < 0 else self.BLACK_MAN_ATTACKS |
| 72 | + |
| 73 | + if is_king: |
| 74 | + # Kings can move and capture in all directions using KING_DIAGONALS |
| 75 | + for direction in self.KING_DIAGONALS[square]: |
| 76 | + if len(direction) >= 1: |
| 77 | + target = direction[0] |
| 78 | + # Regular move (not after capture) |
| 79 | + if pos[target] == EMPTY and not is_after_capture: |
| 80 | + moves.append(Move([square, target])) |
| 81 | + # Capture |
| 82 | + if len(direction) >= 2: |
| 83 | + jump_over = direction[0] |
| 84 | + land_on = direction[1] |
| 85 | + if pos[jump_over] * turn_val < 0 and pos[land_on] == EMPTY: |
| 86 | + move = Move( |
| 87 | + [square, land_on], |
| 88 | + captured_list=[jump_over], |
| 89 | + captured_entities=[pos[jump_over]], |
| 90 | + ) |
| 91 | + moves.append(move) |
| 92 | + self.push(move, False) |
| 93 | + moves += [move + m for m in self._legal_moves_from(land_on, True)] |
| 94 | + self.pop(False) |
| 95 | + else: |
| 96 | + # Men use attack table (forward moves only, but can capture forward) |
| 97 | + for entry in attack_table[square]: |
| 98 | + target = entry.target |
| 99 | + jump_over = entry.jump_over |
| 100 | + land_on = entry.land_on |
| 101 | + |
| 102 | + # Regular move (only forward, not after capture) |
| 103 | + if target >= 0 and not is_after_capture: |
| 104 | + if pos[target] == EMPTY: |
| 105 | + moves.append(Move([square, target])) |
| 106 | + |
| 107 | + # Capture move (men can only capture forward in American checkers) |
| 108 | + if jump_over >= 0 and land_on >= 0: |
| 109 | + if pos[jump_over] * turn_val < 0 and pos[land_on] == EMPTY: |
| 110 | + move = Move( |
| 111 | + [square, land_on], |
| 112 | + captured_list=[jump_over], |
| 113 | + captured_entities=[pos[jump_over]], |
| 114 | + ) |
| 115 | + moves.append(move) |
| 116 | + self.push(move, False) |
| 117 | + moves += [move + m for m in self._legal_moves_from(land_on, True)] |
| 118 | + self.pop(False) |
| 119 | + |
101 | 120 | return moves |
102 | 121 |
|
103 | 122 |
|
|
0 commit comments