#!/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
