Optimize: Cache player occupied squares for faster corner validation

This commit is contained in:
mattlamb227@gmail.com
2026-08-12 23:39:52 -04:00
parent 10d8b0a376
commit 3b2ffa28d4
2 changed files with 35 additions and 28 deletions
+10
View File
@@ -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
View File
@@ -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
@@ -238,49 +238,46 @@ class BlokusGame:
return mask return mask
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