From eca2b4acb84682e380d822a4772fb9493fac2a64 Mon Sep 17 00:00:00 2001 From: Noa Aarts Date: Fri, 5 Dec 2025 15:10:02 +0100 Subject: [PATCH] make blokus in rust --- .envrc | 1 + blokus.py | 177 ++++-------------------- flake.lock | 70 +++++++++- flake.nix | 11 +- game/.gitignore | 72 ++++++++++ game/Cargo.lock | 173 ++++++++++++++++++++++++ game/Cargo.toml | 12 ++ game/pyproject.toml | 15 ++ game/src/lib.rs | 323 ++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 4 + pyrightconfig.json | 4 + 11 files changed, 706 insertions(+), 156 deletions(-) create mode 100644 game/.gitignore create mode 100644 game/Cargo.lock create mode 100644 game/Cargo.toml create mode 100644 game/pyproject.toml create mode 100644 game/src/lib.rs create mode 100644 pyproject.toml create mode 100644 pyrightconfig.json diff --git a/.envrc b/.envrc index 3550a30..34e6b2a 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,2 @@ use flake +source .venv/bin/activate diff --git a/blokus.py b/blokus.py index d55d872..fa3ed88 100755 --- a/blokus.py +++ b/blokus.py @@ -1,159 +1,30 @@ #!/usr/bin/env python -from typing import Any -import numpy as np import random +import game BOARD_SIZE = 14 -def make_board(): - a = np.array([[0 for i in range(BOARD_SIZE)] for j in range(BOARD_SIZE)]) - a[4, 4] = -1 - a[9, 9] = -1 - return a - - -tiles = [ - np.array([[1]]), - np.array([[1], [1]]), - np.array([[1], [1], [1]]), - np.array([[1, 0], [1, 1]]), - np.array([[1], [1], [1], [1]]), - np.array([[1, 0], [1, 0], [1, 1]]), - np.array([[1, 0], [1, 1], [1, 0]]), - np.array([[1, 1], [1, 1]]), - np.array([[1, 1, 0], [0, 1, 1]]), - np.array([[1], [1], [1], [1], [1]]), - np.array([[1, 0], [1, 0], [1, 0], [1, 1]]), - np.array([[1, 0], [1, 0], [1, 1], [0, 1]]), - np.array([[1, 0], [1, 1], [1, 1]]), - np.array([[1, 1], [1, 0], [1, 1]]), - np.array([[1, 0], [1, 1], [1, 0], [1, 0]]), - np.array([[0, 1, 0], [0, 1, 0], [1, 1, 1]]), - np.array([[1, 0, 0], [1, 0, 0], [1, 1, 1]]), - np.array([[1, 1, 0], [0, 1, 1], [0, 0, 1]]), - np.array([[1, 0, 0], [1, 1, 1], [0, 0, 1]]), - np.array([[1, 0, 0], [1, 1, 1], [0, 1, 0]]), - np.array([[0, 1, 0], [1, 1, 1], [0, 1, 0]]), -] - - -def get_permutations(which_tiles: list[int]) -> list[tuple[int, np.ndarray]]: - permutations: list[tuple[int, np.ndarray]] = [] - - for i, tile in enumerate(tiles): - if i not in which_tiles: - continue - - rots = [np.rot90(tile, k) for k in range(4)] - flips = [np.flip(r, axis=1) for r in rots] # flip horizontally - all_orients = rots + flips # 8 orientations - - seen: set[tuple[Any, bytes]] = set() - for t in all_orients: - key = (t.shape, t.tobytes()) - if key not in seen: - seen.add(key) - permutations.append((i, t)) - - return permutations +tiles = game.game_tiles() def can_place( - board: np.ndarray, tile: np.ndarray, player: int + board: game.Board, tile: game.Tile, player: game.Player ) -> list[tuple[int, int]]: - placements: list[tuple[int, int]] = [] - has_minus_one = False - for x in range(BOARD_SIZE): - for y in range(BOARD_SIZE): - if board[x, y] == -1: - has_minus_one = True - with np.nditer(tile, flags=["multi_index"]) as it: - for v in it: - if v == 1: - (i, j) = it.multi_index - if x + i >= BOARD_SIZE: - break - if y + j >= BOARD_SIZE: - break - if board[x + i][y + j] > 0: - break - if x + i - 1 >= 0 and board[x + i - 1][y + j] == player: - break - if y + j - 1 >= 0 and board[x + i][y + j - 1] == player: - break - if x + i + 1 < BOARD_SIZE and board[x + i + 1][y + j] == player: - break - if y + j + 1 < BOARD_SIZE and board[x + i][y + j + 1] == player: - break - else: - placements.append((x, y)) - final: list[tuple[int, int]] = [] - if has_minus_one: - for x, y in placements: - with np.nditer(tile, flags=["multi_index"]) as it: - for v in it: - (i, j) = it.multi_index - if v == 1 and board[x + i, y + j] == -1: - final.append((x, y)) - break - else: - for x, y in placements: - with np.nditer(tile, flags=["multi_index"]) as it: - for v in it: - (i, j) = it.multi_index - if ( - x + i + 1 < BOARD_SIZE - and y + j + 1 < BOARD_SIZE - and board[x + i + 1][y + j + 1] == player - ): - final.append((x, y)) - break - if ( - x + i + 1 < BOARD_SIZE - and y + j - 1 >= 0 - and board[x + i + 1][y + j - 1] == player - ): - final.append((x, y)) - break - if ( - x + i - 1 >= 0 - and y + j + 1 < BOARD_SIZE - and board[x + i - 1][y + j + 1] == player - ): - final.append((x, y)) - break - if ( - x + i - 1 >= 0 - and y + j - 1 >= 0 - and board[x + i - 1][y + j - 1] == player - ): - final.append((x, y)) - break - return final + placements = [] + placements.extend -def do_placement( - tidx: int, - tile: np.ndarray, - placement: tuple[int, int], - game_state: tuple[np.ndarray, list[int], list[int]], - player: int, -): - assert player > 0 - (x, y) = placement - with np.nditer(tile, flags=["multi_index"]) as it: - for v in it: - (i, j) = it.multi_index - if v == 1: - game_state[0][x + i, y + j] = player - game_state[player].remove(tidx) - - -def print_game_state(game_state: tuple[np.ndarray, list[int], list[int]]): +def print_game_state(game_state: tuple[game.Board, list[int], list[int]]): (board, p1tiles, p2tiles) = game_state - for row in board: + barr = [] + for i in range(BOARD_SIZE): + barr.append([]) + for j in range(BOARD_SIZE): + barr[i].append(board[(j, i)]) + + for row in barr: print( "".join( [ @@ -169,7 +40,7 @@ def print_game_state(game_state: tuple[np.ndarray, list[int], list[int]]): game_state = ( - make_board(), + game.Board(), [i for i in range(21)], [i for i in range(21)], ) @@ -179,12 +50,15 @@ playing = True player = 1 while playing: moves = [] - assert player > 0 - for tidx, tile in get_permutations(game_state[player]): - for placement in can_place(game_state[0], tile, player): - moves.append((tidx, tile, placement)) + assert player == 1 or player == 2 + gp = game.Player.P1 if player == 1 else game.Player.P2 + for tile_idx in game_state[player]: + tile = tiles[tile_idx] + perms = tile.permutations() + for perm in perms: + plcs = game_state[0].tile_placements(perm, gp) + moves.extend((tile_idx, perm, plc) for plc in plcs) - print_game_state(game_state) print(f"player {player} has {len(moves)} options") if len(moves) == 0: @@ -193,7 +67,12 @@ while playing: continue (tidx, tile, placement) = random.choice(moves) - do_placement(tidx, tile, placement, game_state, player) + print( + f"player {player} is placing the following tile with index {tidx} at {placement}\n{tile}" + ) + game_state[0].place(tile, placement, gp) + game_state[player].remove(tidx) + print_game_state(game_state) if player == 1: player = 2 diff --git a/flake.lock b/flake.lock index e5c2c44..779a10e 100644 --- a/flake.lock +++ b/flake.lock @@ -1,6 +1,56 @@ { "nodes": { + "fix-py": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1752329544, + "narHash": "sha256-LtZyywexRDB5FFmEU6A1e5tMIb0MX8/xxpjCHonUzYE=", + "owner": "GuillaumeDesforges", + "repo": "fix-python", + "rev": "248e2ea9620faee9b8a2ae10e12320de2a819fe9", + "type": "github" + }, + "original": { + "owner": "GuillaumeDesforges", + "repo": "fix-python", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1689068808, + "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", + "type": "github" + }, + "original": { + "id": "flake-utils", + "type": "indirect" + } + }, "nixpkgs": { + "locked": { + "lastModified": 1682786779, + "narHash": "sha256-m7QFzPS/CE8hbkbIVK4UStihAQMtczr0vSpOgETOM1g=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "08e4dc3a907a6dfec8bb3bbf1540d8abbffea22b", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "nixpkgs_2": { "locked": { "lastModified": 1764167966, "narHash": "sha256-nXv6xb7cq+XpjBYIjWEGTLCqQetxJu6zvVlrqHMsCOA=", @@ -18,8 +68,9 @@ }, "root": { "inputs": { - "nixpkgs": "nixpkgs", - "systems": "systems" + "fix-py": "fix-py", + "nixpkgs": "nixpkgs_2", + "systems": "systems_2" } }, "systems": { @@ -36,6 +87,21 @@ "repo": "default", "type": "github" } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 66a3e35..d8fba7b 100644 --- a/flake.nix +++ b/flake.nix @@ -2,10 +2,11 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; systems.url = "github:nix-systems/default"; + fix-py.url = "github:GuillaumeDesforges/fix-python"; }; outputs = - { nixpkgs, ... }: + { nixpkgs, fix-py, ... }: let eachSystem = f: @@ -15,10 +16,10 @@ devShells = eachSystem (pkgs: { default = pkgs.mkShell { buildInputs = [ - (pkgs.python3.withPackages (ppkgs: [ - ppkgs.numpy - ppkgs.torch - ])) + pkgs.cargo + pkgs.rustc + pkgs.stdenv.cc.cc.lib + fix-py.packages.${pkgs.system}.default ]; }; }); diff --git a/game/.gitignore b/game/.gitignore new file mode 100644 index 0000000..c8f0442 --- /dev/null +++ b/game/.gitignore @@ -0,0 +1,72 @@ +/target + +# Byte-compiled / optimized / DLL files +__pycache__/ +.pytest_cache/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +.venv/ +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +include/ +man/ +venv/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +pip-selfcheck.json + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +.DS_Store + +# Sphinx documentation +docs/_build/ + +# PyCharm +.idea/ + +# VSCode +.vscode/ + +# Pyenv +.python-version diff --git a/game/Cargo.lock b/game/Cargo.lock new file mode 100644 index 0000000..766b914 --- /dev/null +++ b/game/Cargo.lock @@ -0,0 +1,173 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "game" +version = "0.1.0" +dependencies = [ + "pyo3", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a" +dependencies = [ + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" diff --git a/game/Cargo.toml b/game/Cargo.toml new file mode 100644 index 0000000..a284c0e --- /dev/null +++ b/game/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "game" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "game" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = "0.25.0" diff --git a/game/pyproject.toml b/game/pyproject.toml new file mode 100644 index 0000000..a80aac4 --- /dev/null +++ b/game/pyproject.toml @@ -0,0 +1,15 @@ +[build-system] +requires = ["maturin>=1.9,<2.0"] +build-backend = "maturin" + +[project] +name = "game" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dynamic = ["version"] +[tool.maturin] +features = ["pyo3/extension-module"] diff --git a/game/src/lib.rs b/game/src/lib.rs new file mode 100644 index 0000000..c6298bf --- /dev/null +++ b/game/src/lib.rs @@ -0,0 +1,323 @@ +use pyo3::prelude::*; + +#[pymodule] +mod game { + use pyo3::{exceptions::PyIndexError, prelude::*, types::PySequence}; + use std::{ + collections::HashSet, + fmt::{Display, Formatter}, + ops::{Index, IndexMut}, + }; + const BOARD_SIZE: usize = 14; + const START_SQUARES: [(usize, usize); 2] = [(4, 4), (9, 9)]; + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + enum Square { + Start, + Empty, + Player(Player), + } + + impl Square { + fn is_player(&self, player: Player) -> bool { + match self { + Square::Player(p) if *p == player => true, + _ => false, + } + } + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[pyclass] + enum Player { + P1, + P2, + } + + #[derive(Debug, Clone, PartialEq)] + #[pyclass(str)] + struct Tile { + parts: HashSet<(usize, usize)>, + size: (usize, usize), + } + + impl Display for Tile { + fn fmt(&self, fmt: &mut Formatter) -> Result<(), std::fmt::Error> { + let (xs, ys) = self.size; + let mut vvec = Vec::new(); + for _ in 0..=ys { + let mut vec = Vec::new(); + for _ in 0..=xs { + vec.push(" ") + } + vvec.push(vec) + } + for &(x, y) in self.parts.iter() { + vvec[y][x] = "██" + } + + // Print the grid + for row in vvec { + for cell in row { + write!(fmt, "{}", cell)?; + } + writeln!(fmt)?; // Newline per row + } + Ok(()) + } + } + + #[pyfunction] + fn game_tiles() -> Vec { + vec![ + Tile::new(vec![vec![1]]), + Tile::new(vec![vec![1], vec![1]]), + Tile::new(vec![vec![1], vec![1], vec![1]]), + Tile::new(vec![vec![1, 0], vec![1, 1]]), + Tile::new(vec![vec![1], vec![1], vec![1], vec![1]]), + Tile::new(vec![vec![1, 0], vec![1, 0], vec![1, 1]]), + Tile::new(vec![vec![1, 0], vec![1, 1], vec![1, 0]]), + Tile::new(vec![vec![1, 1], vec![1, 1]]), + Tile::new(vec![vec![1, 1, 0], vec![0, 1, 1]]), + Tile::new(vec![vec![1], vec![1], vec![1], vec![1], vec![1]]), + Tile::new(vec![vec![1, 0], vec![1, 0], vec![1, 0], vec![1, 1]]), + Tile::new(vec![vec![1, 0], vec![1, 0], vec![1, 1], vec![0, 1]]), + Tile::new(vec![vec![1, 0], vec![1, 1], vec![1, 1]]), + Tile::new(vec![vec![1, 1], vec![1, 0], vec![1, 1]]), + Tile::new(vec![vec![1, 0], vec![1, 1], vec![1, 0], vec![1, 0]]), + Tile::new(vec![vec![0, 1, 0], vec![0, 1, 0], vec![1, 1, 1]]), + Tile::new(vec![vec![1, 0, 0], vec![1, 0, 0], vec![1, 1, 1]]), + Tile::new(vec![vec![1, 1, 0], vec![0, 1, 1], vec![0, 0, 1]]), + Tile::new(vec![vec![1, 0, 0], vec![1, 1, 1], vec![0, 0, 1]]), + Tile::new(vec![vec![1, 0, 0], vec![1, 1, 1], vec![0, 1, 0]]), + Tile::new(vec![vec![0, 1, 0], vec![1, 1, 1], vec![0, 1, 0]]), + ] + } + + #[pymethods] + impl Tile { + #[new] + fn new(arr: Vec>) -> Self { + let mut parts = HashSet::new(); + for (y, row) in arr.iter().enumerate() { + for (x, val) in row.iter().enumerate() { + if *val != 0 { + parts.insert((x, y)); + } + } + } + let xs = parts + .iter() + .max_by(|x, y| x.0.cmp(&y.0)) + .unwrap_or(&(0, 0)) + .0; + let ys = parts + .iter() + .max_by(|x, y| x.1.cmp(&y.1)) + .unwrap_or(&(0, 0)) + .1; + Tile { + parts, + size: (xs, ys), + } + } + + fn permutations(&self) -> Vec { + let mut vfinal = Vec::new(); + for val in vec![ + self.clone(), + self.rotate(), + self.rotate().rotate(), + self.rotate().rotate().rotate(), + self.flip(), + self.rotate().flip(), + self.rotate().rotate().flip(), + self.rotate().rotate().rotate().flip(), + ] { + if !vfinal.contains(&val) { + vfinal.push(val); + } + } + vfinal + } + + fn flip(&self) -> Self { + let (xs, ys) = self.size; + let v = self.parts.iter().map(|&(i, j)| (xs - i, j)).collect(); + Tile { + parts: v, + size: (xs, ys), + } + } + + fn rotate(&self) -> Self { + let (xs, ys) = self.size; + let v = self + .parts + .iter() + .map(|&(i, j)| (j, self.size.0 - i)) + .collect(); + Tile { + parts: v, + size: (ys, xs), + } + } + } + + #[pyclass] + #[derive(Clone)] + struct Board { + tiles: [[Square; BOARD_SIZE]; BOARD_SIZE], + } + + impl Index<(usize, usize)> for Board { + type Output = Square; + + fn index(&self, index: (usize, usize)) -> &Self::Output { + &self.tiles[index.1][index.0] + } + } + + impl IndexMut<(usize, usize)> for Board { + fn index_mut(&mut self, index: (usize, usize)) -> &mut Self::Output { + &mut self.tiles[index.1][index.0] + } + } + + #[pymethods] + impl Board { + #[new] + fn new() -> Self { + let mut board = Board { + tiles: [[Square::Empty; BOARD_SIZE]; BOARD_SIZE], + }; + for sq in START_SQUARES { + board[sq] = Square::Start + } + board + } + + fn __len__(&self) -> PyResult { + Ok(BOARD_SIZE * BOARD_SIZE) + } + + fn __getitem__(&self, idx: (isize, isize)) -> PyResult { + let (x, y) = idx; + if x >= BOARD_SIZE as isize { + return Err(PyIndexError::new_err("x is larger than BOARD_SIZE")); + } + if y >= BOARD_SIZE as isize { + return Err(PyIndexError::new_err("y is larger than BOARD_SIZE")); + } + if x < -(BOARD_SIZE as isize) { + return Err(PyIndexError::new_err("x is smaller than -BOARD_SIZE")); + } + if y < -(BOARD_SIZE as isize) { + return Err(PyIndexError::new_err("y is smaller than -BOARD_SIZE")); + } + + let sq = self[( + ((x + BOARD_SIZE as isize) % BOARD_SIZE as isize) as usize, + ((y + BOARD_SIZE as isize) % BOARD_SIZE as isize) as usize, + )]; + Ok(match sq { + Square::Start => 3, + Square::Empty => 0, + Square::Player(Player::P1) => 1, + Square::Player(Player::P2) => 2, + }) + } + + fn place(&mut self, tile: Tile, pos: (usize, usize), player: Player) { + let (x, y) = pos; + for &(i, j) in tile.parts.iter() { + assert!( + self[(x + i, y + j)] == Square::Empty || self[(x + i, y + j)] == Square::Start, + "Can't put a tile in a place where it collides with another" + ); + self[(x + i, y + j)] = Square::Player(player) + } + } + + fn tile_placements(&self, tile: Tile, player: Player) -> Vec<(usize, usize)> { + let mut starting = false; + for sq in START_SQUARES { + if self[sq] == Square::Start { + starting = true; + break; + } + } + let mut possible_placements = Vec::new(); + for y in 0..BOARD_SIZE { + for x in 0..BOARD_SIZE { + let mut can_place = true; + let mut on_start = false; + 'inner: for &(i, j) in tile.parts.iter() { + if x + i >= BOARD_SIZE { + can_place = false; + break 'inner; + } + if y + j >= BOARD_SIZE { + can_place = false; + break 'inner; + } + if self[(x + i, y + j)] == Square::Start { + on_start = true; + } + if let Square::Player(_p) = self[(x + i, y + j)] { + can_place = false; + break 'inner; + } + if x + i >= 1 && self[(x + i - 1, y + j)] == Square::Player(player) { + can_place = false; + break 'inner; + } + if y + j >= 1 && self[(x + i, y + j - 1)] == Square::Player(player) { + can_place = false; + break 'inner; + } + if x + i + 1 < BOARD_SIZE + && self[(x + i + 1, y + j)] == Square::Player(player) + { + can_place = false; + break 'inner; + } + if y + j + 1 < BOARD_SIZE + && self[(x + i, y + j + 1)] == Square::Player(player) + { + can_place = false; + break 'inner; + } + } + if can_place { + if starting { + if on_start { + possible_placements.push((x, y)); + } + } else { + for &(i, j) in tile.parts.iter() { + if (x + i + 1 < BOARD_SIZE + && y + j + 1 < BOARD_SIZE + && self[(x + i + 1, y + j + 1)].is_player(player)) + || (x + i + 1 < BOARD_SIZE + && y + j >= 1 + && self[(x + i + 1, y + j - 1)].is_player(player)) + || (x + i >= 1 + && y + j + 1 < BOARD_SIZE + && self[(x + i - 1, y + j + 1)].is_player(player)) + || (x + i >= 1 + && y + j >= 1 + && self[(x + i - 1, y + j - 1)].is_player(player)) + { + possible_placements.push((x, y)); + break; + } + } + } + } + } + } + possible_placements + } + } +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b8a5887 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,4 @@ +[project] +name = "blokus" +requires-python = ">=3.8" + diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 0000000..71f286f --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,4 @@ +{ + "venv": ".venv", + "venvPath": "./" +}