updated to use optimized cyython build
This commit is contained in:
@@ -0,0 +1,801 @@
|
||||
#!/usr/bin/env python3
|
||||
# Combined Cython implementation of Blokus game engine
|
||||
# Compile with: python setup_cython.py build_ext --inplace
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
cimport cython
|
||||
from cython cimport bint
|
||||
import numpy as np
|
||||
cimport numpy as cnp
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Board class (Cython-optimized)
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
cdef class Board:
|
||||
"""Cython-optimized Board class with static typing.
|
||||
|
||||
Key optimizations:
|
||||
- Static typing eliminates Python interpreter overhead
|
||||
- Direct C-level array access instead of Python method calls
|
||||
- Precomputed bounds checking
|
||||
"""
|
||||
|
||||
cdef public int size
|
||||
cdef public cnp.int8_t[:, :] grid
|
||||
cdef public dict _occupied_cache
|
||||
|
||||
def __init__(self, int size):
|
||||
self.size = size
|
||||
self.grid = np.zeros((size, size), dtype=np.int8)
|
||||
self._occupied_cache = {}
|
||||
|
||||
def place(self, int player_idx, list squares):
|
||||
"""Place a player's piece on the board."""
|
||||
cdef int x, y
|
||||
cdef cnp.int8_t[:, :] grid_view = self.grid
|
||||
cdef int size = self.size
|
||||
|
||||
for x, y in squares:
|
||||
if 0 <= x < size and 0 <= y < size:
|
||||
grid_view[y, x] = player_idx
|
||||
|
||||
# Update occupied cache incrementally
|
||||
if player_idx in self._occupied_cache:
|
||||
self._occupied_cache[player_idx].update(squares)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Reset the board to empty."""
|
||||
self.grid.fill(0)
|
||||
|
||||
def is_empty(self, int x, int y) -> bool:
|
||||
"""Check if a cell is empty (C-level fast)."""
|
||||
cdef cnp.int8_t[:, :] grid_view = self.grid
|
||||
cdef int size = self.size
|
||||
|
||||
if 0 <= x < size and 0 <= y < size:
|
||||
return grid_view[y, x] == 0
|
||||
return False
|
||||
|
||||
def in_bounds(self, int x, int y) -> bool:
|
||||
"""Check if coordinates are within the board (C-level fast)."""
|
||||
cdef int size = self.size
|
||||
return 0 <= x < size and 0 <= y < size
|
||||
|
||||
def has_overlap(self, list squares) -> bool:
|
||||
"""Check if any square is already occupied (C-level fast)."""
|
||||
cdef int x, y
|
||||
cdef cnp.int8_t[:, :] grid_view = self.grid
|
||||
cdef int size = self.size
|
||||
|
||||
for x, y in squares:
|
||||
if not (0 <= x < size and 0 <= y < size) or grid_view[y, x] != 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
cdef bint is_cell_empty(self, int x, int y):
|
||||
"""Check if a cell is empty (C-level fast, inlineable)."""
|
||||
cdef cnp.int8_t[:, :] grid_view = self.grid
|
||||
cdef int size = self.size
|
||||
|
||||
return 0 <= x < size and 0 <= y < size and grid_view[y, x] == 0
|
||||
|
||||
def get_cell(self, int x, int y) -> int:
|
||||
"""Get the player index at a cell (0 = empty)."""
|
||||
cdef cnp.int8_t[:, :] grid_view = self.grid
|
||||
cdef int size = self.size
|
||||
|
||||
if 0 <= x < size and 0 <= y < size:
|
||||
return grid_view[y, x]
|
||||
return 0
|
||||
|
||||
def get_player_squares(self, int player_idx) -> list:
|
||||
"""Get all squares occupied by a player."""
|
||||
cdef cnp.int8_t[:, :] grid_view = self.grid
|
||||
cdef int size = self.size
|
||||
cdef list result = []
|
||||
cdef int x, y
|
||||
|
||||
for y in range(size):
|
||||
for x in range(size):
|
||||
if grid_view[y, x] == player_idx:
|
||||
result.append((x, y))
|
||||
return result
|
||||
|
||||
def get_player_occupied(self, int player_idx) -> set:
|
||||
"""Get all squares occupied by a player as a set."""
|
||||
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, int player_idx) -> set:
|
||||
"""Get all corner cells adjacent to a player's pieces."""
|
||||
cdef cnp.int8_t[:, :] grid_view = self.grid
|
||||
cdef int size = self.size
|
||||
cdef set corners = set()
|
||||
cdef list player_squares = self.get_player_squares(player_idx)
|
||||
cdef int x, y, dx, dy, cx, cy
|
||||
|
||||
for x, y in player_squares:
|
||||
for dx, dy in [(-1, -1), (1, -1), (-1, 1), (1, 1)]:
|
||||
cx, cy = x + dx, y + dy
|
||||
if 0 <= cx < size and 0 <= cy < size and grid_view[cy, cx] == 0:
|
||||
corners.add((cx, cy))
|
||||
return corners
|
||||
|
||||
def get_occupied(self) -> set:
|
||||
"""Get all occupied cells."""
|
||||
cdef cnp.int8_t[:, :] grid_view = self.grid
|
||||
cdef int size = self.size
|
||||
cdef set result = set()
|
||||
cdef int x, y
|
||||
|
||||
for y in range(size):
|
||||
for x in range(size):
|
||||
if grid_view[y, x] != 0:
|
||||
result.add((x, y))
|
||||
return result
|
||||
|
||||
def is_full(self) -> bool:
|
||||
"""Check if the board is completely full."""
|
||||
cdef cnp.int8_t[:, :] grid_view = self.grid
|
||||
cdef int size = self.size
|
||||
cdef int x, y
|
||||
|
||||
for y in range(size):
|
||||
for x in range(size):
|
||||
if grid_view[y, x] == 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
def coverage(self) -> float:
|
||||
"""Get the fraction of the board that is occupied."""
|
||||
cdef cnp.int8_t[:, :] grid_view = self.grid
|
||||
cdef int size = self.size
|
||||
cdef int count = 0
|
||||
cdef int x, y
|
||||
|
||||
for y in range(size):
|
||||
for x in range(size):
|
||||
if grid_view[y, x] != 0:
|
||||
count += 1
|
||||
return count / (size * size)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Move and Piece classes
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
cdef class Move:
|
||||
"""A move in the game (piece, orientation, position)."""
|
||||
|
||||
cdef public int piece_id
|
||||
cdef public int orientation_id
|
||||
cdef public int x
|
||||
cdef public int y
|
||||
|
||||
def __init__(self, int piece_id, int orientation_id, int x, int y):
|
||||
self.piece_id = piece_id
|
||||
self.orientation_id = orientation_id
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __repr__(self):
|
||||
return f"Move(piece={self.piece_id}, orient={self.orientation_id}, pos=({self.x}, {self.y}))"
|
||||
|
||||
|
||||
cdef class PieceOrientation:
|
||||
"""A piece in a specific orientation (before placement on board)."""
|
||||
|
||||
cdef public int piece_id
|
||||
cdef public int orientation_id
|
||||
cdef public list squares
|
||||
cdef public list corners
|
||||
|
||||
def __init__(self, int piece_id, int orientation_id, list squares, list corners):
|
||||
self.piece_id = piece_id
|
||||
self.orientation_id = orientation_id
|
||||
self.squares = squares
|
||||
self.corners = corners
|
||||
|
||||
|
||||
cdef class Piece:
|
||||
"""A polyomino piece defined by a set of (x, y) coordinates."""
|
||||
|
||||
cdef public str name
|
||||
cdef public object squares # Use object to allow frozenset
|
||||
|
||||
def __init__(self, str name, object squares):
|
||||
self.name = name
|
||||
self.squares = squares
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return len(self.squares)
|
||||
|
||||
|
||||
cdef class PieceSet:
|
||||
"""A collection of pieces with pre-computed orientations."""
|
||||
|
||||
cdef public list pieces
|
||||
cdef public list piece_names
|
||||
cdef public dict piece_id_map
|
||||
cdef public list orientations
|
||||
cdef public int num_orientations
|
||||
|
||||
def __init__(self, list pieces_list):
|
||||
self.pieces = pieces_list
|
||||
self.piece_names = [p.name for p in pieces_list]
|
||||
self.piece_id_map = {name: i for i, name in enumerate(self.piece_names)}
|
||||
|
||||
# Pre-compute all orientations for each piece
|
||||
self.orientations = []
|
||||
for piece_id, piece in enumerate(pieces_list):
|
||||
orients = generate_orientations_cython(piece)
|
||||
for orient in orients:
|
||||
orient.piece_id = piece_id
|
||||
self.orientations.append(orients)
|
||||
|
||||
self.num_orientations = sum(len(o) for o in self.orientations)
|
||||
|
||||
@property
|
||||
def num_pieces(self) -> int:
|
||||
return len(self.pieces)
|
||||
|
||||
def get_orientations(self, int piece_id) -> list:
|
||||
return self.orientations[piece_id]
|
||||
|
||||
def get_piece(self, int piece_id) -> Piece:
|
||||
return self.pieces[piece_id]
|
||||
|
||||
def get_piece_id(self, str name) -> int:
|
||||
return self.piece_id_map[name]
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Pre-computed standard pieces (Cython-optimized)
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
cdef frozenset _I1_squares = frozenset([(0, 0)])
|
||||
cdef frozenset _I2_squares = frozenset([(0, 0), (0, 1)])
|
||||
cdef frozenset _I3_squares = frozenset([(0, 0), (0, 1), (0, 2)])
|
||||
cdef frozenset _V3_squares = frozenset([(0, 0), (1, 0), (0, 1)])
|
||||
cdef frozenset _I4_squares = frozenset([(0, 0), (0, 1), (0, 2), (0, 3)])
|
||||
cdef frozenset _L4_squares = frozenset([(0, 0), (0, 1), (0, 2), (1, 0)])
|
||||
cdef frozenset _T4_squares = frozenset([(0, 0), (1, 0), (2, 0), (1, 1)])
|
||||
cdef frozenset _S4_squares = frozenset([(0, 0), (1, 0), (1, 1), (2, 1)])
|
||||
cdef frozenset _O4_squares = frozenset([(0, 0), (1, 0), (0, 1), (1, 1)])
|
||||
cdef frozenset _I5_squares = frozenset([(0, 0), (0, 1), (0, 2), (0, 3), (0, 4)])
|
||||
cdef frozenset _L5_squares = frozenset([(0, 0), (0, 1), (0, 2), (0, 3), (1, 0)])
|
||||
cdef frozenset _Y5_squares = frozenset([(0, 0), (0, 1), (0, 2), (0, 3), (1, 1)])
|
||||
cdef frozenset _N5_squares = frozenset([(0, 0), (1, 0), (2, 0), (2, 1), (3, 1)])
|
||||
cdef frozenset _T5_squares = frozenset([(0, 0), (1, 0), (2, 0), (1, 1), (1, 2)])
|
||||
cdef frozenset _U5_squares = frozenset([(0, 0), (2, 0), (0, 1), (1, 1), (2, 1)])
|
||||
cdef frozenset _V5_squares = frozenset([(0, 0), (0, 1), (0, 2), (1, 0), (2, 0)])
|
||||
cdef frozenset _W5_squares = frozenset([(0, 0), (0, 1), (1, 0), (1, 1), (2, 0)])
|
||||
cdef frozenset _Z5_squares = frozenset([(0, 0), (1, 0), (1, 1), (1, 2), (2, 2)])
|
||||
cdef frozenset _F5_squares = frozenset([(0, 0), (1, 0), (1, 1), (2, 1), (1, 2)])
|
||||
cdef frozenset _X5_squares = frozenset([(0, 0), (1, 0), (2, 0), (1, 1), (1, -1)])
|
||||
cdef frozenset _P5_squares = frozenset([(0, 0), (1, 0), (0, 1), (1, 1), (0, 2)])
|
||||
|
||||
cdef Piece _I1 = Piece("I1", _I1_squares)
|
||||
cdef Piece _I2 = Piece("I2", _I2_squares)
|
||||
cdef Piece _I3 = Piece("I3", _I3_squares)
|
||||
cdef Piece _V3 = Piece("V3", _V3_squares)
|
||||
cdef Piece _I4 = Piece("I4", _I4_squares)
|
||||
cdef Piece _L4 = Piece("L4", _L4_squares)
|
||||
cdef Piece _T4 = Piece("T4", _T4_squares)
|
||||
cdef Piece _S4 = Piece("S4", _S4_squares)
|
||||
cdef Piece _O4 = Piece("O4", _O4_squares)
|
||||
cdef Piece _I5 = Piece("I5", _I5_squares)
|
||||
cdef Piece _L5 = Piece("L5", _L5_squares)
|
||||
cdef Piece _Y5 = Piece("Y5", _Y5_squares)
|
||||
cdef Piece _N5 = Piece("N5", _N5_squares)
|
||||
cdef Piece _T5 = Piece("T5", _T5_squares)
|
||||
cdef Piece _U5 = Piece("U5", _U5_squares)
|
||||
cdef Piece _V5 = Piece("V5", _V5_squares)
|
||||
cdef Piece _W5 = Piece("W5", _W5_squares)
|
||||
cdef Piece _Z5 = Piece("Z5", _Z5_squares)
|
||||
cdef Piece _F5 = Piece("F5", _F5_squares)
|
||||
cdef Piece _X5 = Piece("X5", _X5_squares)
|
||||
cdef Piece _P5 = Piece("P5", _P5_squares)
|
||||
|
||||
cdef list STANDARD_PIECES = [
|
||||
_I1, _I2, _I3, _V3, _I4, _L4, _T4, _S4, _O4,
|
||||
_I5, _L5, _Y5, _N5, _T5, _U5, _V5, _W5, _Z5, _F5, _X5, _P5
|
||||
]
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Orientation generation
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
cdef list generate_orientations_cython(Piece piece):
|
||||
"""Generate all unique orientations of a piece."""
|
||||
cdef set orientations_set = set()
|
||||
cdef list result = []
|
||||
cdef list squares = list(piece.squares)
|
||||
cdef list transformed
|
||||
cdef int i
|
||||
|
||||
for i in range(4): # 4 rotations (0, 90, 180, 270 degrees)
|
||||
transformed = rotate_squares_cython(squares, i)
|
||||
normalized = normalize_squares_cython(transformed)
|
||||
key = tuple(normalized)
|
||||
|
||||
if key not in orientations_set:
|
||||
orientations_set.add(key)
|
||||
corners = compute_corners_cython(normalized)
|
||||
result.append(PieceOrientation(0, i, normalized, corners))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
cdef list rotate_squares_cython(list squares, int rotations):
|
||||
"""Rotate squares by 90 * rotations degrees."""
|
||||
cdef list result = []
|
||||
cdef int x, y, i
|
||||
|
||||
for x, y in squares:
|
||||
for i in range(rotations):
|
||||
x, y = -y, x # 90 degree rotation
|
||||
result.append((x, y))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
cdef list normalize_squares_cython(list squares):
|
||||
"""Normalize squares to start at (0, 0)."""
|
||||
cdef int min_x = min(x for x, _ in squares)
|
||||
cdef int min_y = min(y for _, y in squares)
|
||||
|
||||
return [(x - min_x, y - min_y) for x, y in squares]
|
||||
|
||||
|
||||
cdef list compute_corners_cython(list squares):
|
||||
"""Compute corner squares (diagonal neighbors)."""
|
||||
cdef set corners = set()
|
||||
cdef int x, y, dx, dy
|
||||
|
||||
for x, y in squares:
|
||||
for dx, dy in [(-1, -1), (1, -1), (-1, 1), (1, 1)]:
|
||||
corners.add((x + dx, y + dy))
|
||||
|
||||
return list(corners)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Player state
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
cdef class PlayerState:
|
||||
"""State for a single player."""
|
||||
|
||||
cdef public int idx
|
||||
cdef public set available_pieces
|
||||
cdef public set corners
|
||||
cdef public int score
|
||||
cdef public bint has_started
|
||||
cdef public bint can_move
|
||||
|
||||
def __init__(self):
|
||||
self.idx = -1
|
||||
self.available_pieces = set()
|
||||
self.corners = set()
|
||||
self.score = 0
|
||||
self.has_started = False
|
||||
self.can_move = True
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Main Game class
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
cdef class BlokusGame:
|
||||
"""Cython-optimized Blokus game engine."""
|
||||
|
||||
cdef public int board_size
|
||||
cdef public PieceSet piece_set
|
||||
cdef public int num_players
|
||||
cdef public bint corner_rule
|
||||
cdef public object board
|
||||
cdef public list players
|
||||
cdef public int current_player
|
||||
cdef public int rounds
|
||||
cdef public bint game_over
|
||||
cdef public list _action_moves
|
||||
cdef public dict _move_to_action
|
||||
cdef public list _actions_by_piece
|
||||
cdef public list _starting_corners
|
||||
|
||||
def __init__(self, int board_size=20, PieceSet pieces=None, int num_players=4, bint corner_rule=True):
|
||||
self.board_size = board_size
|
||||
self.piece_set = pieces or PieceSet(STANDARD_PIECES)
|
||||
self.num_players = num_players
|
||||
self.corner_rule = corner_rule
|
||||
|
||||
self.board = Board(board_size)
|
||||
self.players = []
|
||||
self.current_player = 0
|
||||
self.rounds = 0
|
||||
self.game_over = False
|
||||
|
||||
# Pre-compute all possible moves
|
||||
self._action_moves = []
|
||||
self._move_to_action = {}
|
||||
self._generate_action_space()
|
||||
|
||||
# Pre-compute action indices per piece
|
||||
self._actions_by_piece = []
|
||||
for piece_id in range(self.piece_set.num_pieces):
|
||||
self._actions_by_piece.append([
|
||||
idx for idx, move in enumerate(self._action_moves)
|
||||
if move.piece_id == piece_id
|
||||
])
|
||||
|
||||
# Starting corners
|
||||
max_idx = board_size - 1
|
||||
self._starting_corners = [
|
||||
(0, 0),
|
||||
(0, max_idx),
|
||||
(max_idx, 0),
|
||||
(max_idx, max_idx),
|
||||
]
|
||||
if num_players == 2:
|
||||
self._starting_corners = [(0, 0), (max_idx, max_idx)]
|
||||
elif num_players == 3:
|
||||
self._starting_corners = [(0, 0), (0, max_idx), (max_idx, 0)]
|
||||
|
||||
# Initialize players
|
||||
for i in range(self.num_players):
|
||||
player = PlayerState()
|
||||
player.idx = i
|
||||
player.available_pieces = set(self.piece_set.piece_names)
|
||||
if self.corner_rule and i < len(self._starting_corners):
|
||||
player.corners = {self._starting_corners[i]}
|
||||
else:
|
||||
player.corners = set()
|
||||
player.score = 0
|
||||
player.has_started = False
|
||||
player.can_move = True
|
||||
self.players.append(player)
|
||||
|
||||
cdef void _generate_action_space(self):
|
||||
"""Pre-compute all possible (piece, orientation, position) moves."""
|
||||
cdef int piece_id, orient_id
|
||||
cdef PieceOrientation orient
|
||||
cdef list squares
|
||||
cdef int max_x, max_y
|
||||
cdef int px, py
|
||||
cdef Move move
|
||||
cdef int action_idx
|
||||
|
||||
self._action_moves = []
|
||||
self._move_to_action = {}
|
||||
|
||||
for piece_id in range(self.piece_set.num_pieces):
|
||||
for orient_id, orient in enumerate(self.piece_set.get_orientations(piece_id)):
|
||||
squares = orient.squares
|
||||
max_x = max(x for x, _ in squares)
|
||||
max_y = max(y for _, y in squares)
|
||||
|
||||
for px in range(self.board_size - max_x):
|
||||
for py in range(self.board_size - max_y):
|
||||
move = Move(piece_id, orient_id, px, py)
|
||||
action_idx = len(self._action_moves)
|
||||
self._action_moves.append(move)
|
||||
self._move_to_action[(piece_id, orient_id, px, py)] = action_idx
|
||||
|
||||
@property
|
||||
def num_actions(self) -> int:
|
||||
return len(self._action_moves)
|
||||
|
||||
def get_move(self, int action) -> Move:
|
||||
return self._action_moves[action]
|
||||
|
||||
def get_action(self, Move move) -> int:
|
||||
return self._move_to_action[(move.piece_id, move.orientation_id, move.x, move.y)]
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the game to its initial state."""
|
||||
self.board = Board(self.board_size)
|
||||
self.players = []
|
||||
self.current_player = 0
|
||||
self.rounds = 0
|
||||
self.game_over = False
|
||||
|
||||
for i in range(self.num_players):
|
||||
player = PlayerState()
|
||||
player.idx = i
|
||||
player.available_pieces = set(self.piece_set.piece_names)
|
||||
if self.corner_rule and i < len(self._starting_corners):
|
||||
player.corners = {self._starting_corners[i]}
|
||||
else:
|
||||
player.corners = set()
|
||||
player.score = 0
|
||||
player.has_started = False
|
||||
player.can_move = True
|
||||
self.players.append(player)
|
||||
|
||||
cdef list _get_placed_squares(self, Move move):
|
||||
"""Get the absolute board coordinates for a move."""
|
||||
cdef PieceOrientation orient = self.piece_set.get_orientations(move.piece_id)[move.orientation_id]
|
||||
cdef list result = []
|
||||
cdef int dx, dy
|
||||
for dx, dy in orient.squares:
|
||||
result.append((move.x + dx, move.y + dy))
|
||||
return result
|
||||
|
||||
cdef list _get_placed_corners(self, Move move):
|
||||
"""Get the absolute corner coordinates for a move."""
|
||||
cdef PieceOrientation orient = self.piece_set.get_orientations(move.piece_id)[move.orientation_id]
|
||||
cdef list result = []
|
||||
cdef int dx, dy
|
||||
for dx, dy in orient.corners:
|
||||
result.append((move.x + dx, move.y + dy))
|
||||
return result
|
||||
|
||||
cpdef bint valid_move(self, int player_idx, Move move):
|
||||
"""Check if a move is valid for the given player."""
|
||||
cdef PlayerState player = self.players[player_idx]
|
||||
cdef Piece piece = self.piece_set.get_piece(move.piece_id)
|
||||
cdef list placed_squares = self._get_placed_squares(move)
|
||||
cdef list placed_corners = self._get_placed_corners(move)
|
||||
cdef int x, y
|
||||
cdef int player_board_idx = player_idx + 1
|
||||
cdef set player_occupied
|
||||
cdef bint result
|
||||
|
||||
# Rule 1: Check if player has the piece
|
||||
if piece.name not in player.available_pieces:
|
||||
return False
|
||||
|
||||
# Rule 2: Check bounds (inline for speed)
|
||||
for x, y in placed_squares:
|
||||
if not self.board.in_bounds(x, y):
|
||||
return False
|
||||
|
||||
# Rule 3: Check overlap
|
||||
if self.board.has_overlap(placed_squares):
|
||||
return False
|
||||
|
||||
# Rule 6: First move must be in a corner
|
||||
if self.corner_rule and not player.has_started:
|
||||
if len(self.players) <= len(self._starting_corners):
|
||||
start_corner = self._starting_corners[player_idx]
|
||||
if (move.x, move.y) != start_corner:
|
||||
return False
|
||||
|
||||
# Rule 5: No edge adjacency with same-color pieces (inline for speed)
|
||||
for x, y in placed_squares:
|
||||
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
||||
if self.board.in_bounds(x + dx, y + dy):
|
||||
if self.board.get_cell(x + dx, y + dy) == player_board_idx:
|
||||
return False
|
||||
|
||||
# Rule 4: Corner rule (must touch same-color at a corner)
|
||||
if player.has_started:
|
||||
player_occupied = self.board.get_player_occupied(player_idx)
|
||||
# Manual any() implementation - Cython doesn't support generator expressions
|
||||
result = False
|
||||
for cx, cy in placed_corners:
|
||||
if self.board.in_bounds(cx, cy) and (cx, cy) in player_occupied:
|
||||
result = True
|
||||
break
|
||||
if not result:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
cpdef get_valid_actions(self, int player_idx):
|
||||
"""Get a boolean mask of valid actions for the given player."""
|
||||
cdef PlayerState player = self.players[player_idx]
|
||||
cdef cnp.ndarray mask = np.zeros(self.num_actions, dtype=bool)
|
||||
cdef list available_piece_ids = []
|
||||
cdef int piece_id, action_idx
|
||||
cdef Move move
|
||||
cdef list placed_corners, placed_squares
|
||||
cdef int x, y, player_board_idx = player_idx + 1
|
||||
cdef set player_occupied
|
||||
cdef bint edge_invalid, result
|
||||
cdef list corners_list
|
||||
cdef int cx, cy
|
||||
|
||||
# Build available_piece_ids list
|
||||
for name in player.available_pieces:
|
||||
available_piece_ids.append(self.piece_set.get_piece_id(name))
|
||||
|
||||
if not player.can_move:
|
||||
return mask
|
||||
|
||||
if not player.has_started or not player.corners:
|
||||
for piece_id in available_piece_ids:
|
||||
for action_idx in self._actions_by_piece[piece_id]:
|
||||
if self.valid_move(player_idx, self._action_moves[action_idx]):
|
||||
mask[action_idx] = True
|
||||
return mask
|
||||
|
||||
# Pre-compute player's occupied squares ONCE
|
||||
player_occupied = self.board.get_player_occupied(player_idx + 1)
|
||||
|
||||
for piece_id in available_piece_ids:
|
||||
for action_idx in self._actions_by_piece[piece_id]:
|
||||
move = self._action_moves[action_idx]
|
||||
placed_corners = self._get_placed_corners(move)
|
||||
|
||||
# Early exit: check corner touch (manual any())
|
||||
result = False
|
||||
for cx, cy in placed_corners:
|
||||
if self.board.in_bounds(cx, cy) and (cx, cy) in player_occupied:
|
||||
result = True
|
||||
break
|
||||
if not result:
|
||||
continue
|
||||
|
||||
placed_squares = self._get_placed_squares(move)
|
||||
|
||||
# Check bounds
|
||||
result = True
|
||||
for x, y in placed_squares:
|
||||
if not self.board.in_bounds(x, y):
|
||||
result = False
|
||||
break
|
||||
if not result:
|
||||
continue
|
||||
|
||||
# Check overlap
|
||||
if self.board.has_overlap(placed_squares):
|
||||
continue
|
||||
|
||||
# Check edge adjacency (inline for speed)
|
||||
edge_invalid = False
|
||||
for x, y in placed_squares:
|
||||
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
||||
if self.board.in_bounds(x + dx, y + dy):
|
||||
if self.board.get_cell(x + dx, y + dy) == player_board_idx:
|
||||
edge_invalid = True
|
||||
break
|
||||
if edge_invalid:
|
||||
break
|
||||
|
||||
if not edge_invalid:
|
||||
mask[action_idx] = True
|
||||
|
||||
return mask
|
||||
|
||||
cpdef bint has_valid_moves(self, int player_idx):
|
||||
"""Check if a player has any valid moves."""
|
||||
cdef PlayerState player = self.players[player_idx]
|
||||
cdef list available_piece_ids = []
|
||||
cdef int piece_id, action_idx
|
||||
|
||||
# Build available_piece_ids list
|
||||
for name in player.available_pieces:
|
||||
available_piece_ids.append(self.piece_set.get_piece_id(name))
|
||||
|
||||
if not player.can_move:
|
||||
return False
|
||||
|
||||
for piece_id in available_piece_ids:
|
||||
for action_idx in self._actions_by_piece[piece_id]:
|
||||
if self.valid_move(player_idx, self._action_moves[action_idx]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def apply_move(self, int player_idx, Move move) -> None:
|
||||
"""Apply a move to the game state. Does not validate."""
|
||||
cdef PlayerState player = self.players[player_idx]
|
||||
cdef Piece piece = self.piece_set.get_piece(move.piece_id)
|
||||
cdef list placed_squares = self._get_placed_squares(move)
|
||||
cdef int x, y
|
||||
|
||||
# Place the piece
|
||||
self.board.place(player_idx + 1, placed_squares)
|
||||
|
||||
# Update player state
|
||||
player.available_pieces.discard(piece.name)
|
||||
player.score += piece.size
|
||||
|
||||
# Update corners
|
||||
if not player.has_started:
|
||||
player.has_started = True
|
||||
player.corners = set()
|
||||
|
||||
# Add new corners
|
||||
cdef list placed_corners = self._get_placed_corners(move)
|
||||
for x, y in placed_corners:
|
||||
if self.board.in_bounds(x, y) and self.board.is_empty(x, y):
|
||||
player.corners.add((x, y))
|
||||
|
||||
# Check game over
|
||||
self._check_game_over()
|
||||
|
||||
def play_move(self, int player_idx, int action) -> bint:
|
||||
"""Play an action for the given player. Validates first."""
|
||||
cdef Move move = self._action_moves[action]
|
||||
|
||||
if not self.valid_move(player_idx, move):
|
||||
return False
|
||||
|
||||
self.apply_move(player_idx, move)
|
||||
return True
|
||||
|
||||
def next_player(self):
|
||||
"""Advance to the next player who can still move.
|
||||
|
||||
If no players can move, the game is over.
|
||||
"""
|
||||
if self.game_over:
|
||||
return self.current_player
|
||||
|
||||
for offset in range(1, self.num_players + 1):
|
||||
next_idx = (self.current_player + offset) % self.num_players
|
||||
next_player = self.players[next_idx]
|
||||
if next_player.can_move and self.has_valid_moves(next_idx):
|
||||
self.current_player = next_idx
|
||||
return next_idx
|
||||
|
||||
# No one can move — game over
|
||||
self.game_over = True
|
||||
return self.current_player
|
||||
|
||||
def _check_game_over(self) -> None:
|
||||
"""Check if the game is over."""
|
||||
cdef PlayerState player
|
||||
cdef int player_idx
|
||||
|
||||
for player_idx, player in enumerate(self.players):
|
||||
if not self.has_valid_moves(player_idx):
|
||||
player.can_move = False
|
||||
|
||||
# Game ends if all players can't move
|
||||
if all(not p.can_move for p in self.players):
|
||||
self.game_over = True
|
||||
elif all(p.has_started and not p.available_pieces for p in self.players):
|
||||
# All pieces played
|
||||
self.game_over = True
|
||||
|
||||
def get_state(self) -> dict:
|
||||
"""Get the current game state as a dictionary."""
|
||||
return {
|
||||
"board_size": self.board_size,
|
||||
"num_players": self.num_players,
|
||||
"current_player": self.current_player,
|
||||
"rounds": self.rounds,
|
||||
"game_over": self.game_over,
|
||||
"players": [
|
||||
{
|
||||
"idx": p.idx,
|
||||
"available_pieces": list(p.available_pieces),
|
||||
"score": p.score,
|
||||
"has_started": p.has_started,
|
||||
"can_move": p.can_move,
|
||||
}
|
||||
for p in self.players
|
||||
],
|
||||
}
|
||||
|
||||
def copy(self) -> BlokusGame:
|
||||
"""Create a deep copy of the game."""
|
||||
cdef BlokusGame new_game = BlokusGame(
|
||||
board_size=self.board_size,
|
||||
pieces=self.piece_set,
|
||||
num_players=self.num_players,
|
||||
corner_rule=self.corner_rule
|
||||
)
|
||||
|
||||
new_game.board = self.board.copy()
|
||||
new_game.current_player = self.current_player
|
||||
new_game.rounds = self.rounds
|
||||
new_game.game_over = self.game_over
|
||||
|
||||
for i, p in enumerate(self.players):
|
||||
new_game.players[i].available_pieces = p.available_pieces.copy()
|
||||
new_game.players[i].corners = p.corners.copy()
|
||||
new_game.players[i].score = p.score
|
||||
new_game.players[i].has_started = p.has_started
|
||||
new_game.players[i].can_move = p.can_move
|
||||
|
||||
return new_game
|
||||
Reference in New Issue
Block a user