updated to use optimized cyython build
This commit is contained in:
@@ -1,35 +0,0 @@
|
|||||||
Metadata-Version: 2.4
|
|
||||||
Name: blokus-gym
|
|
||||||
Version: 0.1.0
|
|
||||||
Summary: A Gymnasium-compatible RL environment for the board game Blokus, with a playable web UI
|
|
||||||
Author: Blokus Gym Contributors
|
|
||||||
License: MIT
|
|
||||||
Project-URL: Homepage, https://github.com/blokus-gym/blokus-gym
|
|
||||||
Project-URL: Documentation, https://github.com/blokus-gym/blokus-gym#readme
|
|
||||||
Keywords: reinforcement-learning,gymnasium,blokus,board-game,pettingzoo,flask,game-ui
|
|
||||||
Classifier: Development Status :: 3 - Alpha
|
|
||||||
Classifier: Intended Audience :: Science/Research
|
|
||||||
Classifier: License :: OSI Approved :: MIT License
|
|
||||||
Classifier: Programming Language :: Python :: 3
|
|
||||||
Classifier: Programming Language :: Python :: 3.10
|
|
||||||
Classifier: Programming Language :: Python :: 3.11
|
|
||||||
Classifier: Programming Language :: Python :: 3.12
|
|
||||||
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
||||||
Requires-Python: >=3.10
|
|
||||||
Description-Content-Type: text/markdown
|
|
||||||
Requires-Dist: numpy>=1.24
|
|
||||||
Requires-Dist: gymnasium>=0.29
|
|
||||||
Requires-Dist: pettingzoo>=1.26
|
|
||||||
Provides-Extra: render
|
|
||||||
Requires-Dist: matplotlib>=3.7; extra == "render"
|
|
||||||
Provides-Extra: ui
|
|
||||||
Requires-Dist: flask>=3.0; extra == "ui"
|
|
||||||
Provides-Extra: dev
|
|
||||||
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
||||||
Requires-Dist: pytest-cov>=4.1; extra == "dev"
|
|
||||||
Requires-Dist: black>=23.7; extra == "dev"
|
|
||||||
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
||||||
Requires-Dist: mypy>=1.5; extra == "dev"
|
|
||||||
Provides-Extra: train
|
|
||||||
Requires-Dist: stable-baselines3>=2.2; extra == "train"
|
|
||||||
Requires-Dist: torch>=2.0; extra == "train"
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
pyproject.toml
|
|
||||||
src/blokus_gym/__init__.py
|
|
||||||
src/blokus_gym.egg-info/PKG-INFO
|
|
||||||
src/blokus_gym.egg-info/SOURCES.txt
|
|
||||||
src/blokus_gym.egg-info/dependency_links.txt
|
|
||||||
src/blokus_gym.egg-info/entry_points.txt
|
|
||||||
src/blokus_gym.egg-info/requires.txt
|
|
||||||
src/blokus_gym.egg-info/top_level.txt
|
|
||||||
src/blokus_gym/core/__init__.py
|
|
||||||
src/blokus_gym/core/board.py
|
|
||||||
src/blokus_gym/core/bots.py
|
|
||||||
src/blokus_gym/core/game.py
|
|
||||||
src/blokus_gym/core/pieces.py
|
|
||||||
src/blokus_gym/envs/__init__.py
|
|
||||||
src/blokus_gym/envs/blokus_env.py
|
|
||||||
src/blokus_gym/envs/multiagent.py
|
|
||||||
src/blokus_gym/utils/__init__.py
|
|
||||||
src/blokus_gym/utils/render.py
|
|
||||||
src/blokus_gym/wrappers/__init__.py
|
|
||||||
src/blokus_gym/wrappers/action_mask.py
|
|
||||||
src/blokus_ui/__init__.py
|
|
||||||
src/blokus_ui/app.py
|
|
||||||
src/blokus_ui/session.py
|
|
||||||
src/blokus_ui/store.py
|
|
||||||
tests/test_board.py
|
|
||||||
tests/test_envs.py
|
|
||||||
tests/test_game.py
|
|
||||||
tests/test_imports.py
|
|
||||||
tests/test_pieces.py
|
|
||||||
tests/test_ui.py
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
numpy>=1.24
|
|
||||||
gymnasium>=0.29
|
|
||||||
pettingzoo>=1.26
|
|
||||||
|
|
||||||
[dev]
|
|
||||||
pytest>=7.4
|
|
||||||
pytest-cov>=4.1
|
|
||||||
black>=23.7
|
|
||||||
ruff>=0.1.0
|
|
||||||
mypy>=1.5
|
|
||||||
|
|
||||||
[render]
|
|
||||||
matplotlib>=3.7
|
|
||||||
|
|
||||||
[train]
|
|
||||||
stable-baselines3>=2.2
|
|
||||||
torch>=2.0
|
|
||||||
|
|
||||||
[ui]
|
|
||||||
flask>=3.0
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
blokus_gym
|
|
||||||
blokus_ui
|
|
||||||
Binary file not shown.
@@ -1,7 +1,24 @@
|
|||||||
from blokus_gym.core.board import Board
|
# Try to import Cython version first, fall back to Python
|
||||||
from blokus_gym.core.bots import Bot, GreedyBot, GreedyCornersBot, MinimaxBot, RandomBot
|
try:
|
||||||
from blokus_gym.core.game import BlokusGame, Move
|
from .blokus_cython import (
|
||||||
from blokus_gym.core.pieces import (
|
Board,
|
||||||
|
BlokusGame,
|
||||||
|
Move,
|
||||||
|
Piece,
|
||||||
|
PieceOrientation,
|
||||||
|
PieceSet,
|
||||||
|
)
|
||||||
|
# Import STANDARD_PIECES from pieces.py since it's not in Cython
|
||||||
|
from .pieces import STANDARD_PIECES, DUO_PIECES, JUNIOR_PIECES
|
||||||
|
# Import generate_orientations from pieces.py
|
||||||
|
from .pieces import generate_orientations
|
||||||
|
# Import bots from Python since they're not in Cython
|
||||||
|
from .bots import Bot, GreedyBot, GreedyCornersBot, MinimaxBot, RandomBot
|
||||||
|
except ImportError:
|
||||||
|
from .board import Board
|
||||||
|
from .bots import Bot, GreedyBot, GreedyCornersBot, MinimaxBot, RandomBot
|
||||||
|
from .game import BlokusGame, Move
|
||||||
|
from .pieces import (
|
||||||
DUO_PIECES,
|
DUO_PIECES,
|
||||||
JUNIOR_PIECES,
|
JUNIOR_PIECES,
|
||||||
STANDARD_PIECES,
|
STANDARD_PIECES,
|
||||||
@@ -9,7 +26,7 @@ from blokus_gym.core.pieces import (
|
|||||||
PieceOrientation,
|
PieceOrientation,
|
||||||
PieceSet,
|
PieceSet,
|
||||||
generate_orientations,
|
generate_orientations,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Board",
|
"Board",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -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
|
||||||
@@ -49,18 +49,20 @@ class Board:
|
|||||||
"""Get the player index at a cell (0 = empty)."""
|
"""Get the player index at a cell (0 = empty)."""
|
||||||
return self.grid[y, x]
|
return self.grid[y, x]
|
||||||
|
|
||||||
def get_player_squares(self, player_idx: int) -> list[tuple[int, int]]:
|
def get_player_squares(self, player_idx: int, player_board_idx: int | None = None) -> list[tuple[int, int]]:
|
||||||
"""Get all squares occupied by a player."""
|
"""Get all squares occupied by a player."""
|
||||||
ys, xs = np.where(self.grid == player_idx)
|
if player_board_idx is None:
|
||||||
|
player_board_idx = player_idx + 1 # Default: PLAYER_OFFSET = 1
|
||||||
|
ys, xs = np.where(self.grid == player_board_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]]:
|
def get_player_occupied(self, player_board_idx: int) -> set[tuple[int, int]]:
|
||||||
"""Get all squares occupied by a player as a set for fast lookup."""
|
"""Get all squares occupied by a player as a set for fast lookup."""
|
||||||
if player_idx not in self._occupied_cache:
|
if player_board_idx not in self._occupied_cache:
|
||||||
self._occupied_cache[player_idx] = set(self.get_player_squares(player_idx))
|
self._occupied_cache[player_board_idx] = set(self.get_player_squares(player_board_idx - 1, player_board_idx))
|
||||||
return self._occupied_cache[player_idx]
|
return self._occupied_cache[player_board_idx]
|
||||||
|
|
||||||
def get_player_corners(self, player_idx: int) -> set[tuple[int, int]]:
|
def get_player_corners(self, player_board_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.
|
||||||
|
|
||||||
Corner cells are the diagonal neighbors of a player's pieces.
|
Corner cells are the diagonal neighbors of a player's pieces.
|
||||||
@@ -68,7 +70,7 @@ class Board:
|
|||||||
(corner-to-corner contact rule).
|
(corner-to-corner contact rule).
|
||||||
"""
|
"""
|
||||||
corners: set[tuple[int, int]] = set()
|
corners: set[tuple[int, int]] = set()
|
||||||
player_squares = self.get_player_squares(player_idx)
|
player_squares = self.get_player_squares(player_board_idx - 1, player_board_idx)
|
||||||
for x, y in player_squares:
|
for x, y in player_squares:
|
||||||
for dx, dy in [(-1, -1), (1, -1), (-1, 1), (1, 1)]:
|
for dx, dy in [(-1, -1), (1, -1), (-1, 1), (1, 1)]:
|
||||||
cx, cy = x + dx, y + dy
|
cx, cy = x + dx, y + dy
|
||||||
|
|||||||
src/blokus_gym/core/build/lib.linux-x86_64-cpython-312/blokus_cython.cpython-312-x86_64-linux-gnu.so
Executable
BIN
Binary file not shown.
Binary file not shown.
@@ -207,7 +207,7 @@ class BlokusGame:
|
|||||||
if player.has_started:
|
if player.has_started:
|
||||||
placed_corners = self._get_placed_corners(move)
|
placed_corners = self._get_placed_corners(move)
|
||||||
# Use cached occupied squares from board for fast lookup
|
# Use cached occupied squares from board for fast lookup
|
||||||
player_occupied = self.board.get_player_occupied(player_idx)
|
player_occupied = self.board.get_player_occupied(player_board_idx)
|
||||||
touches_corner = any(
|
touches_corner = any(
|
||||||
self.board.in_bounds(cx, cy) and (cx, cy) in player_occupied
|
self.board.in_bounds(cx, cy) and (cx, cy) in player_occupied
|
||||||
for cx, cy in placed_corners
|
for cx, cy in placed_corners
|
||||||
@@ -240,7 +240,7 @@ 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)
|
# Pre-compute player's occupied squares ONCE (cached)
|
||||||
player_occupied = self.board.get_player_occupied(player_idx)
|
player_occupied = self.board.get_player_occupied(player_board_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]:
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Setup script for compiling Cython extensions."""
|
||||||
|
|
||||||
|
from setuptools import setup, Extension
|
||||||
|
from Cython.Build import cythonize
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# Define extensions with proper include directories
|
||||||
|
extensions = [
|
||||||
|
Extension(
|
||||||
|
"blokus_cython",
|
||||||
|
["blokus_cython.pyx"],
|
||||||
|
include_dirs=[np.get_include()],
|
||||||
|
define_macros=[("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
setup(
|
||||||
|
ext_modules=cythonize(
|
||||||
|
extensions,
|
||||||
|
compiler_directives={"language_level": "3"},
|
||||||
|
annotate=False,
|
||||||
|
),
|
||||||
|
include_dirs=[np.get_include()],
|
||||||
|
)
|
||||||
src/build/lib.linux-x86_64-cpython-312/blokus_gym/core/blokus_cython.cpython-312-x86_64-linux-gnu.so
Executable
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Setup script for blokus_gym package."""
|
||||||
|
|
||||||
|
from setuptools import setup, Extension
|
||||||
|
from Cython.Build import cythonize
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# Define extensions with proper include directories
|
||||||
|
extensions = [
|
||||||
|
Extension(
|
||||||
|
"blokus_gym.core.blokus_cython",
|
||||||
|
["blokus_gym/core/blokus_cython.pyx"],
|
||||||
|
include_dirs=[np.get_include()],
|
||||||
|
define_macros=[("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="blokus_gym",
|
||||||
|
ext_modules=cythonize(
|
||||||
|
extensions,
|
||||||
|
compiler_directives={"language_level": "3"},
|
||||||
|
annotate=False,
|
||||||
|
),
|
||||||
|
include_dirs=[np.get_include()],
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user