feat: migrate web UI to Cython-optimized game engine
- Add cpdef wrappers (get_placed_squares, get_placed_corners) for Python accessibility of previously cdef-only methods - Add get_standard_pieces() to expose Cython standard piece set - Add is_game_over(), get_scores(), get_winners() to Cython BlokusGame - Fix valid_move() player_occupied lookup (was using player_idx instead of player_idx+1, causing bots to fail after first move) - Update session.py, app.py, bots.py to use Cython with pure Python fallback - Fix board.grid.tolist() to np.array(grid).tolist() for memoryview compat - Add Cython build artifacts to .gitignore
This commit is contained in:
@@ -6,3 +6,8 @@ __pycache__/
|
|||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
runs/
|
runs/
|
||||||
|
# Cython build artifacts
|
||||||
|
*.so
|
||||||
|
*.o
|
||||||
|
blokus_cython.c
|
||||||
|
build/
|
||||||
|
|||||||
@@ -308,6 +308,11 @@ cdef list STANDARD_PIECES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_standard_pieces():
|
||||||
|
"""Python-accessible function to get the standard piece set."""
|
||||||
|
return list(STANDARD_PIECES)
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------
|
# -------------------------------------------------------------------
|
||||||
# Orientation generation
|
# Orientation generation
|
||||||
# -------------------------------------------------------------------
|
# -------------------------------------------------------------------
|
||||||
@@ -528,6 +533,10 @@ cdef class BlokusGame:
|
|||||||
result.append((move.x + dx, move.y + dy))
|
result.append((move.x + dx, move.y + dy))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
cpdef list get_placed_squares(self, Move move):
|
||||||
|
"""Python-accessible wrapper for _get_placed_squares."""
|
||||||
|
return self._get_placed_squares(move)
|
||||||
|
|
||||||
cdef list _get_placed_corners(self, Move move):
|
cdef list _get_placed_corners(self, Move move):
|
||||||
"""Get the absolute corner coordinates for a move."""
|
"""Get the absolute corner coordinates for a move."""
|
||||||
cdef PieceOrientation orient = self.piece_set.get_orientations(move.piece_id)[move.orientation_id]
|
cdef PieceOrientation orient = self.piece_set.get_orientations(move.piece_id)[move.orientation_id]
|
||||||
@@ -537,6 +546,10 @@ cdef class BlokusGame:
|
|||||||
result.append((move.x + dx, move.y + dy))
|
result.append((move.x + dx, move.y + dy))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
cpdef list get_placed_corners(self, Move move):
|
||||||
|
"""Python-accessible wrapper for _get_placed_corners."""
|
||||||
|
return self._get_placed_corners(move)
|
||||||
|
|
||||||
cpdef bint valid_move(self, int player_idx, Move move):
|
cpdef bint valid_move(self, int player_idx, Move move):
|
||||||
"""Check if a move is valid for the given player."""
|
"""Check if a move is valid for the given player."""
|
||||||
cdef PlayerState player = self.players[player_idx]
|
cdef PlayerState player = self.players[player_idx]
|
||||||
@@ -577,7 +590,7 @@ cdef class BlokusGame:
|
|||||||
|
|
||||||
# Rule 4: Corner rule (must touch same-color at a corner)
|
# Rule 4: Corner rule (must touch same-color at a corner)
|
||||||
if player.has_started:
|
if player.has_started:
|
||||||
player_occupied = self.board.get_player_occupied(player_idx)
|
player_occupied = self.board.get_player_occupied(player_board_idx)
|
||||||
# Manual any() implementation - Cython doesn't support generator expressions
|
# Manual any() implementation - Cython doesn't support generator expressions
|
||||||
result = False
|
result = False
|
||||||
for cx, cy in placed_corners:
|
for cx, cy in placed_corners:
|
||||||
@@ -757,6 +770,56 @@ cdef class BlokusGame:
|
|||||||
# All pieces played
|
# All pieces played
|
||||||
self.game_over = True
|
self.game_over = True
|
||||||
|
|
||||||
|
def is_game_over(self) -> bool:
|
||||||
|
"""Check if the game is over (no player can make a move)."""
|
||||||
|
if self.game_over:
|
||||||
|
return True
|
||||||
|
# Check if any player can still move
|
||||||
|
cdef int i
|
||||||
|
for i in range(self.num_players):
|
||||||
|
if self.players[i].can_move and self.has_valid_moves(i):
|
||||||
|
return False
|
||||||
|
self.game_over = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_scores(self) -> list:
|
||||||
|
"""Get the score for each player.
|
||||||
|
|
||||||
|
Score = squares placed (positive) - unplaced squares (negative).
|
||||||
|
A player who placed all pieces gets a +15 bonus.
|
||||||
|
If the last piece was the monomino, an additional +5 bonus.
|
||||||
|
"""
|
||||||
|
cdef list scores = []
|
||||||
|
cdef PlayerState player
|
||||||
|
cdef int unplaced
|
||||||
|
cdef int score
|
||||||
|
cdef int pid
|
||||||
|
|
||||||
|
for player in self.players:
|
||||||
|
# Sum sizes of remaining pieces
|
||||||
|
unplaced = 0
|
||||||
|
for pid in range(self.piece_set.num_pieces):
|
||||||
|
piece = self.piece_set.get_piece(pid)
|
||||||
|
if piece.name in player.available_pieces:
|
||||||
|
unplaced += piece.size
|
||||||
|
score = player.score - unplaced
|
||||||
|
|
||||||
|
# Bonus for placing all pieces
|
||||||
|
if len(player.available_pieces) == 0:
|
||||||
|
score += 15
|
||||||
|
|
||||||
|
scores.append(score)
|
||||||
|
return scores
|
||||||
|
|
||||||
|
def get_winners(self) -> list | None:
|
||||||
|
"""Get the winning player(s). Returns None if game is not over."""
|
||||||
|
if not self.is_game_over():
|
||||||
|
return None
|
||||||
|
|
||||||
|
cdef list scores = self.get_scores()
|
||||||
|
cdef int max_score = max(scores)
|
||||||
|
return [i for i, s in enumerate(scores) if s == max_score]
|
||||||
|
|
||||||
def get_state(self) -> dict:
|
def get_state(self) -> dict:
|
||||||
"""Get the current game state as a dictionary."""
|
"""Get the current game state as a dictionary."""
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ from abc import ABC, abstractmethod
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from blokus_gym.core.game import BlokusGame
|
try:
|
||||||
|
from blokus_gym.core.blokus_cython import BlokusGame
|
||||||
|
except ImportError:
|
||||||
|
from blokus_gym.core.game import BlokusGame
|
||||||
|
|
||||||
|
|
||||||
class Bot(ABC):
|
class Bot(ABC):
|
||||||
@@ -76,7 +79,7 @@ class GreedyCornersBot(Bot):
|
|||||||
for action in valid_indices:
|
for action in valid_indices:
|
||||||
move = game.get_move(int(action))
|
move = game.get_move(int(action))
|
||||||
piece = game.piece_set.get_piece(move.piece_id)
|
piece = game.piece_set.get_piece(move.piece_id)
|
||||||
placed_corners = game._get_placed_corners(move)
|
placed_corners = game.get_placed_corners(move)
|
||||||
|
|
||||||
# Count how many new corners this move opens
|
# Count how many new corners this move opens
|
||||||
new_corners = 0
|
new_corners = 0
|
||||||
|
|||||||
+13
-4
@@ -2,10 +2,19 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
from flask import Flask, jsonify, redirect, render_template, request, url_for
|
from flask import Flask, jsonify, redirect, render_template, request, url_for
|
||||||
|
|
||||||
from blokus_gym.core.game import BlokusGame
|
try:
|
||||||
from blokus_gym.core.pieces import STANDARD_PIECES, PieceSet
|
from blokus_gym.core.blokus_cython import (
|
||||||
|
BlokusGame,
|
||||||
|
PieceSet,
|
||||||
|
get_standard_pieces,
|
||||||
|
)
|
||||||
|
STANDARD_PIECES = get_standard_pieces()
|
||||||
|
except ImportError:
|
||||||
|
from blokus_gym.core.game import BlokusGame
|
||||||
|
from blokus_gym.core.pieces import STANDARD_PIECES, PieceSet
|
||||||
from blokus_ui.session import GameSession
|
from blokus_ui.session import GameSession
|
||||||
from blokus_ui.store import RunStore
|
from blokus_ui.store import RunStore
|
||||||
|
|
||||||
@@ -115,7 +124,7 @@ def replay_page(run_id):
|
|||||||
game.reset()
|
game.reset()
|
||||||
|
|
||||||
states = [{
|
states = [{
|
||||||
"board": game.board.grid.tolist(),
|
"board": np.array(game.board.grid).tolist(),
|
||||||
"current_player": game.current_player,
|
"current_player": game.current_player,
|
||||||
"scores": game.get_scores(),
|
"scores": game.get_scores(),
|
||||||
}]
|
}]
|
||||||
@@ -124,7 +133,7 @@ def replay_page(run_id):
|
|||||||
game.play_move(move["player_idx"], move["action"])
|
game.play_move(move["player_idx"], move["action"])
|
||||||
game.next_player()
|
game.next_player()
|
||||||
states.append({
|
states.append({
|
||||||
"board": game.board.grid.tolist(),
|
"board": np.array(game.board.grid).tolist(),
|
||||||
"current_player": game.current_player,
|
"current_player": game.current_player,
|
||||||
"scores": game.get_scores(),
|
"scores": game.get_scores(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,8 +11,18 @@ from blokus_gym.core.bots import (
|
|||||||
MinimaxBot,
|
MinimaxBot,
|
||||||
RandomBot,
|
RandomBot,
|
||||||
)
|
)
|
||||||
from blokus_gym.core.game import BlokusGame
|
|
||||||
from blokus_gym.core.pieces import STANDARD_PIECES, PieceSet
|
try:
|
||||||
|
from blokus_gym.core.blokus_cython import (
|
||||||
|
BlokusGame,
|
||||||
|
PieceSet,
|
||||||
|
Move,
|
||||||
|
get_standard_pieces,
|
||||||
|
)
|
||||||
|
STANDARD_PIECES = get_standard_pieces()
|
||||||
|
except ImportError:
|
||||||
|
from blokus_gym.core.game import BlokusGame
|
||||||
|
from blokus_gym.core.pieces import STANDARD_PIECES, PieceSet, Move
|
||||||
|
|
||||||
|
|
||||||
class GameSession:
|
class GameSession:
|
||||||
@@ -145,7 +155,7 @@ class GameSession:
|
|||||||
move = self.game.get_move(int(action_idx))
|
move = self.game.get_move(int(action_idx))
|
||||||
if move.piece_id != piece_id:
|
if move.piece_id != piece_id:
|
||||||
continue
|
continue
|
||||||
cells = self.game._get_placed_squares(move)
|
cells = self.game.get_placed_squares(move)
|
||||||
placements.append(
|
placements.append(
|
||||||
{
|
{
|
||||||
"action": int(action_idx),
|
"action": int(action_idx),
|
||||||
@@ -181,7 +191,7 @@ class GameSession:
|
|||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|
||||||
def get_state(self) -> dict[str, Any]:
|
def get_state(self) -> dict[str, Any]:
|
||||||
board = self.game.board.grid.tolist()
|
board = np.array(self.game.board.grid).tolist()
|
||||||
scores = self.game.get_scores()
|
scores = self.game.get_scores()
|
||||||
|
|
||||||
players = []
|
players = []
|
||||||
|
|||||||
Reference in New Issue
Block a user