Optimize: Cache player occupied squares for faster corner validation
This commit is contained in:
@@ -16,11 +16,15 @@ class Board:
|
|||||||
def __init__(self, size: int):
|
def __init__(self, size: int):
|
||||||
self.size = size
|
self.size = size
|
||||||
self.grid = np.zeros((size, size), dtype=np.int8)
|
self.grid = np.zeros((size, size), dtype=np.int8)
|
||||||
|
self._occupied_cache: dict[int, set[tuple[int, int]]] = {} # player_idx -> occupied squares
|
||||||
|
|
||||||
def place(self, player_idx: int, squares: list[tuple[int, int]]) -> None:
|
def place(self, player_idx: int, squares: list[tuple[int, int]]) -> None:
|
||||||
"""Place a player's piece on the board."""
|
"""Place a player's piece on the board."""
|
||||||
for x, y in squares:
|
for x, y in squares:
|
||||||
self.grid[y, x] = player_idx
|
self.grid[y, x] = player_idx
|
||||||
|
# Update occupied cache incrementally instead of clearing
|
||||||
|
if player_idx in self._occupied_cache:
|
||||||
|
self._occupied_cache[player_idx].update(squares)
|
||||||
|
|
||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
"""Reset the board to empty."""
|
"""Reset the board to empty."""
|
||||||
@@ -50,6 +54,12 @@ class Board:
|
|||||||
ys, xs = np.where(self.grid == player_idx)
|
ys, xs = np.where(self.grid == player_idx)
|
||||||
return [(int(x), int(y)) for x, y in zip(xs, ys, strict=True)]
|
return [(int(x), int(y)) for x, y in zip(xs, ys, strict=True)]
|
||||||
|
|
||||||
|
def get_player_occupied(self, player_idx: int) -> set[tuple[int, int]]:
|
||||||
|
"""Get all squares occupied by a player as a set for fast lookup."""
|
||||||
|
if player_idx not in self._occupied_cache:
|
||||||
|
self._occupied_cache[player_idx] = set(self.get_player_squares(player_idx))
|
||||||
|
return self._occupied_cache[player_idx]
|
||||||
|
|
||||||
def get_player_corners(self, player_idx: int) -> set[tuple[int, int]]:
|
def get_player_corners(self, player_idx: int) -> set[tuple[int, int]]:
|
||||||
"""Get all corner cells adjacent to a player's pieces.
|
"""Get all corner cells adjacent to a player's pieces.
|
||||||
|
|
||||||
|
|||||||
+25
-28
@@ -206,12 +206,12 @@ class BlokusGame:
|
|||||||
# Rule 4: Corner rule (must touch same-color at a corner)
|
# Rule 4: Corner rule (must touch same-color at a corner)
|
||||||
if player.has_started:
|
if player.has_started:
|
||||||
placed_corners = self._get_placed_corners(move)
|
placed_corners = self._get_placed_corners(move)
|
||||||
touches_corner = False
|
# Use cached occupied squares from board for fast lookup
|
||||||
for cx, cy in placed_corners:
|
player_occupied = self.board.get_player_occupied(player_idx)
|
||||||
if self.board.in_bounds(cx, cy):
|
touches_corner = any(
|
||||||
if self.board.get_cell(cx, cy) == player_board_idx:
|
self.board.in_bounds(cx, cy) and (cx, cy) in player_occupied
|
||||||
touches_corner = True
|
for cx, cy in placed_corners
|
||||||
break
|
)
|
||||||
if not touches_corner:
|
if not touches_corner:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -239,48 +239,45 @@ class BlokusGame:
|
|||||||
|
|
||||||
player_board_idx = player_idx + self.PLAYER_OFFSET
|
player_board_idx = player_idx + self.PLAYER_OFFSET
|
||||||
|
|
||||||
|
# Pre-compute player's occupied squares ONCE (cached)
|
||||||
|
player_occupied = self.board.get_player_occupied(player_idx)
|
||||||
|
|
||||||
for piece_id in available_piece_ids:
|
for piece_id in available_piece_ids:
|
||||||
for action_idx in self._actions_by_piece[piece_id]:
|
for action_idx in self._actions_by_piece[piece_id]:
|
||||||
move = self._action_moves[action_idx]
|
move = self._action_moves[action_idx]
|
||||||
placed_corners = self._get_placed_corners(move)
|
placed_corners = self._get_placed_corners(move)
|
||||||
|
|
||||||
touches_corner = False
|
# Early exit: check corner touch using cached occupied squares
|
||||||
for cx, cy in placed_corners:
|
touches_corner = any(
|
||||||
if self.board.in_bounds(cx, cy):
|
self.board.in_bounds(cx, cy) and (cx, cy) in player_occupied
|
||||||
if self.board.get_cell(cx, cy) == player_board_idx:
|
for cx, cy in placed_corners
|
||||||
touches_corner = True
|
)
|
||||||
break
|
|
||||||
if not touches_corner:
|
if not touches_corner:
|
||||||
continue
|
continue # Skip expensive overlap/edge checks
|
||||||
|
|
||||||
placed_squares = self._get_placed_squares(move)
|
placed_squares = self._get_placed_squares(move)
|
||||||
|
|
||||||
valid = True
|
# Check bounds
|
||||||
for x, y in placed_squares:
|
if not all(self.board.in_bounds(x, y) for x, y in placed_squares):
|
||||||
if not self.board.in_bounds(x, y):
|
|
||||||
valid = False
|
|
||||||
break
|
|
||||||
if not valid:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Check overlap
|
||||||
if self.board.has_overlap(placed_squares):
|
if self.board.has_overlap(placed_squares):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Check edge adjacency (no same-color edges)
|
||||||
|
edge_invalid = False
|
||||||
for x, y in placed_squares:
|
for x, y in placed_squares:
|
||||||
edge_invalid = False
|
|
||||||
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
||||||
nx, ny = x + dx, y + dy
|
nx, ny = x + dx, y + dy
|
||||||
if self.board.in_bounds(nx, ny):
|
if self.board.in_bounds(nx, ny) and self.board.get_cell(nx, ny) == player_board_idx:
|
||||||
if self.board.get_cell(nx, ny) == player_board_idx:
|
edge_invalid = True
|
||||||
edge_invalid = True
|
break
|
||||||
break
|
|
||||||
if edge_invalid:
|
if edge_invalid:
|
||||||
valid = False
|
|
||||||
break
|
break
|
||||||
if not valid:
|
|
||||||
continue
|
|
||||||
|
|
||||||
mask[action_idx] = True
|
if not edge_invalid:
|
||||||
|
mask[action_idx] = True
|
||||||
|
|
||||||
return mask
|
return mask
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user