From 9c9f70308e2b1bef6de6d6e3fb306f0f177dc607 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Tue, 21 Nov 2023 23:49:08 -0600 Subject: [PATCH 01/55] Initial WIP C bindings --- .gitignore | 1 + .gitmodules | 3 + tilewe/pyproject.toml => pyproject.toml | 5 + setup.py | 17 + tilewe/__init__.py | 1024 +---------------------- tilewe/__init__.pyi | 63 ++ tilewe/engine.py | 12 +- tilewe/setup.py | 3 - tilewe/src/ctilewemodule.c | 299 +++++++ tilewe/src/tilewe | 1 + tilewe/tournament.py | 15 +- 11 files changed, 410 insertions(+), 1033 deletions(-) create mode 100644 .gitmodules rename tilewe/pyproject.toml => pyproject.toml (78%) create mode 100644 setup.py create mode 100644 tilewe/__init__.pyi delete mode 100644 tilewe/setup.py create mode 100644 tilewe/src/ctilewemodule.c create mode 160000 tilewe/src/tilewe diff --git a/.gitignore b/.gitignore index 1204559..764c840 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ tmp/ +.vscode/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..08c1bd8 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tilewe/src/tilewe"] + path = tilewe/src/tilewe + url = https://github.com/nhamil/tilewe diff --git a/tilewe/pyproject.toml b/pyproject.toml similarity index 78% rename from tilewe/pyproject.toml rename to pyproject.toml index a533b8f..2b9153c 100644 --- a/tilewe/pyproject.toml +++ b/pyproject.toml @@ -16,3 +16,8 @@ classifiers = [ [project.urls] "Github" = "https://github.com/nhamil/python-tilewe" +"C Implementation" = "https://github.com/nhamil/tilewe" + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..281dc10 --- /dev/null +++ b/setup.py @@ -0,0 +1,17 @@ +from setuptools import setup, find_packages, Extension + +ext_modules = [ + Extension( + name="ctilewe", + sources=["tilewe/src/ctilewemodule.c", "tilewe/src/tilewe/Source/Tilewe/Piece.c", "tilewe/src/tilewe/Source/Tilewe/Tables.c"], + include_dirs=["tilewe/src/tilewe/Source", "tilewe/src/tilewe/Source/Tilewe"], + extra_compile_args=["-O3", "-funroll-loops"] + ) +] + +setup( + name='tilewe', + version='0.0.1', + packages=find_packages(), + ext_modules=ext_modules +) diff --git a/tilewe/__init__.py b/tilewe/__init__.py index 55301bc..5860acb 100644 --- a/tilewe/__init__.py +++ b/tilewe/__init__.py @@ -1,19 +1,15 @@ -from dataclasses import dataclass -import sys - -import numpy as np +import sys if sys.version_info[0] != 3 or sys.version_info[1] < 10: raise Exception("Requires Python 3.10+") -print_color = True +from ctilewe import * -# internally int, so copies value not reference Tile = int Piece = int Rotation = int Color = int -_PrpSet = int +Move = int # game details related constant declarations TILES = [ @@ -67,1017 +63,3 @@ "a19", "b19", "c19", "d19", "e19", "f19", "g19", "h19", "i19", "j19", "k19", "l19", "m19", "n19", "o19", "p19", "q19", "r19", "s19", "t19", # noqa: 501 "a20", "b20", "c20", "d20", "e20", "f20", "g20", "h20", "i20", "j20", "k20", "l20", "m20", "n20", "o20", "p20", "q20", "r20", "s20", "t20" # noqa: 501 ] - -def tile_to_coords(tile: Tile) -> tuple[int, int]: - return TILE_COORDS[tile] - -def coords_to_tile(coords: tuple[int, int]) -> Tile: - return coords[0] + coords[1] * 20 - -def tile_to_index(tile: Tile) -> int: - return tile - -def out_of_bounds(coords: tuple[int, int]) -> int: - return not (0 <= coords[0] < 20 and 0 <= coords[1] < 20) - -def in_bounds(coords: tuple[int, int]) -> bool: - return 0 <= coords[0] < 20 and 0 <= coords[1] < 20 - -ROTATIONS = [ - NORTH, EAST, SOUTH, WEST, NORTH_F, EAST_F, SOUTH_F, WEST_F -] = [Rotation(x) for x in range(8)] - -ROTATION_NAMES = [ - 'n', 'e', 's', 'w', 'nf', 'ef', 'sf', 'wf' -] - -COLORS = [ - BLUE, YELLOW, RED, GREEN -] = [Color(x) for x in range(4)] - -NO_COLOR: Color = -1 - -COLOR_NAMES = [ - 'blue', 'yellow', 'red', 'green' -] - -class _Piece: - """ - Internal/private representation of a game piece - including all rotations data. - - Parameters - ---------- - name : str - The name/identifier for the piece - id : Piece - The id for the piece - """ - - def __init__(self, name: str, id: Piece): - self.name = name - self.id = id - self.rotations: list[_PieceRotation] = [] - self.unique: list[bool] = [] - self.true_rot: list[Rotation] = [] - self.true_rot_for: list[list[Rotation]] = [] - -class _PieceRotation: - """ - Internal/private representation of a piece given a choice - of rotation. - - Parameters - ---------- - name : str - The name/identifier for this piece + rotation - pc : _Piece - A reference to the underlying internal piece data - rot : Rotation - Which rotation of the piece this piece-rotation has - shape : np.ndarray - The 2D matrix depicting the shape of this piece given the rotation - """ - - def __init__(self, name: str, pc: _Piece, rot: Rotation, shape: np.ndarray): - self.id = len(_PIECE_ROTATIONS) - self.piece = pc - self.rotation = rot - _PIECE_ROTATIONS.append(self) - - self.name = name - self.shape = np.array(shape, dtype=np.uint8) - - # coords relative to a1 of the rotated piece (regardless of if that's a valid contact) - self.rel_tiles: list[tuple[int, int]] = [] - self.rel_contacts: list[tuple[int, int]] = [] - - self.prps: dict[Tile, _PieceRotationPoint] = {} - self.n_corners = 0 - - self.contact_shape = np.zeros_like(shape, dtype=np.uint8) - H, W = shape.shape - for y in range(H): - for x in range(W): - # check each tile in piece - if shape[y, x] != 0: - self.rel_tiles.append((x, y)) - v_neighbors = 0 - h_neighbors = 0 - - if y > 0 and shape[y - 1, x] != 0: v_neighbors += 1 # noqa: E272, E701 - if y < H - 1 and shape[y + 1, x] != 0: v_neighbors += 1 # noqa: E701 - if x > 0 and shape[y, x - 1] != 0: h_neighbors += 1 # noqa: E272, E701 - if x < W - 1 and shape[y, x + 1] != 0: h_neighbors += 1 # noqa: E701 - - n_neighbors = v_neighbors + h_neighbors - - if (n_neighbors <= 1) or (v_neighbors == 1 and h_neighbors == 1): - self.rel_contacts.append((x, y)) - self.contact_shape[y, x] = 1 - - for coord in self.rel_contacts: - self.prps[coords_to_tile(coord)] = _PieceRotationPoint(name, self, coord) - - self.n_corners = len(list(self.prps.values())[0].rel_corners) - self.tiles = [coords_to_tile(t) for t in self.rel_tiles] - self.contacts = [coords_to_tile(t) for t in self.rel_contacts] - -class _PieceRotationPoint: - """ - Internal/private of a piece given a rotation and a contact - point on that piece. - - Parameters - ---------- - name : str - The name/identifier for this piece + rotation - rot : _PieceRotation - A reference to the underlying piece + rotation internal data - pt : Tile - Which contact of the piece is used by this piece-rotation-point - """ - - def __init__(self, name: str, rot: _PieceRotation, pt: tuple[int, int]): - self.id = len(_PIECE_ROTATION_POINTS) - self.as_set = 1 << self.id - global _PRP_SET_ALL - _PRP_SET_ALL |= self.as_set - self.piece = rot.piece - self.rotation = rot - self.piece_id = self.piece.id - self.name = name - self.contact = coords_to_tile(pt) - _PIECE_ROTATION_POINTS.append(self) - - dx, dy = pt - - # coords relative to the contact - self.rel_tiles: list[tuple[int, int]] = [] - self.rel_adjacent: set[tuple[int, int]] = set() - self.rel_corners: set[tuple[int, int]] = set() - - for x, y in rot.rel_tiles: - self.rel_tiles.append((x - dx, y - dy)) - - for x, y in self.rel_tiles: - for cy, cx in [(-1, 0), (1, 0), (0, -1), (0, 1)]: - rel = (x + cx, y + cy) - if rel not in self.rel_tiles: - self.rel_adjacent.add(rel) - - for x, y in self.rel_tiles: - for cx, cy in [(-1, -1), (1, -1), (-1, 1), (1, 1)]: - rel = (x + cx, y + cy) - if rel not in self.rel_tiles and rel not in self.rel_adjacent: - self.rel_corners.add(rel) - - self.rel_adjacent = sorted(list(self.rel_adjacent)) - self.rel_corners = sorted(list(self.rel_corners)) - -# internal global data for all game pieces -# initialized on library load one time below -N_PIECES = 0 -_PIECES: list[_Piece] = [] -_PIECE_ROTATIONS: list[_PieceRotation] = [] -_PIECE_ROTATION_POINTS: list[_PieceRotationPoint] = [] -_PRP_SET_ALL: _PrpSet = 0 - -def _create_piece(name: str, shape: list[list[int]]) -> Piece: - """ - Adds a piece to the game data, including all rotation and - horizontal flips of that piece. - - Parameters - ---------- - name : str - The name/identifier of the piece, used in notation - shape : list[list[int]] - A two dimensional matrix outlining the shape of the piece - - Returns - ------- - id : Piece - The new id assigned to this piece - """ - - global N_PIECES - id = N_PIECES - N_PIECES += 1 - pc = _Piece(name, id) - _PIECES.append(pc) - f_names = [] - - def add(suffix: str, arr: np.ndarray): - """ - Helper to add a variant of a piece, accounting - for uniqueness across rotations and horizontal flips. - """ - - rot = None - unique = True - cur_rot = len(pc.rotations) - true_rot = cur_rot # assume rotation is unique - - for x in pc.rotations: - if x.shape.shape == arr.shape and np.all(x.shape == arr): - rot = x - unique = False - true_rot = x.rotation - break - - if rot is None: - rot = _PieceRotation(name + suffix, pc, len(pc.rotations), arr) - - f_names.append(suffix + "f") - pc.rotations.append(rot) - pc.unique.append(unique) - pc.true_rot.append(true_rot) - pc.true_rot_for.append([]) - pc.true_rot_for[true_rot].append(cur_rot) - - # original shape, north - cur = np.array(shape, dtype=np.uint8)[::-1] - add("n", cur) - - # east - cur = np.rot90(cur, 1) - add("e", cur) - - # south - cur = np.rot90(cur, 1) - add("s", cur) - - # west - cur = np.rot90(cur, 1) - add("w", cur) - - # flipped - n_unflipped = len(pc.rotations) - for i in range(n_unflipped): - add(f_names[i], np.fliplr(pc.rotations[i].shape)) - - return id - -# setup the 21 piece shapes -O1 = _create_piece("O1", [ - [1] -]) - -I2 = _create_piece("I2", [ - [1], - [1] -]) - -I3 = _create_piece("I3", [ - [1], - [1], - [1] -]) -L3 = _create_piece("L3", [ - [1, 0], - [1, 1] -]) - -I4 = _create_piece("I4", [ - [1], - [1], - [1], - [1] -]) -L4 = _create_piece("L4", [ - [1, 0], - [1, 0], - [1, 1] -]) -Z4 = _create_piece("Z4", [ - [1, 1, 0], - [0, 1, 1] -]) -O4 = _create_piece("O4", [ - [1, 1], - [1, 1] -]) -T4 = _create_piece("T4", [ - [1, 1, 1], - [0, 1, 0] -]) - -F5 = _create_piece("F5", [ - [0, 1, 1], - [1, 1, 0], - [0, 1, 0] -]) -I5 = _create_piece("I5", [ - [1], - [1], - [1], - [1], - [1] -]) -L5 = _create_piece("L5", [ - [1, 0], - [1, 0], - [1, 0], - [1, 1] -]) -N5 = _create_piece("N5", [ - [0, 1], - [1, 1], - [1, 0], - [1, 0] -]) -P5 = _create_piece("P5", [ - [1, 1], - [1, 1], - [1, 0] -]) -T5 = _create_piece("T5", [ - [1, 1, 1], - [0, 1, 0], - [0, 1, 0] -]) -U5 = _create_piece("U5", [ - [1, 0, 1], - [1, 1, 1] -]) -V5 = _create_piece("V5", [ - [0, 0, 1], - [0, 0, 1], - [1, 1, 1] -]) -W5 = _create_piece("W5", [ - [0, 0, 1], - [0, 1, 1], - [1, 1, 0] -]) -X5 = _create_piece("X5", [ - [0, 1, 0], - [1, 1, 1], - [0, 1, 0] -]) -Y5 = _create_piece("Y5", [ - [0, 1], - [1, 1], - [0, 1], - [0, 1] -]) -Z5 = _create_piece("Z5", [ - [1, 1, 0], - [0, 1, 0], - [0, 1, 1] -]) - -def create_rel_tile(pt: tuple[int, int]) -> Tile: - return pt[0] + 32 + ((pt[1] + 32) << 6) - -_REL_TILE_COORDS = [ - (x - 32, y - 32) for y in range(64) for x in range(64) -] - -# compute relative coordinates for the pieces -_PRP_WITH_REL_COORD: list[_PrpSet] = [0] * (64 * 64) -for _pt in _PIECE_ROTATION_POINTS: - for _tile in _pt.rel_tiles: - _PRP_WITH_REL_COORD[create_rel_tile(_tile)] |= _pt.as_set - -_PRP_WITH_ADJ_REL_COORD: list[_PrpSet] = [0] * (64 * 64) -for _pt in _PIECE_ROTATION_POINTS: - for _tile in _pt.rel_adjacent: - _PRP_WITH_ADJ_REL_COORD[create_rel_tile(_tile)] |= _pt.as_set - -_PRP_REL_COORDS: list[Tile] = set() -for _i, _pt in enumerate(_PRP_WITH_REL_COORD): - if _pt: - _PRP_REL_COORDS.add(_i) -for _i, _pt in enumerate(_PRP_WITH_ADJ_REL_COORD): - if _pt: - _PRP_REL_COORDS.add(_i) -_PRP_REL_COORDS = list(_PRP_REL_COORDS) - -_PRP_WITH_PC_ID: list[_PrpSet] = [0] * N_PIECES -for _pt in _PIECE_ROTATION_POINTS: - _PRP_WITH_PC_ID[_pt.piece_id] |= _pt.as_set - -# helpers for retrieving information about game pieces -def n_piece_contacts(piece: Piece) -> int: - return len(_PIECES[piece].rotations[0].rel_contacts) - -def n_piece_tiles(piece: Piece) -> int: - return len(_PIECES[piece].rotations[0].rel_tiles) - -def n_piece_corners(piece: Piece) -> int: - return _PIECES[piece].rotations[0].n_corners - -def piece_tiles(piece: Piece, rotation: Rotation) -> list[Tile]: - return list(_PIECES[piece].rotations[rotation].tiles) - -def piece_tile_coords(piece: Piece, rotation: Rotation, contact: Tile=None) -> list[tuple[int, int]]: - if contact is None: - return list(_PIECES[piece].rotations[rotation].rel_tiles) - else: - return list(_PIECES[piece].rotations[rotation].prps[contact].rel_tiles) - -@dataclass -class _PlayerState: - """ - Internal/private minimal representation of a player's state data - that will be pushed on a stack as the player takes game actions. - - Parameters - ---------- - prps : _PrpSet - The player's current set of piece-rotation-points - corners: dict[Tile, _PrpSet] - The player's current set of piece-rotation-points for each open corner tile - has_player : bool - Whether or not the player has made a move yet - score : int - The player's current score in the active game - """ - - prps: _PrpSet - corners: dict[Tile, _PrpSet] - has_played: bool - score: int - - def copy(self) -> '_PlayerState': - out = _PlayerState.__new__(_PlayerState) - out.prps = self.prps - out.corners = dict(self.corners) - out.has_played = self.has_played - out.score = self.score - - return out - -class _Player: - """ - Internal/private representation of a player used - by a game Board instance. - - Parameters - ---------- - name : str - A name to identify the player by - id : Color - Which color the player is playing as, i.e. their turn order - borad : Board - A reference to the board the player is playing on - """ - - def __init__(self, name: str, id: Color, board: 'Board'): - self.name = name - self.id = id - self._prps = _PRP_SET_ALL - self.board = board - self._tiles = board._tiles - self.corners: dict[Tile, _PrpSet] = {} - self.has_played = False - self.score = 0 - self._state: list[_PlayerState] = [] - self._tgt = id + 1 - - # add the 4 initial corners of the board at game start - # since each player's first move has this rule exception - self.add_corner(A01) - self.add_corner(A20) - self.add_corner(T01) - self.add_corner(T20) - - def copy_current_state(self, board: 'Board') -> '_Player': - out = _Player.__new__(_Player) - out.name = self.name - out.id = self.id - out._prps = self._prps - out.board = board - out._tiles = board._tiles - out.corners = dict(self.corners) - out.has_played = self.has_played - out.score = self.score - out._state = [] - out._tgt = self._tgt - - return out - - @property - def can_play(self) -> bool: - return len(self.corners) > 0 - - def push_state(self) -> None: - prps = self._prps - corners = dict(self.corners) - - self._state.append(_PlayerState(prps, corners, self.has_played, self.score)) - - def pop_state(self) -> bool: - state = self._state.pop() - - self._prps = state.prps - self.corners = state.corners - self.has_played = state.has_played - self.score = state.score - - def remove_piece(self, piece_id: int) -> None: - # remove piece permutations from availability list - not_prps = ~_PRP_WITH_PC_ID[piece_id] - self._prps &= not_prps - - remove = [] - - # remove piece permutations from all open corners - for key, corner in self.corners.items(): - corner &= not_prps - if corner == 0: - remove.append(key) - else: - # corner is value, not reference - self.corners[key] = corner - - for r in remove: - del self.corners[r] - - def on_tiles_filled(self, tiles: list[Tile]) -> None: - # if an open corner was filled, no player can use it anymore - for tile in tiles: - self.corners.pop(tile, None) - - remove = [] - - # for each open corner, - # find all piece permutations that need one of the filled tiles - # and remove them from possible moves - for corner, prps in self.corners.items(): - cy, cx = tile_to_coords(corner) - invalid: _PrpSet = 0 - for tile in tiles: - c = TILE_COORDS[tile] - rel = create_rel_tile((c[0] - cy, c[1] - cx)) - invalid |= _PRP_WITH_REL_COORD[rel] - prps &= ~invalid - if prps == 0: - remove.append(corner) - else: - self.corners[corner] = prps - - for r in remove: - del self.corners[r] - - def add_corner(self, tile: Tile) -> None: - if tile in self.corners: - return - - bad: _PrpSet = ~_PRP_SET_ALL - - tgt = self._tgt - x, y = tile_to_coords(tile) - - for rel in _PRP_REL_COORDS: - pt = _REL_TILE_COORDS[rel] - pt = (pt[0] + x, pt[1] + y) - t = pt[0] + pt[1] * 20 - ib = in_bounds(pt) - - if ib: - if col := self._tiles[t]: - bad |= _PRP_WITH_REL_COORD[rel] - if not (col - tgt): - bad |= _PRP_WITH_ADJ_REL_COORD[rel] - else: - bad |= _PRP_WITH_REL_COORD[rel] - - prps = self._prps & ~bad - if prps: - self.corners[tile] = prps - -class Move: - """ - Public representation of a Move to be played on a game board. - - Parameters - ---------- - piece : Piece - Which of the 21 basic piece shapes is used in this Move - rotation : Rotation - Which of the 8 rotations (4 normal, 4 flipped) is used in this Move - contact : Tile - Which square on the piece is being placed at an open corner in this Move - to_tile : Tile - Which square on the board the contact is being placed at by this Move - """ - - def __init__(self, piece: Piece, rotation: Rotation, contact: Tile, to_tile: Tile): - self.piece = piece - self.rotation = rotation - self.contact = contact - self.to_tile = to_tile - - def __str__(self): - return _PIECES[self.piece].name + \ - ROTATION_NAMES[self.rotation] + \ - "-" + \ - TILE_NAMES[self.contact] + \ - TILE_NAMES[self.to_tile] - - def __hash__(self): - # adds support for using Move objects in sets - return self.piece * 2659 + \ - self.rotation * 5393 + \ - self.contact * 571 + \ - self.to_tile * 1607 - - def is_equal(self, value: 'Move') -> bool: - return \ - self.piece == value.piece and \ - self.rotation == value.rotation and \ - self.contact == value.contact and \ - self.to_tile == value.to_tile - - def __eq__(self, value: object) -> bool: - if isinstance(value, Move): - return self.is_equal(value) - else: - return False - - def to_unique(self) -> 'Move': - """ - If a Move has several rotations/flips that result in the same move - this function helps simplify them to one version. - """ - - return Move( - self.piece, - _PIECES[self.piece].true_rot[self.rotation], - self.contact, - self.to_tile - ) - -@dataclass -class _BoardState: - """ - Internal/private minimal representation of a board's state data - that will be pushed on a stack as actions are taken in the game. - - Parameters - ---------- - cur_player : int - Which player's turn it is in the game - tiles : list[Tile] - The list of all Tile objects on the board - """ - - cur_player: int - tiles: list[Tile] - -class Board: - """ - Public representation of a game Board which is how - players or bots interface with the game. - - Parameters - ---------- - n_players : int - The number of players participating in the game on this board - """ - - def __init__(self, n_players: int): - if n_players < 1 or n_players > 4: - raise Exception("Number of players must be between 1 and 4") - - self._state: list[_BoardState] = [] - self._tiles = np.zeros((400,), dtype=np.uint8) - self._n_players = n_players - self._players: list[_Player] = [] - - chars = [ - 'B', - 'Y', - 'R', - 'G' - ] - for i in range(n_players): - self._players.append(_Player(chars[i], i, self)) - - self.current_player: Color = BLUE - self.finished = False - self.ply = 0 - self.moves: list[Move] = [] - - def copy_current_state(self) -> 'Board': - out = Board.__new__(Board) - out._state = [] - out._tiles = np.copy(self._tiles) - out._n_players = self._n_players - out._players = [p.copy_current_state(out) for p in self._players] - out.current_player = self.current_player - out.finished = self.finished - out.ply = self.ply - out.moves = [] - - return out - - @property - def n_players(self) -> int: - return self._n_players - - @property - def scores(self) -> list[int]: - return [p.score for p in self._players] - - @property - def winners(self) -> list[int]: - if not self.finished: - return None - - best = [0] - for i in range(1, len(self._players)): - if self._players[i].score == self._players[best[0]].score: - best.append(i) - elif self._players[i].score > self._players[best[0]].score: - best = [i] - return best - - def player_corners(self, player: Color) -> list[Tile]: - return list(self._players[player].corners.keys()) - - def n_player_corners(self, player: Color) -> int: - return len(self._players[player].corners) - - def player_score(self, player: Color) -> int: - return self._players[player].score - - def can_play(self, player: Color) -> bool: - return self._players[player].can_play - - def _remaining_piece_set(self, player: Color) -> set[Piece]: - pieces = set() - prps = self._players[player]._prps - - while prps: - # get least significant bit - prp = (prps & -prps).bit_length() - 1 - # remove it so the next LSB is another PRP - prps ^= 1 << prp - - pieces.add(_PIECE_ROTATION_POINTS[prp].piece_id) - - return pieces - - def remaining_pieces(self, player: Color) -> list[Piece]: - return list(self._remaining_piece_set(player)) - - def n_remaining_pieces(self, player: Color) -> int: - return len(self._remaining_piece_set(player)) - - def color_at(self, tile: Tile) -> Color: - return Color(self._tiles[tile] - 1) - - def _is_legal(self, prp_id: int, tile: Tile, player: Color=None) -> bool: - if player is None: - player = self.current_player - - player = self._players[player] - prps = player.corners.get(tile, 0) - - return (prps & (1 << prp_id)) != 0 - - def is_legal(self, move: Move, for_player: Color=None) -> bool: - player = self._players[self.current_player if for_player is None else for_player] - - # target tile must be empty - if move.to_tile is None or self.color_at(move.to_tile) != NO_COLOR: - return False - - # piece must be real - if move.piece is None or move.piece >= len(_PIECES) or move.piece < 0: - return False - pc = _PIECES[move.piece] - - # rotation must be real - if move.rotation is None or move.rotation >= len(ROTATIONS) or move.rotation < 0: - return False - pc_rot = pc.rotations[move.rotation] - - # piece rotation must have the contact - prp = pc_rot.prps.get(move.contact, None) - if prp is None: - return False - - # available permutations at the requested tile - prps = player.corners.get(move.to_tile, None) - if prps is None: - return False - - # permutation must fit at the corner square - return (prps & prp.as_set) != 0 - - def n_legal_moves(self, unique: bool=True, for_player: Color=None) -> int: - player = self._players[self.current_player if for_player is None else for_player] - total = 0 - - if unique: - for prps in player.corners.values(): - total += prps.bit_count() - else: - for prps in player.corners.values(): - while prps: - # get least significant bit - prp_id = (prps & -prps).bit_length() - 1 - # remove it so the next LSB is another PRP - prps ^= 1 << prp_id - - prp = _PIECE_ROTATION_POINTS[prp_id] - - # include all rotations that are equivalent to this PRP - total += len(prp.piece.true_rot_for[prp.rotation.rotation]) - - return total - - def generate_legal_moves(self, unique: bool=True, for_player: Color=None) -> list[Move]: - moves: list[Move] = [] - player = self._players[self.current_player if for_player is None else for_player] - - # duplicate for loop so that we don't check the if statement for every permutation - if unique: - for to_sq, prps in player.corners.items(): - while prps: - # get least significant bit - prp_id = (prps & -prps).bit_length() - 1 - # remove it so the next LSB is another PRP - prps ^= 1 << prp_id - - prp = _PIECE_ROTATION_POINTS[prp_id] - - moves.append(Move( - prp.piece.id, - prp.rotation.rotation, - prp.contact, - to_sq - )) - else: - for to_sq, prps in player.corners.items(): - while prps: - # get least significant bit - prp_id = (prps & -prps).bit_length() - 1 - # remove it so the next LSB is another PRP - prps ^= 1 << prp_id - - prp = _PIECE_ROTATION_POINTS[prp_id] - - # include permutations with non-unique rotations/flips - for rot in prp.piece.true_rot_for[prp.rotation.rotation]: - moves.append(Move( - prp.piece.id, - rot, - prp.contact, - to_sq - )) - - return moves - - def push_null(self) -> None: - self._state.append(_BoardState(self._incr_player(), None)) - - def pop_null(self) -> None: - state = self._state.pop() - self.current_player = state.cur_player - self.finished = False - self.ply -= 1 - - def push(self, move: Move) -> None: - """ - Legality is assumed to be true. - """ - prp = _PIECES[move.piece].rotations[move.rotation].prps[move.contact] - self._push_prp(move, prp, move.to_tile) - - def pop(self) -> None: - state = self._state.pop() - self.moves.pop() - - # take back piece - for tile in state.tiles: - self._tiles[tile] = 0 - - self.current_player = state.cur_player - self.finished = False - self.ply -= 1 - - # player state is stored per player - for player in self._players: - player.pop_state() - - def _incr_player(self) -> Color: - self.ply += 1 - cur_turn = self.current_player - while True: - self.current_player += 1 - if self.current_player >= len(self._players): - self.current_player = 0 - if self._players[self.current_player].can_play: - break - - # looped all the way back to same player - # no player can play, game is done - if cur_turn == self.current_player: - self.finished = True - break - - # return last player - return cur_turn - - def _push_prp(self, move: Move, prp: _PieceRotationPoint, tile: Tile) -> None: - self.moves.append(move) - - player = self._players[self.current_player] - - for p in self._players: - p.push_state() - - # absolute position of tiles - tx, ty = TILE_COORDS[tile] - tiles = [coords_to_tile((t[0] + tx, t[1] + ty)) for t in prp.rel_tiles] - corners = [(t[0] + tx, t[1] + ty) for t in prp.rel_corners] - adj = [(t[0] + tx, t[1] + ty) for t in prp.rel_adjacent] - - corners = [coords_to_tile(c) for c in corners if in_bounds(c)] - adj = [coords_to_tile(c) for c in adj if in_bounds(c)] - - for abs_tile in corners: - player.add_corner(abs_tile) - - for abs_tile in tiles: - self._tiles[abs_tile] = self.current_player + 1 - - for p in self._players: - p.on_tiles_filled(tiles) - - # player can't place tiles adjacent to their own color - player.on_tiles_filled(adj) - player.remove_piece(prp.piece_id) - - if not player.has_played: - player.corners.pop(A01, None) - player.corners.pop(A20, None) - player.corners.pop(T01, None) - player.corners.pop(T20, None) - - player.has_played = True - player.score += len(prp.rel_tiles) - - # inc turn and make sure player can move - cur_turn = self._incr_player() - - self._state.append(_BoardState(cur_turn, tiles)) - - def __str__(self): - out = "" - - board = self._tiles.reshape((20, 20))[::-1] - - chars = None - if print_color: - chars = [ - '.', - '\033[94mB\033[0m', - '\033[93mY\033[0m', - '\033[91mR\033[0m', - '\033[92mG\033[0m' - ] - else: - chars = [ - '.', - 'B', - 'Y', - 'R', - 'G' - ] - - for y in range(20): - for x in range(20): - out += f"{chars[board[y,x]]} " - out += "\n" - - for player in self._players: - out += f"{player.name}: {int(player.score)} " - - pcs = [_PIECES[pc] for pc in self._remaining_piece_set(player.id)] - - if len(pcs) > 0: - pcs = sorted(pcs, key=lambda x: x.id) - - out += "( " - for pc in pcs: - out += pc.name + " " - out += ")\n" - else: - out += "( )\n" - - out += f"Finished: {self.finished}" - if self.finished: - out += "\nWinner: " - for p in self.winners: - out += f"{self._players[p].name} " - else: - out += f"\nTurn: {self._players[self.current_player].name}" - - return out diff --git a/tilewe/__init__.pyi b/tilewe/__init__.pyi new file mode 100644 index 0000000..b6e6367 --- /dev/null +++ b/tilewe/__init__.pyi @@ -0,0 +1,63 @@ +import tilewe + +Tile = int +Piece = int +Rotation = int +Color = int +Move = int + +class Board: + + def __init__(self, n_players: int=4): + ... + + def __str__(self) -> str: + ... + + @property + def ply(self) -> int: + ... + + @property + def current_player(self) -> Color: + """Color of the current player""" + ... + + @property + def cur_player(self) -> Color: + """Color of the current player""" + ... + + @property + def n_players(self) -> int: + """Number of players""" + ... + + @property + def scores(self) -> list[int]: + ... + + @property + def winners(self) -> list[int]: + ... + + @property + def finished(self) -> bool: + """Whether the game is done""" + ... + + def generate_legal_moves(self, for_player: tilewe.Color=None) -> list[tilewe.Move]: + """Generates moves""" + ... + + def push(self, move: tilewe.Move) -> None: + """Play a move""" + ... + + def pop(self) -> None: + """Undo a move""" + ... + + def n_legal_moves(self, for_player: Color=None) -> int: + """Gets total number of legal moves for a player""" + ... diff --git a/tilewe/engine.py b/tilewe/engine.py index e704c7a..8f956cc 100644 --- a/tilewe/engine.py +++ b/tilewe/engine.py @@ -69,7 +69,7 @@ def __init__(self, name: str="Random"): super().__init__(name) def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: - return random.choice(board.generate_legal_moves(unique=True)) + return random.choice(board.generate_legal_moves()) class MostOpenCornersEngine(Engine): """ @@ -83,7 +83,7 @@ def __init__(self, name: str="MostOpenCorners"): super().__init__(name) def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: - moves = board.generate_legal_moves(unique=True) + moves = board.generate_legal_moves() random.shuffle(moves) player = board.current_player @@ -110,7 +110,7 @@ def __init__(self, name: str="LargestPiece"): super().__init__(name) def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: - moves = board.generate_legal_moves(unique=True) + moves = board.generate_legal_moves() random.shuffle(moves) best = max(moves, key=lambda m: @@ -135,7 +135,7 @@ def __init__(self, name: str="MaximizeMoveDifference"): super().__init__(name) def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: - moves = board.generate_legal_moves(unique=True) + moves = board.generate_legal_moves() random.shuffle(moves) player = board.current_player @@ -144,7 +144,7 @@ def eval_after_move(m: tilewe.Move) -> int: with MoveExecutor(board, m): total = 0 for color in range(board.n_players): - n_moves = board.n_legal_moves(unique=True, for_player=color) + n_moves = board.n_legal_moves(for_player=color) total += n_moves * (1 if color == player else -1) return total @@ -245,7 +245,7 @@ def evaluate_move_weight(move: tilewe.Move) -> float: return total - moves = board.generate_legal_moves(unique=True) + moves = board.generate_legal_moves() random.shuffle(moves) if board.ply < board.n_players: diff --git a/tilewe/setup.py b/tilewe/setup.py deleted file mode 100644 index 51a6afa..0000000 --- a/tilewe/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup, find_packages - -setup(name='tilewe', version='0.0.1', packages=find_packages()) diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c new file mode 100644 index 0000000..caa5060 --- /dev/null +++ b/tilewe/src/ctilewemodule.c @@ -0,0 +1,299 @@ +#define PY_SSIZE_T_CLEAN +#include + +#include + +#include "Tilewe/Tilewe.h" + +typedef struct BoardObject BoardObject; + +struct BoardObject +{ + PyObject_HEAD + Tw_Board Board; +}; + +static int Board_init(BoardObject* self, PyObject* args, PyObject* kwds) +{ + static char* kwlist[] = { "n_players", NULL }; + + int numPlayers = 4; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i", kwlist, &numPlayers)) + { + return -1; + } + + if (numPlayers < 1 || numPlayers > 4) + { + PyErr_SetString(PyExc_AttributeError, "n_players must be between 1 and 4"); + return -1; + } + + Tw_InitBoard(&self->Board, numPlayers); + return 0; +} + +static PyObject* Board_CurrentPlayer(BoardObject* self, void* closure) +{ + return PyBool_FromLong(self->Board.CurTurn); +} + +static PyObject* Board_NumPlayers(BoardObject* self, void* closure) +{ + return PyLong_FromLong(self->Board.NumPlayers); +} + +static PyObject* Board_Finished(BoardObject* self, void* closure) +{ + return PyBool_FromLong(self->Board.Finished); +} + +static PyObject* Board_Ply(BoardObject* self, void* closure) +{ + return PyLong_FromLong(self->Board.Ply); +} + +static PyObject* Board_Moves(BoardObject* self, void* closure) +{ + PyObject* list = PyList_New((unsigned) self->Board.Ply); + + for (int i = 0; i < self->Board.Ply; i++) + { + PyList_SetItem( + list, + i, + PyLong_FromUnsignedLong(self->Board.History[i].Move) + ); + } + + return list; +} + +static PyObject* Board_Scores(BoardObject* self, void* closure) +{ + PyObject* list = PyList_New((unsigned) self->Board.NumPlayers); + + for (int i = 0; i < self->Board.NumPlayers; i++) + { + PyList_SetItem( + list, + i, + PyLong_FromLong(self->Board.Players[i].Score) + ); + } + + return list; +} + +static PyObject* Board_Winners(BoardObject* self, void* closure) +{ + PyObject* list = PyList_New(0); + + int best = -1; + for (int i = 0; i < self->Board.NumPlayers; i++) + { + int score = self->Board.Players[i].Score; + + if (score > best) + { + PyList_SetSlice(list, 0, PyList_Size(list), NULL); + best = score; + } + + if (score == best) + { + PyList_Append(list, PyLong_FromLong(i)); + } + } + + return list; +} + +static PyObject* Board_GenMoves(BoardObject* self, PyObject* Py_UNUSED(ignored)) +{ + Tw_MoveList moves; + Tw_InitMoveList(&moves); + Tw_Board_GenMoves(&self->Board, &moves); + + PyObject* list = PyList_New(moves.Count); + + for (int i = 0; i < moves.Count; i++) + { + PyList_SetItem( + list, + i, + PyLong_FromUnsignedLong((unsigned long) moves.Elements[i]) + ); + } + + return list; +} + +static PyObject* Board_Push(BoardObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = + { + "move", + NULL + }; + + unsigned long long move; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "L", kwlist, &move)) + { + return NULL; + } + + Tw_Board_Push(&self->Board, (Tw_Move) move); + + Py_RETURN_NONE; +} + +static PyObject* Board_ColorAt(BoardObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = + { + "tile", + NULL + }; + + unsigned long tile; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "I", kwlist, &tile)) + { + return NULL; + } + + return PyLong_FromUnsignedLong(Tw_Board_ColorAt(&self->Board, tile)); +} + +static PyObject* Board_NumLegalMoves(BoardObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = + { + "for_player", + NULL + }; + + int player = -1; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i", kwlist, &player)) + { + return NULL; + } + + if (player < 0) + { + return PyLong_FromLong(Tw_Board_NumMoves(&self->Board)); + } + else + { + return PyLong_FromLong(Tw_Board_NumMovesForPlayer(&self->Board, (Tw_Color) player)); + } +} + +static PyObject* Board_Pop(BoardObject* self, PyObject* Py_UNUSED(ignored)) +{ + Tw_Board_Pop(&self->Board); + Py_RETURN_NONE; +} + +static PyObject* Board_str(BoardObject* self, PyObject* Py_UNUSED(ignored)) +{ + char buf[Tw_BoardStrSize]; + Tw_Board_ToStr(&self->Board, buf); + + return PyUnicode_FromString(buf); +} + +static PyObject* Tilewe_PlayRandomGame(PyObject* self, PyObject* args) +{ + Tw_Board board[1]; + Tw_MoveList moves[1]; + Tw_InitBoard(board, 4); + + while (!board->Finished) + { + Tw_InitMoveList(moves); + Tw_Board_GenMoves(board, moves); + Tw_Board_Push(board, moves->Elements[rand() % moves->Count]); + } + + Tw_Board_Print(board); + + Py_RETURN_NONE; +} + +static PyGetSetDef Board_getsets[] = +{ + { "moves", Board_Moves, NULL, "Move history", NULL }, + { "finished", Board_Finished, NULL, "Whether the game is done", NULL }, + { "n_players", Board_NumPlayers, NULL, "Number of players", NULL }, + { "current_player", Board_CurrentPlayer, NULL, "Color of the current player", NULL }, + { "cur_player", Board_CurrentPlayer, NULL, "Color of the current player", NULL }, + { "ply", Board_Ply, NULL, "Current board ply", NULL }, + { "scores", Board_Scores, NULL, "Scores of all players", NULL }, + { "winners", Board_Winners, NULL, "Gets list of player indices who have the highest score", NULL }, + { NULL } +}; + +static PyMethodDef Board_methods[] = +{ + { "generate_legal_moves", Board_GenMoves, METH_NOARGS, "Returns a list of legal moves" }, + { "gen_moves", Board_GenMoves, METH_NOARGS, "Returns a list of legal moves" }, + { "push", Board_Push, METH_VARARGS | METH_KEYWORDS, "Plays a move" }, + { "pop", Board_Pop, METH_NOARGS, "Undoes a move" }, + { "color_at", Board_ColorAt, METH_VARARGS | METH_KEYWORDS, "Color that claimed the tile" }, + { "n_legal_moves", Board_NumLegalMoves, METH_VARARGS | METH_KEYWORDS, "Gets total number of legal moves for a player" }, + // { "copy", Board_Copy, METH_NOARGS, "Returns a clone of the current board state" }, + { NULL } +}; + +static PyTypeObject BoardType = +{ + .ob_base = PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "ctilewe.Board", + .tp_doc = PyDoc_STR("Public representation of a game Board which is how players or bots interface with the game."), + .tp_basicsize = sizeof(BoardObject), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = PyType_GenericNew, + .tp_init = Board_init, + .tp_str = Board_str, + .tp_getset = Board_getsets, + .tp_methods = Board_methods +}; + +static PyMethodDef TileweMethods[] = +{ + { "play_random_game", Tilewe_PlayRandomGame, METH_NOARGS, "Plays a random game" }, + { NULL, NULL, 0, NULL } +}; + +static PyModuleDef TileweModule = +{ + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "ctilewe", + .m_doc = "Contains Tilewe implementation", + .m_size = -1, + .m_methods = TileweMethods +}; + +PyMODINIT_FUNC PyInit_ctilewe(void) +{ + PyObject* m; + + if (PyType_Ready(&BoardType) < 0) return NULL; + + if (!(m = PyModule_Create(&TileweModule))) return NULL; + + Py_INCREF(&BoardType); + if (PyModule_AddObject(m, "Board", (PyObject*) &BoardType) < 0) + { + Py_DECREF(&BoardType); + Py_DECREF(m); + return NULL; + } + + return m; +} diff --git a/tilewe/src/tilewe b/tilewe/src/tilewe new file mode 160000 index 0000000..4683466 --- /dev/null +++ b/tilewe/src/tilewe @@ -0,0 +1 @@ +Subproject commit 4683466ea0de62e9d60dedc243b4abdce77e938a diff --git a/tilewe/tournament.py b/tilewe/tournament.py index 157f430..4c28b87 100644 --- a/tilewe/tournament.py +++ b/tilewe/tournament.py @@ -278,7 +278,10 @@ def get_engine_rankings() -> str: with multiprocessing.Pool(n_threads, initializer=init_func, initargs=init_args) as pool: try: - for winners, scores, board, player_to_engine, time_sec in pool.imap_unordered(self._play_game, args): + for winners, scores, moves, player_to_engine, time_sec in pool.imap_unordered(self._play_game, args): + board = tilewe.Board(len(player_to_engine)) + for move in moves: + board.push(move) if len(winners) > 0: # at least one player always wins, if none then game crashed total_games += 1 for p in player_to_engine: @@ -380,7 +383,13 @@ def _play_game(self, player_to_engine: list[int]) -> tuple[list[int], list[int], engine_to_player = { value: key for key, value in enumerate(player_to_engine) } while not board.finished: engine = self.engines[player_to_engine[board.current_player]] - move = engine.search(board.copy_current_state(), self.move_seconds) + + b = tilewe.Board(n_players=len(player_to_engine)) + for move in board.moves: + b.push(move) + + move = engine.search(b, self.move_seconds) + # move = engine.search(board.copy_current_state(), self.move_seconds) # TODO test legality board.push(move) end_time = time.time() @@ -389,7 +398,7 @@ def _play_game(self, player_to_engine: list[int]) -> tuple[list[int], list[int], winners = [ player_to_engine[x] for x in board.winners ] scores = [ board.scores[engine_to_player[i]] if i in engine_to_player else 0 for i in range(len(self.engines)) ] - return winners, scores, board, player_to_engine, end_time - start_time + return winners, scores, board.moves, player_to_engine, end_time - start_time except BaseException: traceback.print_exc() From 7dffaae26271da3c5f5588cc04b0af87fd8e55ab Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Wed, 22 Nov 2023 00:16:10 -0600 Subject: [PATCH 02/55] Fix argument checking for n_legal_moves --- tilewe/src/ctilewemodule.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index caa5060..a63f248 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -175,21 +175,25 @@ static PyObject* Board_NumLegalMoves(BoardObject* self, PyObject* args, PyObject NULL }; - int player = -1; + int player = Tw_Color_None; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i", kwlist, &player)) { return NULL; } - if (player < 0) + if (player == Tw_Color_None) { - return PyLong_FromLong(Tw_Board_NumMoves(&self->Board)); + player = self->Board.CurTurn; } - else + + if (player >= 0 && player < self->Board.NumPlayers) { return PyLong_FromLong(Tw_Board_NumMovesForPlayer(&self->Board, (Tw_Color) player)); } + + PyErr_SetString(PyExc_AttributeError, "for_player must be valid or None"); + return NULL; } static PyObject* Board_Pop(BoardObject* self, PyObject* Py_UNUSED(ignored)) From d5570d60225443faf06d2428d2b0b31b871acc21 Mon Sep 17 00:00:00 2001 From: Michael Conard Date: Wed, 22 Nov 2023 01:21:48 -0500 Subject: [PATCH 03/55] add Board_NumPlayerPcs C function for board.n_remaining_pieces --- tilewe/__init__.pyi | 4 ++++ tilewe/src/ctilewemodule.c | 30 ++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/tilewe/__init__.pyi b/tilewe/__init__.pyi index b6e6367..7412052 100644 --- a/tilewe/__init__.pyi +++ b/tilewe/__init__.pyi @@ -61,3 +61,7 @@ class Board: def n_legal_moves(self, for_player: Color=None) -> int: """Gets total number of legal moves for a player""" ... + + def n_remaining_pieces(self, for_player: Color=None) -> int: + """Gets total number of pieces remaining for a player""" + ... diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index a63f248..28c7144 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -196,6 +196,35 @@ static PyObject* Board_NumLegalMoves(BoardObject* self, PyObject* args, PyObject return NULL; } +static PyObject* Board_NumPlayerPcs(BoardObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = + { + "for_player", + NULL + }; + + int player = Tw_Color_None; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i", kwlist, &player)) + { + return NULL; + } + + if (player == Tw_Color_None) + { + player = self->Board.CurTurn; + } + + if (player >= 0 && player < self->Board.NumPlayers) + { + return PyLong_FromLong(Tw_Board_NumPlayerPcs(&self->Board, (Tw_Color) player)); + } + + PyErr_SetString(PyExc_AttributeError, "for_player must be valid or None"); + return NULL; +} + static PyObject* Board_Pop(BoardObject* self, PyObject* Py_UNUSED(ignored)) { Tw_Board_Pop(&self->Board); @@ -249,6 +278,7 @@ static PyMethodDef Board_methods[] = { "pop", Board_Pop, METH_NOARGS, "Undoes a move" }, { "color_at", Board_ColorAt, METH_VARARGS | METH_KEYWORDS, "Color that claimed the tile" }, { "n_legal_moves", Board_NumLegalMoves, METH_VARARGS | METH_KEYWORDS, "Gets total number of legal moves for a player" }, + { "n_remaining_pieces", Board_NumPlayerPcs, METH_VARARGS | METH_KEYWORDS, "Gets total number of pieces remaining for a player" }, // { "copy", Board_Copy, METH_NOARGS, "Returns a clone of the current board state" }, { NULL } }; From b60a08b317de00c321ab3e4817cf0a1c6c55c0e9 Mon Sep 17 00:00:00 2001 From: Michael Conard Date: Wed, 22 Nov 2023 16:04:44 -0500 Subject: [PATCH 04/55] in submodule: add Tile.h to Tilewe.h, refactor for_each macros --- tilewe/src/tilewe | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tilewe/src/tilewe b/tilewe/src/tilewe index 4683466..1b6c595 160000 --- a/tilewe/src/tilewe +++ b/tilewe/src/tilewe @@ -1 +1 @@ -Subproject commit 4683466ea0de62e9d60dedc243b4abdce77e938a +Subproject commit 1b6c595e389bebe3c54886578888551e52fbcbcf From 55da6980fba0490b4603efaa82d223613275270e Mon Sep 17 00:00:00 2001 From: Michael Conard Date: Wed, 22 Nov 2023 16:05:36 -0500 Subject: [PATCH 05/55] fix engine selection in tournament, rename board copy --- tilewe/tournament.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tilewe/tournament.py b/tilewe/tournament.py index c01f46a..18dc433 100644 --- a/tilewe/tournament.py +++ b/tilewe/tournament.py @@ -418,14 +418,14 @@ def _play_game(self, player_to_engine: list[int]) -> tuple[list[int], list[int], try: engine_to_player = { value: key for key, value in enumerate(player_to_engine) } while not board.finished: - engine = self.engines[player_to_engine[board.current_player]] # ghetto board copy to avoid exposing the real board to the engine - b = tilewe.Board(n_players=len(player_to_engine)) + board_copy = tilewe.Board(n_players=len(player_to_engine)) for move in board.moves: - b.push(move) + board_copy.push(move) + engine = self.engines[player_to_engine[board_copy.current_player]] - move = engine.search(b, self.move_seconds) + move = engine.search(board_copy, self.move_seconds) # move = engine.search(board.copy_current_state(), self.move_seconds) # TODO test legality board.push(move) From 28aa4597a1928b8c1bb5bef5ebd5cdd0d0c333c0 Mon Sep 17 00:00:00 2001 From: Michael Conard Date: Wed, 22 Nov 2023 16:06:21 -0500 Subject: [PATCH 06/55] implement n_player_corners with Board_NumPlayerOpenCorners C API func --- tilewe/__init__.pyi | 5 +++++ tilewe/src/ctilewemodule.c | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/tilewe/__init__.pyi b/tilewe/__init__.pyi index 7412052..c36918b 100644 --- a/tilewe/__init__.pyi +++ b/tilewe/__init__.pyi @@ -65,3 +65,8 @@ class Board: def n_remaining_pieces(self, for_player: Color=None) -> int: """Gets total number of pieces remaining for a player""" ... + + def n_player_corners(self, for_player: Color=None) -> int: + """Gets total number of open corners for a player""" + ... + \ No newline at end of file diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index 28c7144..b099457 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -225,6 +225,40 @@ static PyObject* Board_NumPlayerPcs(BoardObject* self, PyObject* args, PyObject* return NULL; } +static PyObject* Board_NumPlayerOpenCorners(BoardObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = + { + "for_player", + NULL + }; + + int player = Tw_Color_None; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i", kwlist, &player)) + { + return NULL; + } + + if (player == Tw_Color_None) + { + player = self->Board.CurTurn; + } + + if (player >= 0 && player < self->Board.NumPlayers) + { + long openCorners = 0; + Tw_TileSet_FOR_EACH(self->Board.Players[player].OpenCorners.Keys, tile, + { + openCorners++; + }); + return PyLong_FromLong(openCorners); + } + + PyErr_SetString(PyExc_AttributeError, "for_player must be valid or None"); + return NULL; +} + static PyObject* Board_Pop(BoardObject* self, PyObject* Py_UNUSED(ignored)) { Tw_Board_Pop(&self->Board); @@ -279,6 +313,7 @@ static PyMethodDef Board_methods[] = { "color_at", Board_ColorAt, METH_VARARGS | METH_KEYWORDS, "Color that claimed the tile" }, { "n_legal_moves", Board_NumLegalMoves, METH_VARARGS | METH_KEYWORDS, "Gets total number of legal moves for a player" }, { "n_remaining_pieces", Board_NumPlayerPcs, METH_VARARGS | METH_KEYWORDS, "Gets total number of pieces remaining for a player" }, + { "n_player_corners", Board_NumPlayerOpenCorners, METH_VARARGS | METH_KEYWORDS, "Gets total number of open corners for a player" }, // { "copy", Board_Copy, METH_NOARGS, "Returns a clone of the current board state" }, { NULL } }; From 30c18a9913bcf893f2d95ec2ba7f4028374e359c Mon Sep 17 00:00:00 2001 From: Michael Conard Date: Wed, 22 Nov 2023 18:22:40 -0500 Subject: [PATCH 07/55] implement player_corners with Board_PlayerOpenCorners C API function --- tilewe/__init__.pyi | 4 +++ tilewe/src/ctilewemodule.c | 52 +++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/tilewe/__init__.pyi b/tilewe/__init__.pyi index c36918b..e0fc9c2 100644 --- a/tilewe/__init__.pyi +++ b/tilewe/__init__.pyi @@ -69,4 +69,8 @@ class Board: def n_player_corners(self, for_player: Color=None) -> int: """Gets total number of open corners for a player""" ... + + def player_corners(self, for_player: Color=None) -> list[tilewe.Tile]: + """Gets a list of the open corners for a player""" + ... \ No newline at end of file diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index b099457..20a8c97 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -35,7 +35,7 @@ static int Board_init(BoardObject* self, PyObject* args, PyObject* kwds) static PyObject* Board_CurrentPlayer(BoardObject* self, void* closure) { - return PyBool_FromLong(self->Board.CurTurn); + return PyLong_FromLong(self->Board.CurTurn); } static PyObject* Board_NumPlayers(BoardObject* self, void* closure) @@ -225,6 +225,55 @@ static PyObject* Board_NumPlayerPcs(BoardObject* self, PyObject* args, PyObject* return NULL; } +static PyObject* Board_PlayerOpenCorners(BoardObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = + { + "for_player", + NULL + }; + + int player = Tw_Color_None; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i", kwlist, &player)) + { + return NULL; + } + + if (player == Tw_Color_None) + { + player = self->Board.CurTurn; + } + + if (player >= 0 && player < self->Board.NumPlayers) + { + // count the open corners + Py_ssize_t openCorners = 0; + Tw_TileSet_FOR_EACH(self->Board.Players[player].OpenCorners.Keys, tile, + { + openCorners++; + }); + + // build Python list of the open corner tiles + PyObject* list = PyList_New(openCorners); + Py_ssize_t cornerIndex = 0; + Tw_TileSet_FOR_EACH(self->Board.Players[player].OpenCorners.Keys, tile, + { + PyList_SetItem( + list, + cornerIndex, + PyLong_FromUnsignedLong((unsigned long) tile) + ); + cornerIndex++; + }); + + return list; + } + + PyErr_SetString(PyExc_AttributeError, "for_player must be valid or None"); + return NULL; +} + static PyObject* Board_NumPlayerOpenCorners(BoardObject* self, PyObject* args, PyObject* kwds) { static const char* kwlist[] = @@ -314,6 +363,7 @@ static PyMethodDef Board_methods[] = { "n_legal_moves", Board_NumLegalMoves, METH_VARARGS | METH_KEYWORDS, "Gets total number of legal moves for a player" }, { "n_remaining_pieces", Board_NumPlayerPcs, METH_VARARGS | METH_KEYWORDS, "Gets total number of pieces remaining for a player" }, { "n_player_corners", Board_NumPlayerOpenCorners, METH_VARARGS | METH_KEYWORDS, "Gets total number of open corners for a player" }, + { "player_corners", Board_PlayerOpenCorners, METH_VARARGS | METH_KEYWORDS, "Gets a list of the open corners for a player" }, // { "copy", Board_Copy, METH_NOARGS, "Returns a clone of the current board state" }, { NULL } }; From c3663ccfaf7e04b5df91ac49237d73e96282b09d Mon Sep 17 00:00:00 2001 From: Michael Conard Date: Wed, 22 Nov 2023 18:57:11 -0500 Subject: [PATCH 08/55] refactor C API funcs with only for_player args to use ForPlayerArgHandler --- tilewe/engine.py | 3 +- tilewe/src/ctilewemodule.c | 144 ++++++++++++++----------------------- 2 files changed, 54 insertions(+), 93 deletions(-) diff --git a/tilewe/engine.py b/tilewe/engine.py index 8f956cc..54204b7 100644 --- a/tilewe/engine.py +++ b/tilewe/engine.py @@ -139,11 +139,12 @@ def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: random.shuffle(moves) player = board.current_player + N = board.n_players def eval_after_move(m: tilewe.Move) -> int: with MoveExecutor(board, m): total = 0 - for color in range(board.n_players): + for color in range(N): n_moves = board.n_legal_moves(for_player=color) total += n_moves * (1 if color == player else -1) return total diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index 20a8c97..b50d795 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -167,7 +167,7 @@ static PyObject* Board_ColorAt(BoardObject* self, PyObject* args, PyObject* kwds return PyLong_FromUnsignedLong(Tw_Board_ColorAt(&self->Board, tile)); } -static PyObject* Board_NumLegalMoves(BoardObject* self, PyObject* args, PyObject* kwds) +static bool ForPlayerArgHandler(BoardObject* self, PyObject* args, PyObject* kwds, int* player) { static const char* kwlist[] = { @@ -175,137 +175,97 @@ static PyObject* Board_NumLegalMoves(BoardObject* self, PyObject* args, PyObject NULL }; - int player = Tw_Color_None; + *player = Tw_Color_None; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i", kwlist, &player)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i", kwlist, player)) { - return NULL; + *player = -1; + return false; } - if (player == Tw_Color_None) + if (*player == Tw_Color_None) { - player = self->Board.CurTurn; + *player = self->Board.CurTurn; } - if (player >= 0 && player < self->Board.NumPlayers) + if (*player < 0 || *player >= self->Board.NumPlayers) { - return PyLong_FromLong(Tw_Board_NumMovesForPlayer(&self->Board, (Tw_Color) player)); + *player = -1; + PyErr_SetString(PyExc_AttributeError, "for_player must be valid or None"); + return false; } - PyErr_SetString(PyExc_AttributeError, "for_player must be valid or None"); - return NULL; + return true; } -static PyObject* Board_NumPlayerPcs(BoardObject* self, PyObject* args, PyObject* kwds) +static PyObject* Board_NumLegalMoves(BoardObject* self, PyObject* args, PyObject* kwds) { - static const char* kwlist[] = - { - "for_player", - NULL - }; - - int player = Tw_Color_None; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i", kwlist, &player)) + int player; + if (!ForPlayerArgHandler(self, args, kwds, &player)) { - return NULL; + return NULL; } - if (player == Tw_Color_None) - { - player = self->Board.CurTurn; - } + return PyLong_FromLong(Tw_Board_NumMovesForPlayer(&self->Board, (Tw_Color) player)); +} - if (player >= 0 && player < self->Board.NumPlayers) +static PyObject* Board_NumPlayerPcs(BoardObject* self, PyObject* args, PyObject* kwds) +{ + int player; + if (!ForPlayerArgHandler(self, args, kwds, &player)) { - return PyLong_FromLong(Tw_Board_NumPlayerPcs(&self->Board, (Tw_Color) player)); + return NULL; } - PyErr_SetString(PyExc_AttributeError, "for_player must be valid or None"); - return NULL; + return PyLong_FromLong(Tw_Board_NumPlayerPcs(&self->Board, (Tw_Color) player)); } static PyObject* Board_PlayerOpenCorners(BoardObject* self, PyObject* args, PyObject* kwds) { - static const char* kwlist[] = - { - "for_player", - NULL - }; - - int player = Tw_Color_None; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i", kwlist, &player)) + int player; + if (!ForPlayerArgHandler(self, args, kwds, &player)) { - return NULL; + return NULL; } - if (player == Tw_Color_None) + // count the open corners + Py_ssize_t openCorners = 0; + Tw_TileSet_FOR_EACH(self->Board.Players[player].OpenCorners.Keys, tile, { - player = self->Board.CurTurn; - } + openCorners++; + }); - if (player >= 0 && player < self->Board.NumPlayers) + // build Python list of the open corner tiles + PyObject* list = PyList_New(openCorners); + Py_ssize_t cornerIndex = 0; + Tw_TileSet_FOR_EACH(self->Board.Players[player].OpenCorners.Keys, tile, { - // count the open corners - Py_ssize_t openCorners = 0; - Tw_TileSet_FOR_EACH(self->Board.Players[player].OpenCorners.Keys, tile, - { - openCorners++; - }); - - // build Python list of the open corner tiles - PyObject* list = PyList_New(openCorners); - Py_ssize_t cornerIndex = 0; - Tw_TileSet_FOR_EACH(self->Board.Players[player].OpenCorners.Keys, tile, - { - PyList_SetItem( - list, - cornerIndex, - PyLong_FromUnsignedLong((unsigned long) tile) - ); - cornerIndex++; - }); - - return list; - } + PyList_SetItem( + list, + cornerIndex, + PyLong_FromUnsignedLong((unsigned long) tile) + ); + cornerIndex++; + }); - PyErr_SetString(PyExc_AttributeError, "for_player must be valid or None"); - return NULL; + return list; } static PyObject* Board_NumPlayerOpenCorners(BoardObject* self, PyObject* args, PyObject* kwds) { - static const char* kwlist[] = + int player; + if (!ForPlayerArgHandler(self, args, kwds, &player)) { - "for_player", - NULL - }; - - int player = Tw_Color_None; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i", kwlist, &player)) - { - return NULL; + return NULL; } - if (player == Tw_Color_None) + long openCorners = 0; + Tw_TileSet_FOR_EACH(self->Board.Players[player].OpenCorners.Keys, tile, { - player = self->Board.CurTurn; - } - - if (player >= 0 && player < self->Board.NumPlayers) - { - long openCorners = 0; - Tw_TileSet_FOR_EACH(self->Board.Players[player].OpenCorners.Keys, tile, - { - openCorners++; - }); - return PyLong_FromLong(openCorners); - } + openCorners++; + }); - PyErr_SetString(PyExc_AttributeError, "for_player must be valid or None"); - return NULL; + return PyLong_FromLong(openCorners); } static PyObject* Board_Pop(BoardObject* self, PyObject* Py_UNUSED(ignored)) From dbe43eb3467fc1d3430584d9b5a6a20f716fe820 Mon Sep 17 00:00:00 2001 From: Michael Conard Date: Wed, 22 Nov 2023 19:33:32 -0500 Subject: [PATCH 09/55] use the new Tw_Board_PlayerCorners and Tw_Board_NumPlayerCorners functions --- tilewe/src/ctilewemodule.c | 39 ++++++++++++++------------------------ tilewe/src/tilewe | 2 +- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index b50d795..a28f32b 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -220,7 +220,7 @@ static PyObject* Board_NumPlayerPcs(BoardObject* self, PyObject* args, PyObject* return PyLong_FromLong(Tw_Board_NumPlayerPcs(&self->Board, (Tw_Color) player)); } -static PyObject* Board_PlayerOpenCorners(BoardObject* self, PyObject* args, PyObject* kwds) +static PyObject* Board_PlayerCorners(BoardObject* self, PyObject* args, PyObject* kwds) { int player; if (!ForPlayerArgHandler(self, args, kwds, &player)) @@ -228,30 +228,25 @@ static PyObject* Board_PlayerOpenCorners(BoardObject* self, PyObject* args, PyOb return NULL; } - // count the open corners - Py_ssize_t openCorners = 0; - Tw_TileSet_FOR_EACH(self->Board.Players[player].OpenCorners.Keys, tile, - { - openCorners++; - }); + // get a list of the player's open corners + Tw_TileList openCorners; + Tw_InitTileList(&openCorners); + Tw_Board_PlayerCorners(&self->Board, player, &openCorners); // build Python list of the open corner tiles - PyObject* list = PyList_New(openCorners); - Py_ssize_t cornerIndex = 0; - Tw_TileSet_FOR_EACH(self->Board.Players[player].OpenCorners.Keys, tile, - { + PyObject* list = PyList_New(openCorners.Count); + for (int i = 0; i < openCorners.Count; i++) { PyList_SetItem( list, - cornerIndex, - PyLong_FromUnsignedLong((unsigned long) tile) + i, + PyLong_FromUnsignedLong((unsigned long) openCorners.Elements[i]) ); - cornerIndex++; - }); + } return list; } -static PyObject* Board_NumPlayerOpenCorners(BoardObject* self, PyObject* args, PyObject* kwds) +static PyObject* Board_NumPlayerCorners(BoardObject* self, PyObject* args, PyObject* kwds) { int player; if (!ForPlayerArgHandler(self, args, kwds, &player)) @@ -259,13 +254,7 @@ static PyObject* Board_NumPlayerOpenCorners(BoardObject* self, PyObject* args, P return NULL; } - long openCorners = 0; - Tw_TileSet_FOR_EACH(self->Board.Players[player].OpenCorners.Keys, tile, - { - openCorners++; - }); - - return PyLong_FromLong(openCorners); + return PyLong_FromLong(Tw_Board_NumPlayerCorners(&self->Board, player)); } static PyObject* Board_Pop(BoardObject* self, PyObject* Py_UNUSED(ignored)) @@ -322,8 +311,8 @@ static PyMethodDef Board_methods[] = { "color_at", Board_ColorAt, METH_VARARGS | METH_KEYWORDS, "Color that claimed the tile" }, { "n_legal_moves", Board_NumLegalMoves, METH_VARARGS | METH_KEYWORDS, "Gets total number of legal moves for a player" }, { "n_remaining_pieces", Board_NumPlayerPcs, METH_VARARGS | METH_KEYWORDS, "Gets total number of pieces remaining for a player" }, - { "n_player_corners", Board_NumPlayerOpenCorners, METH_VARARGS | METH_KEYWORDS, "Gets total number of open corners for a player" }, - { "player_corners", Board_PlayerOpenCorners, METH_VARARGS | METH_KEYWORDS, "Gets a list of the open corners for a player" }, + { "n_player_corners", Board_NumPlayerCorners, METH_VARARGS | METH_KEYWORDS, "Gets total number of open corners for a player" }, + { "player_corners", Board_PlayerCorners, METH_VARARGS | METH_KEYWORDS, "Gets a list of the open corners for a player" }, // { "copy", Board_Copy, METH_NOARGS, "Returns a clone of the current board state" }, { NULL } }; diff --git a/tilewe/src/tilewe b/tilewe/src/tilewe index 1b6c595..022b394 160000 --- a/tilewe/src/tilewe +++ b/tilewe/src/tilewe @@ -1 +1 @@ -Subproject commit 1b6c595e389bebe3c54886578888551e52fbcbcf +Subproject commit 022b3941112e654760434b1c604854a4be48699e From 6d3ef863415c326995bcb1dc1adf2957848574bc Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Wed, 22 Nov 2023 19:30:35 -0600 Subject: [PATCH 10/55] Add board.remaining_pieces() --- tilewe/__init__.py | 32 ++++++++++++++++++++++++++++++++ tilewe/__init__.pyi | 17 +++++++++++++++++ tilewe/src/ctilewemodule.c | 27 +++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/tilewe/__init__.py b/tilewe/__init__.py index 5860acb..73628c5 100644 --- a/tilewe/__init__.py +++ b/tilewe/__init__.py @@ -63,3 +63,35 @@ "a19", "b19", "c19", "d19", "e19", "f19", "g19", "h19", "i19", "j19", "k19", "l19", "m19", "n19", "o19", "p19", "q19", "r19", "s19", "t19", # noqa: 501 "a20", "b20", "c20", "d20", "e20", "f20", "g20", "h20", "i20", "j20", "k20", "l20", "m20", "n20", "o20", "p20", "q20", "r20", "s20", "t20" # noqa: 501 ] + +ROTATIONS = [ + NORTH, EAST, SOUTH, WEST, NORTH_F, EAST_F, SOUTH_F, WEST_F +] = [Rotation(x) for x in range(8)] + +ROTATION_NAMES = [ + 'n', 'e', 's', 'w', 'nf', 'ef', 'sf', 'wf' +] + +COLORS = [ + BLUE, YELLOW, RED, GREEN +] = [Color(x) for x in range(4)] + +NO_COLOR: Color = len(COLORS) + +COLOR_NAMES = [ + 'blue', 'yellow', 'red', 'green' +] + +PIECES = [ + O1, I2, I3, L3, O4, I4, L4, + Z4, T4, F5, I5, L5, N5, P5, + T5, U5, V5, W5, X5, Y5, Z5, +] = [Piece(x) for x in range(21)] + +NO_PIECE: Piece = len(PIECES) + +PIECE_NAMES = [ + "O1", "I2", "I3", "L3", "O4", "I4", "L4", + "Z4", "T4", "F5", "I5", "L5", "N5", "P5", + "T5", "U5", "V5", "W5", "X5", "Y5", "Z5", +] diff --git a/tilewe/__init__.pyi b/tilewe/__init__.pyi index e0fc9c2..7f24a0e 100644 --- a/tilewe/__init__.pyi +++ b/tilewe/__init__.pyi @@ -6,6 +6,19 @@ Rotation = int Color = int Move = int +TILES: list[Tile] +PIECES: list[Piece] +ROTATIONS: list[Rotation] +COLOR: list[Color] + +NO_PIECES: Piece +NO_COLOR: Color + +TILE_NAMES: list[str] +PIECE_NAMES: list[str] +ROTATION_NAMES: list[str] +COLOR_NAMES: list[str] + class Board: def __init__(self, n_players: int=4): @@ -66,6 +79,10 @@ class Board: """Gets total number of pieces remaining for a player""" ... + def remaining_pieces(self, for_player: Color=None) -> list[tilewe.Piece]: + """Gets a list of pieces remaining for a player""" + ... + def n_player_corners(self, for_player: Color=None) -> int: """Gets total number of open corners for a player""" ... diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index a28f32b..411dfdc 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -209,6 +209,32 @@ static PyObject* Board_NumLegalMoves(BoardObject* self, PyObject* args, PyObject return PyLong_FromLong(Tw_Board_NumMovesForPlayer(&self->Board, (Tw_Color) player)); } +static PyObject* Board_PlayerPcs(BoardObject* self, PyObject* args, PyObject* kwds) +{ + int player; + if (!ForPlayerArgHandler(self, args, kwds, &player)) + { + return NULL; + } + + // get a list of the player's pieces + Tw_PcList pcs; + Tw_InitPcList(&pcs); + Tw_Board_PlayerPcs(&self->Board, player, &pcs); + + // build Python list of the pieces + PyObject* list = PyList_New(pcs.Count); + for (int i = 0; i < pcs.Count; i++) { + PyList_SetItem( + list, + i, + PyLong_FromUnsignedLong((unsigned long) pcs.Elements[i]) + ); + } + + return list; +} + static PyObject* Board_NumPlayerPcs(BoardObject* self, PyObject* args, PyObject* kwds) { int player; @@ -311,6 +337,7 @@ static PyMethodDef Board_methods[] = { "color_at", Board_ColorAt, METH_VARARGS | METH_KEYWORDS, "Color that claimed the tile" }, { "n_legal_moves", Board_NumLegalMoves, METH_VARARGS | METH_KEYWORDS, "Gets total number of legal moves for a player" }, { "n_remaining_pieces", Board_NumPlayerPcs, METH_VARARGS | METH_KEYWORDS, "Gets total number of pieces remaining for a player" }, + { "remaining_pieces", Board_PlayerPcs, METH_VARARGS | METH_KEYWORDS, "Gets a list of pieces remaining for a player" }, { "n_player_corners", Board_NumPlayerCorners, METH_VARARGS | METH_KEYWORDS, "Gets total number of open corners for a player" }, { "player_corners", Board_PlayerCorners, METH_VARARGS | METH_KEYWORDS, "Gets a list of the open corners for a player" }, // { "copy", Board_Copy, METH_NOARGS, "Returns a clone of the current board state" }, From 7090fb91aee17e113827acee95972cf6b788e5f1 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Wed, 22 Nov 2023 20:02:54 -0600 Subject: [PATCH 11/55] Add C extension board functions: player_score, can_play, is_legal --- tilewe/__init__.pyi | 13 ++++++- tilewe/src/ctilewemodule.c | 78 ++++++++++++++++++++++++++++++++++++-- tilewe/src/tilewe | 2 +- 3 files changed, 88 insertions(+), 5 deletions(-) diff --git a/tilewe/__init__.pyi b/tilewe/__init__.pyi index 7f24a0e..6af03a9 100644 --- a/tilewe/__init__.pyi +++ b/tilewe/__init__.pyi @@ -90,4 +90,15 @@ class Board: def player_corners(self, for_player: Color=None) -> list[tilewe.Tile]: """Gets a list of the open corners for a player""" ... - \ No newline at end of file + + def player_score(self, for_player: Color=None) -> int: + """Gets the score of a player""" + ... + + def can_play(self, for_player: Color=None) -> bool: + """Whether a player has remaining moves""" + ... + + def is_legal(self, move: tilewe.Move, for_player: Color=None) -> bool: + """Whether a move is legal for a player""" + ... diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index 411dfdc..34ced5b 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -131,7 +131,7 @@ static PyObject* Board_GenMoves(BoardObject* self, PyObject* Py_UNUSED(ignored)) static PyObject* Board_Push(BoardObject* self, PyObject* args, PyObject* kwds) { - static const char* kwlist[] = + static char* kwlist[] = { "move", NULL @@ -151,7 +151,7 @@ static PyObject* Board_Push(BoardObject* self, PyObject* args, PyObject* kwds) static PyObject* Board_ColorAt(BoardObject* self, PyObject* args, PyObject* kwds) { - static const char* kwlist[] = + static char* kwlist[] = { "tile", NULL @@ -169,7 +169,7 @@ static PyObject* Board_ColorAt(BoardObject* self, PyObject* args, PyObject* kwds static bool ForPlayerArgHandler(BoardObject* self, PyObject* args, PyObject* kwds, int* player) { - static const char* kwlist[] = + static char* kwlist[] = { "for_player", NULL @@ -198,6 +198,41 @@ static bool ForPlayerArgHandler(BoardObject* self, PyObject* args, PyObject* kwd return true; } +// TODO use better way that doesn't duplicate so much code +static bool ForPlayerAndMoveArgHandler(BoardObject* self, PyObject* args, PyObject* kwds, unsigned* move, int* player) +{ + static char* kwlist[] = + { + "move", + "for_player", + NULL + }; + + *player = Tw_Color_None; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Ii", kwlist, move, player)) + { + *move = (unsigned) Tw_NoMove; + *player = -1; + return false; + } + + if (*player == Tw_Color_None) + { + *player = self->Board.CurTurn; + } + + if (*player < 0 || *player >= self->Board.NumPlayers) + { + *move = (unsigned) Tw_NoMove; + *player = -1; + PyErr_SetString(PyExc_AttributeError, "for_player must be valid or None"); + return false; + } + + return true; +} + static PyObject* Board_NumLegalMoves(BoardObject* self, PyObject* args, PyObject* kwds) { int player; @@ -272,6 +307,28 @@ static PyObject* Board_PlayerCorners(BoardObject* self, PyObject* args, PyObject return list; } +static PyObject* Board_PlayerScore(BoardObject* self, PyObject* args, PyObject* kwds) +{ + int player; + if (!ForPlayerArgHandler(self, args, kwds, &player)) + { + return NULL; + } + + return PyLong_FromLong(self->Board.Players[player].Score); +} + +static PyObject* Board_CanPlay(BoardObject* self, PyObject* args, PyObject* kwds) +{ + int player; + if (!ForPlayerArgHandler(self, args, kwds, &player)) + { + return NULL; + } + + return PyBool_FromLong((long) self->Board.Players[player].CanPlay); +} + static PyObject* Board_NumPlayerCorners(BoardObject* self, PyObject* args, PyObject* kwds) { int player; @@ -283,6 +340,18 @@ static PyObject* Board_NumPlayerCorners(BoardObject* self, PyObject* args, PyObj return PyLong_FromLong(Tw_Board_NumPlayerCorners(&self->Board, player)); } +static PyObject* Board_IsLegal(BoardObject* self, PyObject* args, PyObject* kwds) +{ + int player; + unsigned move; + if (!ForPlayerAndMoveArgHandler(self, args, kwds, &move, &player)) + { + return NULL; + } + + return PyBool_FromLong(Tw_Board_IsLegalForPlayer(&self->Board, (Tw_Color) player, (Tw_Move) move)); +} + static PyObject* Board_Pop(BoardObject* self, PyObject* Py_UNUSED(ignored)) { Tw_Board_Pop(&self->Board); @@ -340,6 +409,9 @@ static PyMethodDef Board_methods[] = { "remaining_pieces", Board_PlayerPcs, METH_VARARGS | METH_KEYWORDS, "Gets a list of pieces remaining for a player" }, { "n_player_corners", Board_NumPlayerCorners, METH_VARARGS | METH_KEYWORDS, "Gets total number of open corners for a player" }, { "player_corners", Board_PlayerCorners, METH_VARARGS | METH_KEYWORDS, "Gets a list of the open corners for a player" }, + { "player_score", Board_PlayerScore, METH_VARARGS | METH_KEYWORDS, "Gets the score of a player" }, + { "can_play", Board_CanPlay, METH_VARARGS | METH_KEYWORDS, "Whether a player has remaining moves" }, + { "is_legal", Board_IsLegal, METH_VARARGS | METH_KEYWORDS, "Whether a move is legal for a player" }, // { "copy", Board_Copy, METH_NOARGS, "Returns a clone of the current board state" }, { NULL } }; diff --git a/tilewe/src/tilewe b/tilewe/src/tilewe index 022b394..16eed61 160000 --- a/tilewe/src/tilewe +++ b/tilewe/src/tilewe @@ -1 +1 @@ -Subproject commit 022b3941112e654760434b1c604854a4be48699e +Subproject commit 16eed61bd3f90fe496aaf053390df1970e09efd7 From 703113b3c58c259d7bd8b7f6f6cee15d230e7e25 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Wed, 22 Nov 2023 20:07:04 -0600 Subject: [PATCH 12/55] Make kwlist const --- tilewe/src/ctilewemodule.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index 34ced5b..0bd70f1 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -15,7 +15,7 @@ struct BoardObject static int Board_init(BoardObject* self, PyObject* args, PyObject* kwds) { - static char* kwlist[] = { "n_players", NULL }; + static const char* kwlist[] = { "n_players", NULL }; int numPlayers = 4; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|i", kwlist, &numPlayers)) @@ -131,7 +131,7 @@ static PyObject* Board_GenMoves(BoardObject* self, PyObject* Py_UNUSED(ignored)) static PyObject* Board_Push(BoardObject* self, PyObject* args, PyObject* kwds) { - static char* kwlist[] = + static const char* kwlist[] = { "move", NULL @@ -151,7 +151,7 @@ static PyObject* Board_Push(BoardObject* self, PyObject* args, PyObject* kwds) static PyObject* Board_ColorAt(BoardObject* self, PyObject* args, PyObject* kwds) { - static char* kwlist[] = + static const char* kwlist[] = { "tile", NULL @@ -169,7 +169,7 @@ static PyObject* Board_ColorAt(BoardObject* self, PyObject* args, PyObject* kwds static bool ForPlayerArgHandler(BoardObject* self, PyObject* args, PyObject* kwds, int* player) { - static char* kwlist[] = + static const char* kwlist[] = { "for_player", NULL @@ -201,7 +201,7 @@ static bool ForPlayerArgHandler(BoardObject* self, PyObject* args, PyObject* kwd // TODO use better way that doesn't duplicate so much code static bool ForPlayerAndMoveArgHandler(BoardObject* self, PyObject* args, PyObject* kwds, unsigned* move, int* player) { - static char* kwlist[] = + static const char* kwlist[] = { "move", "for_player", From 8426df569b8148cea8cb050cc177d187f04487ab Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Wed, 22 Nov 2023 20:48:10 -0600 Subject: [PATCH 13/55] Move interface to __init__.py --- tilewe/__init__.py | 148 +++++++++++++++++++++++++++++++++++++++++++- tilewe/__init__.pyi | 104 ------------------------------- 2 files changed, 147 insertions(+), 105 deletions(-) delete mode 100644 tilewe/__init__.pyi diff --git a/tilewe/__init__.py b/tilewe/__init__.py index 73628c5..57cadd8 100644 --- a/tilewe/__init__.py +++ b/tilewe/__init__.py @@ -3,7 +3,136 @@ if sys.version_info[0] != 3 or sys.version_info[1] < 10: raise Exception("Requires Python 3.10+") -from ctilewe import * +Tile = int +Piece = int +Rotation = int +Color = int +Move = int + +TILES: list[Tile] +PIECES: list[Piece] +ROTATIONS: list[Rotation] +COLOR: list[Color] + +NO_PIECES: Piece +NO_COLOR: Color + +TILE_NAMES: list[str] +PIECE_NAMES: list[str] +ROTATION_NAMES: list[str] +COLOR_NAMES: list[str] + +def tile_to_coords(tile: Tile) -> tuple[int, int]: ... + +def tile_in_bounds(tile: Tile) -> bool: ... + +def coords_to_tile(coords: tuple[int, int]) -> Tile: ... + +def coords_in_bounds(coords: tuple[int, int]) -> bool: ... + +def n_piece_contacts(piece: Piece) -> int: ... + +def n_piece_tiles(piece: Piece) -> int: ... + +def n_piece_corners(piece: Piece) -> int: ... + +def piece_tiles(piece: Piece, rotation: Rotation) -> list[Tile]: ... + +def piece_tile_coords(piece: Piece, rotation: Rotation, contact: Tile=None) -> list[tuple[int, int]]: ... + +def create_move(piece: Piece, rotation: Rotation, contact: Tile, to_tile: Tile) -> Move: ... + +def move_piece(move: Move) -> Piece: ... + +def move_rotation(move: Move) -> Rotation: ... + +def move_contact(move: Move) -> Tile: ... + +def move_tile(move: Move) -> Tile: ... + +class Board: + + def __init__(self, n_players: int=4): + ... + + def __str__(self) -> str: + ... + + @property + def ply(self) -> int: + ... + + @property + def current_player(self) -> Color: + """Color of the current player""" + ... + + @property + def cur_player(self) -> Color: + """Color of the current player""" + ... + + @property + def n_players(self) -> int: + """Number of players""" + ... + + @property + def scores(self) -> list[int]: + ... + + @property + def winners(self) -> list[int]: + ... + + @property + def finished(self) -> bool: + """Whether the game is done""" + ... + + def generate_legal_moves(self, for_player: Color=None) -> list[Move]: + """Generates moves""" + ... + + def push(self, move: Move) -> None: + """Play a move""" + ... + + def pop(self) -> None: + """Undo a move""" + ... + + def n_legal_moves(self, for_player: Color=None) -> int: + """Gets total number of legal moves for a player""" + ... + + def n_remaining_pieces(self, for_player: Color=None) -> int: + """Gets total number of pieces remaining for a player""" + ... + + def remaining_pieces(self, for_player: Color=None) -> list[Piece]: + """Gets a list of pieces remaining for a player""" + ... + + def n_player_corners(self, for_player: Color=None) -> int: + """Gets total number of open corners for a player""" + ... + + def player_corners(self, for_player: Color=None) -> list[Tile]: + """Gets a list of the open corners for a player""" + ... + + def player_score(self, for_player: Color=None) -> int: + """Gets the score of a player""" + ... + + def can_play(self, for_player: Color=None) -> bool: + """Whether a player has remaining moves""" + ... + + def is_legal(self, move: Move, for_player: Color=None) -> bool: + """Whether a move is legal for a player""" + ... Tile = int Piece = int @@ -64,6 +193,21 @@ "a20", "b20", "c20", "d20", "e20", "f20", "g20", "h20", "i20", "j20", "k20", "l20", "m20", "n20", "o20", "p20", "q20", "r20", "s20", "t20" # noqa: 501 ] +def tile_to_coords(tile: Tile) -> tuple[int, int]: + return TILE_COORDS[tile] + +def coords_to_tile(coords: tuple[int, int]) -> Tile: + return coords[0] + coords[1] * 20 + +def tile_to_index(tile: Tile) -> int: + return tile + +def out_of_bounds(coords: tuple[int, int]) -> int: + return not (0 <= coords[0] < 20 and 0 <= coords[1] < 20) + +def in_bounds(coords: tuple[int, int]) -> bool: + return 0 <= coords[0] < 20 and 0 <= coords[1] < 20 + ROTATIONS = [ NORTH, EAST, SOUTH, WEST, NORTH_F, EAST_F, SOUTH_F, WEST_F ] = [Rotation(x) for x in range(8)] @@ -95,3 +239,5 @@ "Z4", "T4", "F5", "I5", "L5", "N5", "P5", "T5", "U5", "V5", "W5", "X5", "Y5", "Z5", ] + +from ctilewe import * \ No newline at end of file diff --git a/tilewe/__init__.pyi b/tilewe/__init__.pyi deleted file mode 100644 index 6af03a9..0000000 --- a/tilewe/__init__.pyi +++ /dev/null @@ -1,104 +0,0 @@ -import tilewe - -Tile = int -Piece = int -Rotation = int -Color = int -Move = int - -TILES: list[Tile] -PIECES: list[Piece] -ROTATIONS: list[Rotation] -COLOR: list[Color] - -NO_PIECES: Piece -NO_COLOR: Color - -TILE_NAMES: list[str] -PIECE_NAMES: list[str] -ROTATION_NAMES: list[str] -COLOR_NAMES: list[str] - -class Board: - - def __init__(self, n_players: int=4): - ... - - def __str__(self) -> str: - ... - - @property - def ply(self) -> int: - ... - - @property - def current_player(self) -> Color: - """Color of the current player""" - ... - - @property - def cur_player(self) -> Color: - """Color of the current player""" - ... - - @property - def n_players(self) -> int: - """Number of players""" - ... - - @property - def scores(self) -> list[int]: - ... - - @property - def winners(self) -> list[int]: - ... - - @property - def finished(self) -> bool: - """Whether the game is done""" - ... - - def generate_legal_moves(self, for_player: tilewe.Color=None) -> list[tilewe.Move]: - """Generates moves""" - ... - - def push(self, move: tilewe.Move) -> None: - """Play a move""" - ... - - def pop(self) -> None: - """Undo a move""" - ... - - def n_legal_moves(self, for_player: Color=None) -> int: - """Gets total number of legal moves for a player""" - ... - - def n_remaining_pieces(self, for_player: Color=None) -> int: - """Gets total number of pieces remaining for a player""" - ... - - def remaining_pieces(self, for_player: Color=None) -> list[tilewe.Piece]: - """Gets a list of pieces remaining for a player""" - ... - - def n_player_corners(self, for_player: Color=None) -> int: - """Gets total number of open corners for a player""" - ... - - def player_corners(self, for_player: Color=None) -> list[tilewe.Tile]: - """Gets a list of the open corners for a player""" - ... - - def player_score(self, for_player: Color=None) -> int: - """Gets the score of a player""" - ... - - def can_play(self, for_player: Color=None) -> bool: - """Whether a player has remaining moves""" - ... - - def is_legal(self, move: tilewe.Move, for_player: Color=None) -> bool: - """Whether a move is legal for a player""" - ... From e426ad712effda12ea01d5a805a69972e5efc5d8 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Wed, 22 Nov 2023 20:49:08 -0600 Subject: [PATCH 14/55] Add tile_to_coords --- tilewe/src/ctilewemodule.c | 78 +++++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index 0bd70f1..51298fa 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -366,24 +366,6 @@ static PyObject* Board_str(BoardObject* self, PyObject* Py_UNUSED(ignored)) return PyUnicode_FromString(buf); } -static PyObject* Tilewe_PlayRandomGame(PyObject* self, PyObject* args) -{ - Tw_Board board[1]; - Tw_MoveList moves[1]; - Tw_InitBoard(board, 4); - - while (!board->Finished) - { - Tw_InitMoveList(moves); - Tw_Board_GenMoves(board, moves); - Tw_Board_Push(board, moves->Elements[rand() % moves->Count]); - } - - Tw_Board_Print(board); - - Py_RETURN_NONE; -} - static PyGetSetDef Board_getsets[] = { { "moves", Board_Moves, NULL, "Move history", NULL }, @@ -431,9 +413,69 @@ static PyTypeObject BoardType = .tp_methods = Board_methods }; +static bool TileArgHandler(PyObject* args, PyObject* kwds, Tw_Tile* tile) +{ + static const char* kwlist[] = + { + "tile", + NULL + }; + + *tile = Tw_Tile_None; + + unsigned tileValue; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "I", kwlist, &tileValue)) + { + return false; + } + + *tile = (Tw_Tile) tileValue; + + if (!Tw_Tile_InBounds(*tile)) + { + PyErr_SetString(PyExc_AttributeError, "tile must be in bounds"); + return false; + } + + return true; +} + +static PyObject* Tilewe_TileToCoords(PyObject* self, PyObject* args, PyObject* kwds) +{ + Tw_Tile tile; + if (!TileArgHandler(args, kwds, &tile)) + { + return NULL; + } + + int x, y; + Tw_Tile_ToCoords(tile, &x, &y); + + return Py_BuildValue("ii", x, y); +} + +static PyObject* Tilewe_PlayRandomGame(PyObject* self, PyObject* args) +{ + Tw_Board board[1]; + Tw_MoveList moves[1]; + Tw_InitBoard(board, 4); + + while (!board->Finished) + { + Tw_InitMoveList(moves); + Tw_Board_GenMoves(board, moves); + Tw_Board_Push(board, moves->Elements[rand() % moves->Count]); + } + + Tw_Board_Print(board); + + Py_RETURN_NONE; +} + static PyMethodDef TileweMethods[] = { { "play_random_game", Tilewe_PlayRandomGame, METH_NOARGS, "Plays a random game" }, + { "tile_to_coords", Tilewe_TileToCoords, METH_VARARGS | METH_KEYWORDS, "Get x,y coordinates of a tile" }, { NULL, NULL, 0, NULL } }; From b8c1ca91b3fe167b819c55c40e31901a8ca37bb1 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Wed, 22 Nov 2023 20:54:44 -0600 Subject: [PATCH 15/55] Make move required in is_legal --- tilewe/src/ctilewemodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index 51298fa..55cf727 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -210,7 +210,7 @@ static bool ForPlayerAndMoveArgHandler(BoardObject* self, PyObject* args, PyObje *player = Tw_Color_None; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Ii", kwlist, move, player)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "I|i", kwlist, move, player)) { *move = (unsigned) Tw_NoMove; *player = -1; From adbf3fb2db4a65af7a9e6eb62515852146b75076 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Wed, 22 Nov 2023 21:10:51 -0600 Subject: [PATCH 16/55] Add coords_to_tile --- tilewe/__init__.py | 15 --------------- tilewe/src/ctilewemodule.c | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/tilewe/__init__.py b/tilewe/__init__.py index 57cadd8..a799ee0 100644 --- a/tilewe/__init__.py +++ b/tilewe/__init__.py @@ -193,21 +193,6 @@ def is_legal(self, move: Move, for_player: Color=None) -> bool: "a20", "b20", "c20", "d20", "e20", "f20", "g20", "h20", "i20", "j20", "k20", "l20", "m20", "n20", "o20", "p20", "q20", "r20", "s20", "t20" # noqa: 501 ] -def tile_to_coords(tile: Tile) -> tuple[int, int]: - return TILE_COORDS[tile] - -def coords_to_tile(coords: tuple[int, int]) -> Tile: - return coords[0] + coords[1] * 20 - -def tile_to_index(tile: Tile) -> int: - return tile - -def out_of_bounds(coords: tuple[int, int]) -> int: - return not (0 <= coords[0] < 20 and 0 <= coords[1] < 20) - -def in_bounds(coords: tuple[int, int]) -> bool: - return 0 <= coords[0] < 20 and 0 <= coords[1] < 20 - ROTATIONS = [ NORTH, EAST, SOUTH, WEST, NORTH_F, EAST_F, SOUTH_F, WEST_F ] = [Rotation(x) for x in range(8)] diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index 55cf727..2915f09 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -440,6 +440,30 @@ static bool TileArgHandler(PyObject* args, PyObject* kwds, Tw_Tile* tile) return true; } +static bool CoordsArgHandler(PyObject* args, PyObject* kwds, int vals[2]) +{ + static const char* kwlist[] = + { + "coords", + NULL + }; + + vals[0] = vals[1] = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "(ii)", kwlist, vals, vals + 1)) + { + return false; + } + + if (!Tw_CoordsInBounds(vals[0], vals[1])) + { + PyErr_SetString(PyExc_AttributeError, "coords must be in bounds"); + return false; + } + + return true; +} + static PyObject* Tilewe_TileToCoords(PyObject* self, PyObject* args, PyObject* kwds) { Tw_Tile tile; @@ -454,6 +478,17 @@ static PyObject* Tilewe_TileToCoords(PyObject* self, PyObject* args, PyObject* k return Py_BuildValue("ii", x, y); } +static PyObject* Tilewe_CoordsToTile(PyObject* self, PyObject* args, PyObject* kwds) +{ + int vals[2]; + if (!CoordsArgHandler(args, kwds, vals)) + { + return NULL; + } + + return Py_BuildValue("i", Tw_MakeTile(vals[0], vals[1])); +} + static PyObject* Tilewe_PlayRandomGame(PyObject* self, PyObject* args) { Tw_Board board[1]; @@ -476,6 +511,7 @@ static PyMethodDef TileweMethods[] = { { "play_random_game", Tilewe_PlayRandomGame, METH_NOARGS, "Plays a random game" }, { "tile_to_coords", Tilewe_TileToCoords, METH_VARARGS | METH_KEYWORDS, "Get x,y coordinates of a tile" }, + { "coords_to_tile", Tilewe_CoordsToTile, METH_VARARGS | METH_KEYWORDS, "Get tile from x,y coordinates" }, { NULL, NULL, 0, NULL } }; From ee69812c75e0765a3403380b18012277cc329bfd Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Wed, 22 Nov 2023 21:18:06 -0600 Subject: [PATCH 17/55] Add bounds checking for tiles and coordinates --- tilewe/src/ctilewemodule.c | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index 2915f09..bc263a2 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -413,7 +413,7 @@ static PyTypeObject BoardType = .tp_methods = Board_methods }; -static bool TileArgHandler(PyObject* args, PyObject* kwds, Tw_Tile* tile) +static bool TileArgHandler(PyObject* args, PyObject* kwds, bool checkBounds, Tw_Tile* tile) { static const char* kwlist[] = { @@ -431,7 +431,7 @@ static bool TileArgHandler(PyObject* args, PyObject* kwds, Tw_Tile* tile) *tile = (Tw_Tile) tileValue; - if (!Tw_Tile_InBounds(*tile)) + if (checkBounds && !Tw_Tile_InBounds(*tile)) { PyErr_SetString(PyExc_AttributeError, "tile must be in bounds"); return false; @@ -440,7 +440,7 @@ static bool TileArgHandler(PyObject* args, PyObject* kwds, Tw_Tile* tile) return true; } -static bool CoordsArgHandler(PyObject* args, PyObject* kwds, int vals[2]) +static bool CoordsArgHandler(PyObject* args, PyObject* kwds, bool checkBounds, int vals[2]) { static const char* kwlist[] = { @@ -455,7 +455,7 @@ static bool CoordsArgHandler(PyObject* args, PyObject* kwds, int vals[2]) return false; } - if (!Tw_CoordsInBounds(vals[0], vals[1])) + if (checkBounds && !Tw_CoordsInBounds(vals[0], vals[1])) { PyErr_SetString(PyExc_AttributeError, "coords must be in bounds"); return false; @@ -467,7 +467,7 @@ static bool CoordsArgHandler(PyObject* args, PyObject* kwds, int vals[2]) static PyObject* Tilewe_TileToCoords(PyObject* self, PyObject* args, PyObject* kwds) { Tw_Tile tile; - if (!TileArgHandler(args, kwds, &tile)) + if (!TileArgHandler(args, kwds, true, &tile)) { return NULL; } @@ -478,17 +478,39 @@ static PyObject* Tilewe_TileToCoords(PyObject* self, PyObject* args, PyObject* k return Py_BuildValue("ii", x, y); } +static PyObject* Tilewe_TileInBounds(PyObject* self, PyObject* args, PyObject* kwds) +{ + Tw_Tile tile; + if (!TileArgHandler(args, kwds, false, &tile)) + { + return NULL; + } + + return PyBool_FromLong(Tw_Tile_InBounds(tile)); +} + static PyObject* Tilewe_CoordsToTile(PyObject* self, PyObject* args, PyObject* kwds) { int vals[2]; - if (!CoordsArgHandler(args, kwds, vals)) + if (!CoordsArgHandler(args, kwds, true, vals)) { return NULL; } - + return Py_BuildValue("i", Tw_MakeTile(vals[0], vals[1])); } +static PyObject* Tilewe_CoordsInBounds(PyObject* self, PyObject* args, PyObject* kwds) +{ + int vals[2]; + if (!CoordsArgHandler(args, kwds, false, vals)) + { + return NULL; + } + + return PyBool_FromLong(Tw_CoordsInBounds(vals[0], vals[1])); +} + static PyObject* Tilewe_PlayRandomGame(PyObject* self, PyObject* args) { Tw_Board board[1]; @@ -511,7 +533,9 @@ static PyMethodDef TileweMethods[] = { { "play_random_game", Tilewe_PlayRandomGame, METH_NOARGS, "Plays a random game" }, { "tile_to_coords", Tilewe_TileToCoords, METH_VARARGS | METH_KEYWORDS, "Get x,y coordinates of a tile" }, + { "tile_in_bounds", Tilewe_TileInBounds, METH_VARARGS | METH_KEYWORDS, "Checks if tile is in bounds" }, { "coords_to_tile", Tilewe_CoordsToTile, METH_VARARGS | METH_KEYWORDS, "Get tile from x,y coordinates" }, + { "coords_in_bounds", Tilewe_CoordsInBounds, METH_VARARGS | METH_KEYWORDS, "Checks if coords are in bounds" }, { NULL, NULL, 0, NULL } }; From 90fae409915f7c9d2a94e65b29eab329752636ff Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Wed, 22 Nov 2023 21:41:04 -0600 Subject: [PATCH 18/55] Add n_piece_X functions --- tilewe/__init__.py | 4 +-- tilewe/src/ctilewemodule.c | 65 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/tilewe/__init__.py b/tilewe/__init__.py index a799ee0..2594394 100644 --- a/tilewe/__init__.py +++ b/tilewe/__init__.py @@ -30,10 +30,10 @@ def coords_to_tile(coords: tuple[int, int]) -> Tile: ... def coords_in_bounds(coords: tuple[int, int]) -> bool: ... -def n_piece_contacts(piece: Piece) -> int: ... - def n_piece_tiles(piece: Piece) -> int: ... +def n_piece_contacts(piece: Piece) -> int: ... + def n_piece_corners(piece: Piece) -> int: ... def piece_tiles(piece: Piece, rotation: Rotation) -> list[Tile]: ... diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index bc263a2..29f2f23 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -464,6 +464,33 @@ static bool CoordsArgHandler(PyObject* args, PyObject* kwds, bool checkBounds, i return true; } +static bool PcArgHandler(PyObject* args, PyObject* kwds, bool checkBounds, Tw_Pc* pc) +{ + static const char* kwlist[] = + { + "piece", + NULL + }; + + *pc = Tw_Pc_None; + + unsigned pcValue; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "I", kwlist, &pcValue)) + { + return false; + } + + *pc = (Tw_Pc) pcValue; + + if (checkBounds && !Tw_Tile_InBounds(*pc)) + { + PyErr_SetString(PyExc_AttributeError, "piece must be valid"); + return false; + } + + return true; +} + static PyObject* Tilewe_TileToCoords(PyObject* self, PyObject* args, PyObject* kwds) { Tw_Tile tile; @@ -511,6 +538,39 @@ static PyObject* Tilewe_CoordsInBounds(PyObject* self, PyObject* args, PyObject* return PyBool_FromLong(Tw_CoordsInBounds(vals[0], vals[1])); } +static PyObject* Tilewe_NumPcContacts(PyObject* self, PyObject* args, PyObject* kwds) +{ + Tw_Pc pc; + if (!PcArgHandler(args, kwds, true, &pc)) + { + return NULL; + } + + return PyLong_FromLong(Tw_TileSet_Count(&Tw_RotPcInfos[Tw_ToRotPc(pc, Tw_Rot_N)].Contacts)); +} + +static PyObject* Tilewe_NumPcTiles(PyObject* self, PyObject* args, PyObject* kwds) +{ + Tw_Pc pc; + if (!PcArgHandler(args, kwds, true, &pc)) + { + return NULL; + } + + return PyLong_FromLong(Tw_TileSet_Count(&Tw_RotPcInfos[Tw_ToRotPc(pc, Tw_Rot_N)].Tiles)); +} + +static PyObject* Tilewe_NumPcCorners(PyObject* self, PyObject* args, PyObject* kwds) +{ + Tw_Pc pc; + if (!PcArgHandler(args, kwds, true, &pc)) + { + return NULL; + } + + return PyLong_FromLong(Tw_TileSet_Count(&Tw_RotPcInfos[Tw_ToRotPc(pc, Tw_Rot_N)].RelCorners)); +} + static PyObject* Tilewe_PlayRandomGame(PyObject* self, PyObject* args) { Tw_Board board[1]; @@ -536,6 +596,9 @@ static PyMethodDef TileweMethods[] = { "tile_in_bounds", Tilewe_TileInBounds, METH_VARARGS | METH_KEYWORDS, "Checks if tile is in bounds" }, { "coords_to_tile", Tilewe_CoordsToTile, METH_VARARGS | METH_KEYWORDS, "Get tile from x,y coordinates" }, { "coords_in_bounds", Tilewe_CoordsInBounds, METH_VARARGS | METH_KEYWORDS, "Checks if coords are in bounds" }, + { "n_piece_tiles", Tilewe_NumPcTiles, METH_VARARGS | METH_KEYWORDS, "Gets number of tiles in a piece" }, + { "n_piece_contacts", Tilewe_NumPcContacts, METH_VARARGS | METH_KEYWORDS, "Gets number of contacts in a piece" }, + { "n_piece_corners", Tilewe_NumPcCorners, METH_VARARGS | METH_KEYWORDS, "Gets number of corners in a piece" }, { NULL, NULL, 0, NULL } }; @@ -550,6 +613,8 @@ static PyModuleDef TileweModule = PyMODINIT_FUNC PyInit_ctilewe(void) { + Tw_Init(); + PyObject* m; if (PyType_Ready(&BoardType) < 0) return NULL; From c6879f55874d1891d025bbbf7bc5f44485ab39f8 Mon Sep 17 00:00:00 2001 From: Michael Conard Date: Wed, 22 Nov 2023 22:42:13 -0500 Subject: [PATCH 19/55] add elo error margin computation to tournaments --- tilewe/elo.py | 114 +++++++++++++++++++++++++++++++++++++------ tilewe/engine.py | 5 ++ tilewe/tournament.py | 67 +++++++++++++++++++------ 3 files changed, 155 insertions(+), 31 deletions(-) diff --git a/tilewe/elo.py b/tilewe/elo.py index 6b9e771..44d869f 100644 --- a/tilewe/elo.py +++ b/tilewe/elo.py @@ -1,10 +1,15 @@ import math +from scipy.special import erfinv def elo_win_probability(elo1: float, elo2: float, C: int=400): """ Returns the probability of player with elo1 winning against player with elo2. + References + ---------- + https://en.wikipedia.org/wiki/Elo_rating_system + Parameters ---------- elo1 : float @@ -18,10 +23,6 @@ def elo_win_probability(elo1: float, elo2: float, C: int=400): ------- probability : float The probability that player 1 will win against player 2, range 0 to 1 - - See Also - -------- - https://en.wikipedia.org/wiki/Elo_rating_system """ # raw @@ -37,6 +38,10 @@ def compute_elo_adjustment_2(elo1: float, elo2: float, outcome: float, K: int = Returns the adjustment to the elo of player with elo1 based on the outcome of the match against player with elo2. + References + ---------- + https://en.wikipedia.org/wiki/Elo_rating_system + Parameters ---------- elo1 : float @@ -54,9 +59,9 @@ def compute_elo_adjustment_2(elo1: float, elo2: float, outcome: float, K: int = The change in player 1's elo based on the outcome of the match """ - p1_win_probability = elo_win_probability(elo1, elo2) - new_elo1 = elo1 + K * (outcome - p1_win_probability) - delta_elo = new_elo1 - elo1 + p1_win_probability: float = elo_win_probability(elo1, elo2) + new_elo1: float = elo1 + K * (outcome - p1_win_probability) + delta_elo: float = new_elo1 - elo1 return delta_elo def compute_elo_adjustment_n(elos: list[float], scores: list[int], K: int = 32): @@ -80,10 +85,10 @@ def compute_elo_adjustment_n(elos: list[float], scores: list[int], K: int = 32): The change in each player's elo based on the outcome of the match """ - player_count = len(elos) - mod_K = K / (player_count - 1) - delta_elos = [ 0 for _ in range(player_count)] - winning_score = max(scores) + player_count: int = len(elos) + mod_K: float = K / (player_count - 1) + delta_elos: float = [0] * player_count + winning_score: int = max(scores) for player1 in range(player_count): for player2 in range(player_count): @@ -91,11 +96,90 @@ def compute_elo_adjustment_n(elos: list[float], scores: list[int], K: int = 32): continue # any losers lose to winners and draw with other losers / any winners win over losers and draw with other winners - player1_win = scores[player1] == winning_score - player2_win = scores[player2] == winning_score - outcome = 0 if player2_win and not player1_win else 1 if player1_win and not player2_win else 0.5 + player1_win: bool = scores[player1] == winning_score + player2_win: bool = scores[player2] == winning_score + outcome: float = 0 if player2_win and not player1_win else 1 if player1_win and not player2_win else 0.5 - delta_elo = compute_elo_adjustment_2(elos[player1], elos[player2], outcome, mod_K) + delta_elo: float = compute_elo_adjustment_2(elos[player1], elos[player2], outcome, mod_K) delta_elos[player1] += delta_elo return delta_elos + +def compute_inverse_error(e: float) -> float: + """ + Returns the inverse error of e. + + Parameters + ---------- + e : float + The input value + + Returns + ------- + inverse_error : float + The inverse error of e + """ + + return math.sqrt(2) * erfinv(2 * e - 1) + +def compute_elo_error_margin(wins: int, draws: int, losses: int, confidence: float=0.95, C: int=400) -> float: + """ + Returns the Elo error margin for a set of results. + + References + ---------- + https://en.wikipedia.org/wiki/Elo_rating_system + https://stackoverflow.com/a/31266328 + https://github.com/cutechess + + Parameters + ---------- + wins : int + The number of wins + draws : int + The number of draws + losses : int + The number of losses + confidence : float + The confidence level, usually 0.95 + C : int + The elo relativity constant, usually 400 + + Returns + ------- + error_margin : float + The Elo error margin + """ + + total: int = wins + draws + losses + if total == 0: + # no games, no confidence + return math.inf + + win_rate: float = wins / total + draw_rate: float = draws / total + loss_rate: float = losses / total + win_draw_factor: float = win_rate + (draw_rate / 2) + + win_deviation: float = win_rate * math.pow(1 - win_draw_factor, 2) + draw_deviation: float = draw_rate * math.pow(0.5 - win_draw_factor, 2) + loss_deviation: float = loss_rate * math.pow(0 - win_draw_factor, 2) + total_deviation: float = math.sqrt(win_deviation + draw_deviation + loss_deviation) / math.sqrt(total) + + minimum: float = win_draw_factor + compute_inverse_error(1 - confidence) * total_deviation + maximum: float = win_draw_factor + compute_inverse_error(confidence) * total_deviation + + if minimum == 0 or maximum == 0: + # cannot compute confidence due to division by zero + return math.inf + + min_recip: float = 1 / minimum + max_recip: float = 1 / maximum + if min_recip <= 1 or max_recip <= 1: + # cannot compute confidence due to zero/negative log + return math.inf + + min_log: float = -C * math.log10(min_recip - 1) + max_log: float = -C * math.log10(max_recip - 1) + + return abs((max_log - min_log) / 2) diff --git a/tilewe/engine.py b/tilewe/engine.py index 54204b7..2f9268e 100644 --- a/tilewe/engine.py +++ b/tilewe/engine.py @@ -17,6 +17,7 @@ def __init__(self, name: str): self.name = name self.seconds = 0 self.end_at = time.time() + self.estimated_elo = None def out_of_time(self) -> bool: return time.time() >= self.end_at @@ -67,6 +68,7 @@ class RandomEngine(Engine): def __init__(self, name: str="Random"): super().__init__(name) + self.estimated_elo = -100.0 def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: return random.choice(board.generate_legal_moves()) @@ -81,6 +83,7 @@ class MostOpenCornersEngine(Engine): def __init__(self, name: str="MostOpenCorners"): super().__init__(name) + self.estimated_elo = 15.0 def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: moves = board.generate_legal_moves() @@ -108,6 +111,7 @@ class LargestPieceEngine(Engine): def __init__(self, name: str="LargestPiece"): super().__init__(name) + self.estimated_elo = 30.0 def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: moves = board.generate_legal_moves() @@ -133,6 +137,7 @@ class MaximizeMoveDifferenceEngine(Engine): def __init__(self, name: str="MaximizeMoveDifference"): super().__init__(name) + self.estimated_elo = 50.0 def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: moves = board.generate_legal_moves() diff --git a/tilewe/tournament.py b/tilewe/tournament.py index 18dc433..55f403d 100644 --- a/tilewe/tournament.py +++ b/tilewe/tournament.py @@ -9,7 +9,7 @@ import tilewe from tilewe.engine import Engine -from tilewe.elo import compute_elo_adjustment_n +from tilewe.elo import compute_elo_adjustment_n, compute_elo_error_margin @dataclass class MatchData: @@ -119,13 +119,35 @@ def avg_scores(self) -> list[float]: def elo_delta(self) -> list[float]: return [self.elo_end[i] - self.elo_start[i] for i in range(self.total_engines)] + @property + def elo_error_margin(self) -> list[float]: + # with default 95% confidence and C=400 + return [ + compute_elo_error_margin( + self.win_counts[i], + self.draw_counts[i], + self.lose_counts[i] + ) for i in range(self.total_engines) + ] + @property def average_match_duration(self) -> float: return self.total_time / max(1, self.total_games) def get_matches_by_engine(self, engine: int) -> list[MatchData]: + # filter the matches for those involving this engine return [x for x in self.match_data if engine in x.engines] - + + def get_elo_error_margin(self, engine: int, confidence: float=0.95, C: int=400) -> float: + # supports custom confidence and C values + return compute_elo_error_margin( + self.win_counts[engine], + self.draw_counts[engine], + self.lose_counts[engine], + confidence, + C + ) + def get_engine_rankings_display(self, sort_by: str = 'elo_end', sort_dir: str = 'desc') -> str: # verify the given sort property exists if not hasattr(self, sort_by): @@ -147,10 +169,11 @@ def get_engine_rankings_display(self, sort_by: str = 'elo_end', sort_dir: str = len_names = max(5, min(24, max([len(x) for x in self.engine_names]) + 1)) len_score = max(6, max([math.floor(math.log10(max(1, self.total_scores[i])) + 1) for i in range(N)]) + 1) len_games = max(7, max([math.floor(math.log10(max(1, self.game_counts[i])) + 1) for i in range(N)]) + 1) + len_elo = max(4, max([math.floor(math.log10(max(1, abs(self.elo_end[i]))) + 1) for i in range(N)]) + 1) out = f"Ranking by {sort_by} {sort_dir}:\n" - out += f"{'Rank':4} {'Name':{len_names}} {'Elo':>5} {'Score':>{len_score}} {'Avg Score':>10} {'Games':>{len_games}} " - out += f"{'Wins':>{len_games}} {'Draws':>{len_games}} {'Losses':>{len_games}} {'Win %':>7}\n" + out += f"{'Rank':4} {'Name':{len_names}} {'Elo':^{len_elo + 9}} {'Score':>{len_score}} {'Avg Score':>10} " + out += f"{'Games':>{len_games}} {'Wins':>{len_games}} {'Draws':>{len_games}} {'Losses':>{len_games}} {'Win Rate':>8}\n" dir = -1 if sort_dir == 'desc' else 1 @@ -158,11 +181,13 @@ def get_engine_rankings_display(self, sort_by: str = 'elo_end', sort_dir: str = name = self.engine_names[engine] draws, wins, games = self.draw_counts[engine], self.win_counts[engine], self.game_counts[engine] losses, score, elo = games - wins - draws, self.total_scores[engine], self.elo_end[engine] + elo_margin = compute_elo_error_margin(wins, draws, losses) - win_rate = f"{(wins / games * 100):>6.2f}%" if games > 0 else f"{'-':>7}" + win_rate = f"{(wins / games * 100):>7.2f}%" if games > 0 else f"{'-':>8}" avg_score = f"{(score / games):>10.2f}" if games > 0 else f"{'-':>10}" + elo_range = f"{elo:>{len_elo}.0f} +/- {elo_margin:<4.0f}" - out += f"{rank:>4d} {name:{len_names}.{len_names}} {elo:>5.0f} {score:>{len_score}d} {avg_score} " + out += f"{rank:>4d} {name:{len_names}.{len_names}} {elo_range} {score:>{len_score}d} {avg_score} " out += f"{games:>{len_games}d} {wins:>{len_games}d} {draws:>{len_games}d} {losses:>{len_games}d} {win_rate}\n" return out @@ -214,7 +239,8 @@ def play( players_per_game: int=4, move_seconds: int=None, verbose_board: bool=False, - verbose_rankings: bool=True + verbose_rankings: bool=True, + use_starting_elos: bool=False, ): """ Used to launch a series of games in an initialized Tournament. @@ -233,6 +259,8 @@ def play( Whether or not to print the final board state of each match verbose_rankings : bool=True Whether or not to print periodic ranking updates and the final rankings at the end + use_starting_elos : bool=False + Whether or not to initialize engines with their self proposed estimated Elo, otherwise 0 """ if n_games <= 0: @@ -249,11 +277,15 @@ def play( # initialize trackers and game controls N = len(self.engines) total_games = 0 - draws = [0 for _ in range(N)] - wins = [0 for _ in range(N)] - games = [0 for _ in range(N)] - elos = [0 for _ in range(N)] - totals = [0 for _ in range(N)] + draws = [0] * N + wins = [0] * N + games = [0] * N + totals = [0] * N + + if use_starting_elos: + elos = [0 if self.engines[i].estimated_elo is None else self.engines[i].estimated_elo for i in range(N)] + else: + elos = [0] * N initial_elos = [i for i in elos] match_results: list[MatchData] = [] @@ -263,20 +295,23 @@ def get_engine_rankings() -> str: len_name = max(5, min(24, max([len(x.name) for x in self.engines]) + 1)) len_score = max(6, max([math.floor(math.log10(max(1, totals[i])) + 1) for i in range(N)]) + 1) len_games = max(7, max([math.floor(math.log10(max(1, games[i])) + 1) for i in range(N)]) + 1) + len_elo = max(4, max([math.floor(math.log10(max(1, abs(elos[i]))) + 1) for i in range(N)]) + 1) - out = f"\n{'Rank':4} {'Name':{len_name}} {'Elo':>5} {'Score':>{len_score}} {'Avg Score':>10} " + out = f"\n{'Rank':4} {'Name':{len_name}} {'Elo':^{len_elo + 9}} {'Score':>{len_score}} {'Avg Score':>10} " out += f"{'Games':>{len_games}} {'Wins':>{len_games}} {'Draws':>{len_games}} " - out += f"{'Losses':>{len_games}} {'Win %':>7}\n" + out += f"{'Losses':>{len_games}} {'Win Rate':>8}\n" for rank, engine in enumerate(sorted(range(N), key=lambda x: -elos[x])): name = self.engines[engine].name draw_count, win_count, game_count = draws[engine], wins[engine], games[engine] loss_count, score, elo = game_count - win_count - draw_count, totals[engine], elos[engine] + elo_margin = compute_elo_error_margin(win_count, draw_count, loss_count) - win_rate = f"{(win_count / game_count * 100):>6.2f}%" if game_count > 0 else f"{'-':>7}" + win_rate = f"{(win_count / game_count * 100):>7.2f}%" if game_count > 0 else f"{'-':>8}" avg_score = f"{(score / game_count):>10.2f}" if game_count > 0 else f"{'-':>10}" + elo_range = f"{elo:>{len_elo}.0f} +/- {elo_margin:<4.0f}" - out += f"{rank:>4d} {name:{len_name}.{len_name}} {elo:>5.0f} {score:>{len_score}d} " + out += f"{rank:>4d} {name:{len_name}.{len_name}} {elo_range} {score:>{len_score}d} " out += f"{avg_score} {game_count:>{len_games}d} {win_count:>{len_games}d} " out += f"{draw_count:>{len_games}d} {loss_count:>{len_games}d} {win_rate}\n" From e08864a975ce2daa16dfc6b2616560aaba981120 Mon Sep 17 00:00:00 2001 From: Michael Conard Date: Wed, 22 Nov 2023 22:45:49 -0500 Subject: [PATCH 20/55] update requirements --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 221af1b..fa02fb3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -numpy==1.26.2 \ No newline at end of file +numpy==1.26.2 +scipy==1.11.4 \ No newline at end of file From 924a10f1289e10671182cb86be1299967f855d64 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Wed, 22 Nov 2023 21:53:03 -0600 Subject: [PATCH 21/55] Add piece_X functions --- tilewe/__init__.py | 2 +- tilewe/src/ctilewemodule.c | 76 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/tilewe/__init__.py b/tilewe/__init__.py index 2594394..1d92955 100644 --- a/tilewe/__init__.py +++ b/tilewe/__init__.py @@ -38,7 +38,7 @@ def n_piece_corners(piece: Piece) -> int: ... def piece_tiles(piece: Piece, rotation: Rotation) -> list[Tile]: ... -def piece_tile_coords(piece: Piece, rotation: Rotation, contact: Tile=None) -> list[tuple[int, int]]: ... +def piece_contacts(piece: Piece, rotation: Rotation) -> list[Tile]: ... def create_move(piece: Piece, rotation: Rotation, contact: Tile, to_tile: Tile) -> Move: ... diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index 29f2f23..256d62a 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -491,6 +491,42 @@ static bool PcArgHandler(PyObject* args, PyObject* kwds, bool checkBounds, Tw_Pc return true; } +static bool PcRotArgHandler(PyObject* args, PyObject* kwds, bool checkBounds, Tw_Pc* pc, Tw_Rot* rot) +{ + static const char* kwlist[] = + { + "piece", + "rotation", + NULL + }; + + *pc = Tw_Pc_None; + *rot = Tw_Rot_N; + + unsigned pcValue, rotValue; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "II", kwlist, &pcValue, &rotValue)) + { + return false; + } + + *pc = (Tw_Pc) pcValue; + *rot = (Tw_Rot) rotValue; + + if (checkBounds && !Tw_Tile_InBounds(*pc)) + { + PyErr_SetString(PyExc_AttributeError, "piece must be valid"); + return false; + } + + if (checkBounds && (*rot < 0 || *rot >= Tw_NumRots)) + { + PyErr_SetString(PyExc_AttributeError, "rotation must be valid"); + return false; + } + + return true; +} + static PyObject* Tilewe_TileToCoords(PyObject* self, PyObject* args, PyObject* kwds) { Tw_Tile tile; @@ -571,6 +607,44 @@ static PyObject* Tilewe_NumPcCorners(PyObject* self, PyObject* args, PyObject* k return PyLong_FromLong(Tw_TileSet_Count(&Tw_RotPcInfos[Tw_ToRotPc(pc, Tw_Rot_N)].RelCorners)); } +static PyObject* Tilewe_PcTiles(PyObject* self, PyObject* args, PyObject* kwds) +{ + Tw_Pc pc; + Tw_Rot rot; + if (!PcRotArgHandler(args, kwds, true, &pc, &rot)) + { + return NULL; + } + + PyObject* list = PyList_New(0); + + Tw_TileSet_FOR_EACH(Tw_RotPcInfos[Tw_ToRotPc(pc, Tw_Rot_N)].Tiles, tile, + { + PyList_Append(list, PyLong_FromLong((long) tile)); + }); + + return list; +} + +static PyObject* Tilewe_PcContacts(PyObject* self, PyObject* args, PyObject* kwds) +{ + Tw_Pc pc; + Tw_Rot rot; + if (!PcRotArgHandler(args, kwds, true, &pc, &rot)) + { + return NULL; + } + + PyObject* list = PyList_New(0); + + Tw_TileSet_FOR_EACH(Tw_RotPcInfos[Tw_ToRotPc(pc, Tw_Rot_N)].Contacts, tile, + { + PyList_Append(list, PyLong_FromLong((long) tile)); + }); + + return list; +} + static PyObject* Tilewe_PlayRandomGame(PyObject* self, PyObject* args) { Tw_Board board[1]; @@ -599,6 +673,8 @@ static PyMethodDef TileweMethods[] = { "n_piece_tiles", Tilewe_NumPcTiles, METH_VARARGS | METH_KEYWORDS, "Gets number of tiles in a piece" }, { "n_piece_contacts", Tilewe_NumPcContacts, METH_VARARGS | METH_KEYWORDS, "Gets number of contacts in a piece" }, { "n_piece_corners", Tilewe_NumPcCorners, METH_VARARGS | METH_KEYWORDS, "Gets number of corners in a piece" }, + { "piece_tiles", Tilewe_PcTiles, METH_VARARGS | METH_KEYWORDS, "Gets tiles in a rotated piece" }, + { "piece_contacts", Tilewe_PcContacts, METH_VARARGS | METH_KEYWORDS, "Gets contacts in a rotated piece" }, { NULL, NULL, 0, NULL } }; From f15cd98c0656ffb0aad6ab1e9fe98ae687e0c1a7 Mon Sep 17 00:00:00 2001 From: Michael Conard Date: Wed, 22 Nov 2023 22:56:08 -0500 Subject: [PATCH 22/55] update estimated_elo comment --- tilewe/engine.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tilewe/engine.py b/tilewe/engine.py index 2f9268e..0396285 100644 --- a/tilewe/engine.py +++ b/tilewe/engine.py @@ -9,15 +9,21 @@ class Engine: Currently requires overriding the `on_search` function which must return one legal move within the given time control. + An Engine's self-estimated Elo should be relative to a 0-point scale, + not a 1500-point scale. For example, if you think your engine would + have a 50% win rate against an "average" opponent, you should leave it + at None/0.0. If you think your engine would have a 75% win rate against + an "average" opponent, you should set it to 200.0. + For extension examples, see the Sample Engines below. For construction examples, see the tilewe.tournament.Tournament class. """ - def __init__(self, name: str): + def __init__(self, name: str, estimated_elo: float=None): self.name = name + self.estimated_elo = estimated_elo self.seconds = 0 self.end_at = time.time() - self.estimated_elo = None def out_of_time(self) -> bool: return time.time() >= self.end_at @@ -67,8 +73,7 @@ class RandomEngine(Engine): """ def __init__(self, name: str="Random"): - super().__init__(name) - self.estimated_elo = -100.0 + super().__init__(name, -100.0) def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: return random.choice(board.generate_legal_moves()) @@ -82,8 +87,7 @@ class MostOpenCornersEngine(Engine): """ def __init__(self, name: str="MostOpenCorners"): - super().__init__(name) - self.estimated_elo = 15.0 + super().__init__(name, 15.0) def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: moves = board.generate_legal_moves() @@ -110,8 +114,7 @@ class LargestPieceEngine(Engine): """ def __init__(self, name: str="LargestPiece"): - super().__init__(name) - self.estimated_elo = 30.0 + super().__init__(name, 30.0) def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: moves = board.generate_legal_moves() @@ -136,8 +139,7 @@ class MaximizeMoveDifferenceEngine(Engine): """ def __init__(self, name: str="MaximizeMoveDifference"): - super().__init__(name) - self.estimated_elo = 50.0 + super().__init__(name, 50.0) def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: moves = board.generate_legal_moves() From 01e4c3945bd270e77d8c636dc7c890ad4753b289 Mon Sep 17 00:00:00 2001 From: Michael Conard Date: Wed, 22 Nov 2023 23:01:18 -0500 Subject: [PATCH 23/55] bump win rate col width for padding --- tilewe/tournament.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tilewe/tournament.py b/tilewe/tournament.py index 55f403d..f9889be 100644 --- a/tilewe/tournament.py +++ b/tilewe/tournament.py @@ -173,7 +173,7 @@ def get_engine_rankings_display(self, sort_by: str = 'elo_end', sort_dir: str = out = f"Ranking by {sort_by} {sort_dir}:\n" out += f"{'Rank':4} {'Name':{len_names}} {'Elo':^{len_elo + 9}} {'Score':>{len_score}} {'Avg Score':>10} " - out += f"{'Games':>{len_games}} {'Wins':>{len_games}} {'Draws':>{len_games}} {'Losses':>{len_games}} {'Win Rate':>8}\n" + out += f"{'Games':>{len_games}} {'Wins':>{len_games}} {'Draws':>{len_games}} {'Losses':>{len_games}} {'Win Rate':>9}\n" dir = -1 if sort_dir == 'desc' else 1 @@ -183,7 +183,7 @@ def get_engine_rankings_display(self, sort_by: str = 'elo_end', sort_dir: str = losses, score, elo = games - wins - draws, self.total_scores[engine], self.elo_end[engine] elo_margin = compute_elo_error_margin(wins, draws, losses) - win_rate = f"{(wins / games * 100):>7.2f}%" if games > 0 else f"{'-':>8}" + win_rate = f"{(wins / games * 100):>8.2f}%" if games > 0 else f"{'-':>9}" avg_score = f"{(score / games):>10.2f}" if games > 0 else f"{'-':>10}" elo_range = f"{elo:>{len_elo}.0f} +/- {elo_margin:<4.0f}" @@ -299,7 +299,7 @@ def get_engine_rankings() -> str: out = f"\n{'Rank':4} {'Name':{len_name}} {'Elo':^{len_elo + 9}} {'Score':>{len_score}} {'Avg Score':>10} " out += f"{'Games':>{len_games}} {'Wins':>{len_games}} {'Draws':>{len_games}} " - out += f"{'Losses':>{len_games}} {'Win Rate':>8}\n" + out += f"{'Losses':>{len_games}} {'Win Rate':>9}\n" for rank, engine in enumerate(sorted(range(N), key=lambda x: -elos[x])): name = self.engines[engine].name @@ -307,7 +307,7 @@ def get_engine_rankings() -> str: loss_count, score, elo = game_count - win_count - draw_count, totals[engine], elos[engine] elo_margin = compute_elo_error_margin(win_count, draw_count, loss_count) - win_rate = f"{(win_count / game_count * 100):>7.2f}%" if game_count > 0 else f"{'-':>8}" + win_rate = f"{(win_count / game_count * 100):>8.2f}%" if game_count > 0 else f"{'-':>9}" avg_score = f"{(score / game_count):>10.2f}" if game_count > 0 else f"{'-':>10}" elo_range = f"{elo:>{len_elo}.0f} +/- {elo_margin:<4.0f}" From e46023bd2315486243fbf0b85c0c120ce890528f Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Wed, 22 Nov 2023 22:06:06 -0600 Subject: [PATCH 24/55] Add move getters --- tilewe/__init__.py | 13 +++++- tilewe/src/ctilewemodule.c | 82 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/tilewe/__init__.py b/tilewe/__init__.py index 1d92955..6394736 100644 --- a/tilewe/__init__.py +++ b/tilewe/__init__.py @@ -40,7 +40,7 @@ def piece_tiles(piece: Piece, rotation: Rotation) -> list[Tile]: ... def piece_contacts(piece: Piece, rotation: Rotation) -> list[Tile]: ... -def create_move(piece: Piece, rotation: Rotation, contact: Tile, to_tile: Tile) -> Move: ... +def create_move(piece: Piece, rotation: Rotation, contact: Tile, to_tile: Tile) -> Move | None: ... def move_piece(move: Move) -> Piece: ... @@ -50,6 +50,15 @@ def move_contact(move: Move) -> Tile: ... def move_tile(move: Move) -> Tile: ... +def move_str(move: Move) -> str: + out = "" + out += PIECE_NAMES[move_piece(move)] + out += ROTATION_NAMES[move_rotation(move)] + out += "-" + out += TILE_NAMES[move_contact(move)] + out += TILE_NAMES[move_tile(move)] + return out + class Board: def __init__(self, n_players: int=4): @@ -225,4 +234,4 @@ def is_legal(self, move: Move, for_player: Color=None) -> bool: "T5", "U5", "V5", "W5", "X5", "Y5", "Z5", ] -from ctilewe import * \ No newline at end of file +from ctilewe import * diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index 256d62a..5187abe 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -527,6 +527,38 @@ static bool PcRotArgHandler(PyObject* args, PyObject* kwds, bool checkBounds, Tw return true; } +static bool MoveArgHandler(PyObject* args, PyObject* kwds, bool checkBounds, Tw_Move* move) +{ + static const char* kwlist[] = + { + "move", + NULL + }; + + *move = Tw_NoMove; + + unsigned moveValue; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "I", kwlist, &moveValue)) + { + return false; + } + + Tw_Move tmp = *move = (Tw_Move) moveValue; + + if (checkBounds && (tmp == Tw_NoMove || tmp != Tw_MakeMove_Safe( + Tw_Move_Pc(tmp), + Tw_Move_Rot(tmp), + Tw_Move_Con(tmp), + Tw_Move_ToTile(tmp) + ))) + { + PyErr_SetString(PyExc_AttributeError, "move must be valid"); + return false; + } + + return true; +} + static PyObject* Tilewe_TileToCoords(PyObject* self, PyObject* args, PyObject* kwds) { Tw_Tile tile; @@ -645,6 +677,52 @@ static PyObject* Tilewe_PcContacts(PyObject* self, PyObject* args, PyObject* kwd return list; } +static PyObject* Tilewe_MovePc(PyObject* self, PyObject* args, PyObject* kwds) +{ + Tw_Move move; + if (!MoveArgHandler(args, kwds, true, &move)) + { + return NULL; + } + + return PyLong_FromLong(Tw_Move_Pc(move)); +} + +static PyObject* Tilewe_MoveRot(PyObject* self, PyObject* args, PyObject* kwds) +{ + Tw_Move move; + if (!MoveArgHandler(args, kwds, true, &move)) + { + return NULL; + } + + return PyLong_FromLong(Tw_Move_Rot(move)); +} + + +static PyObject* Tilewe_MoveCon(PyObject* self, PyObject* args, PyObject* kwds) +{ + Tw_Move move; + if (!MoveArgHandler(args, kwds, true, &move)) + { + return NULL; + } + + return PyLong_FromLong(Tw_Move_Con(move)); +} + + +static PyObject* Tilewe_MoveTile(PyObject* self, PyObject* args, PyObject* kwds) +{ + Tw_Move move; + if (!MoveArgHandler(args, kwds, true, &move)) + { + return NULL; + } + + return PyLong_FromLong(Tw_Move_ToTile(move)); +} + static PyObject* Tilewe_PlayRandomGame(PyObject* self, PyObject* args) { Tw_Board board[1]; @@ -675,6 +753,10 @@ static PyMethodDef TileweMethods[] = { "n_piece_corners", Tilewe_NumPcCorners, METH_VARARGS | METH_KEYWORDS, "Gets number of corners in a piece" }, { "piece_tiles", Tilewe_PcTiles, METH_VARARGS | METH_KEYWORDS, "Gets tiles in a rotated piece" }, { "piece_contacts", Tilewe_PcContacts, METH_VARARGS | METH_KEYWORDS, "Gets contacts in a rotated piece" }, + { "move_piece", Tilewe_MovePc, METH_VARARGS | METH_KEYWORDS, "Gets the piece used in a move" }, + { "move_rotation", Tilewe_MoveRot, METH_VARARGS | METH_KEYWORDS, "Gets the piece rotation used in a move" }, + { "move_contact", Tilewe_MoveCon, METH_VARARGS | METH_KEYWORDS, "Gets the contact tile used in a move" }, + { "move_tile", Tilewe_MoveTile, METH_VARARGS | METH_KEYWORDS, "Gets the open corner used in a move" }, { NULL, NULL, 0, NULL } }; From 9d45bc5066eb9ac1a42e25145f2b476d183c4b1e Mon Sep 17 00:00:00 2001 From: Michael Conard Date: Wed, 22 Nov 2023 23:14:21 -0500 Subject: [PATCH 25/55] enforce ranges on confidence/C --- tilewe/elo.py | 5 +++++ tilewe/tournament.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tilewe/elo.py b/tilewe/elo.py index 44d869f..2e5771b 100644 --- a/tilewe/elo.py +++ b/tilewe/elo.py @@ -150,6 +150,11 @@ def compute_elo_error_margin(wins: int, draws: int, losses: int, confidence: flo error_margin : float The Elo error margin """ + + if confidence <= 0 or confidence >= 1: + raise ValueError(f"confidence must be between 0 and 1 (exclusive), got {confidence}") + if C < 1: + raise ValueError(f"C must be greater than or equal to 1, got {C}") total: int = wins + draws + losses if total == 0: diff --git a/tilewe/tournament.py b/tilewe/tournament.py index f9889be..0340d1b 100644 --- a/tilewe/tournament.py +++ b/tilewe/tournament.py @@ -144,8 +144,8 @@ def get_elo_error_margin(self, engine: int, confidence: float=0.95, C: int=400) self.win_counts[engine], self.draw_counts[engine], self.lose_counts[engine], - confidence, - C + max(0.001, min(0.999, confidence)), + max(1, C) ) def get_engine_rankings_display(self, sort_by: str = 'elo_end', sort_dir: str = 'desc') -> str: From b9436918dc9ac802fd5c26121c42f646e308521e Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Wed, 22 Nov 2023 22:26:30 -0600 Subject: [PATCH 26/55] Add create_move --- tilewe/__init__.py | 2 +- tilewe/src/ctilewemodule.c | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/tilewe/__init__.py b/tilewe/__init__.py index 6394736..8e08c44 100644 --- a/tilewe/__init__.py +++ b/tilewe/__init__.py @@ -40,7 +40,7 @@ def piece_tiles(piece: Piece, rotation: Rotation) -> list[Tile]: ... def piece_contacts(piece: Piece, rotation: Rotation) -> list[Tile]: ... -def create_move(piece: Piece, rotation: Rotation, contact: Tile, to_tile: Tile) -> Move | None: ... +def create_move(piece: Piece, rotation: Rotation, contact: Tile, to_tile: Tile) -> Move: ... def move_piece(move: Move) -> Piece: ... diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index 5187abe..0d08c97 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -708,10 +708,10 @@ static PyObject* Tilewe_MoveCon(PyObject* self, PyObject* args, PyObject* kwds) return NULL; } + return PyLong_FromLong(Tw_Move_Con(move)); } - static PyObject* Tilewe_MoveTile(PyObject* self, PyObject* args, PyObject* kwds) { Tw_Move move; @@ -723,6 +723,36 @@ static PyObject* Tilewe_MoveTile(PyObject* self, PyObject* args, PyObject* kwds) return PyLong_FromLong(Tw_Move_ToTile(move)); } +static PyObject* Tilewe_CreateMove(PyObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = + { + "piece", + "rotation", + "contact", + "to_tile", + NULL + }; + + unsigned pc, rot, con, tile; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "IIII", kwlist, &pc, &rot, &con, &tile)) + { + // PyErr_SetString(PyExc_AttributeError, "piece must be valid"); + return false; + } + + Tw_Move move = Tw_MakeMove_Safe(pc, rot, con, tile); + + if (move == Tw_NoMove) + { + Py_RETURN_NONE; + } + else + { + PyLong_FromLong(move); + } +} + static PyObject* Tilewe_PlayRandomGame(PyObject* self, PyObject* args) { Tw_Board board[1]; @@ -757,6 +787,7 @@ static PyMethodDef TileweMethods[] = { "move_rotation", Tilewe_MoveRot, METH_VARARGS | METH_KEYWORDS, "Gets the piece rotation used in a move" }, { "move_contact", Tilewe_MoveCon, METH_VARARGS | METH_KEYWORDS, "Gets the contact tile used in a move" }, { "move_tile", Tilewe_MoveTile, METH_VARARGS | METH_KEYWORDS, "Gets the open corner used in a move" }, + { "create_move", Tilewe_CreateMove, METH_VARARGS | METH_KEYWORDS, "Creates a move from a piece, rotation, contact, and tile" }, { NULL, NULL, 0, NULL } }; From 62cc9566e6dc301434bc4528f3f67ec5d50f9c99 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Wed, 22 Nov 2023 23:10:52 -0600 Subject: [PATCH 27/55] Consider rotation when returning piece tiles --- tilewe/src/ctilewemodule.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index 0d08c97..1985f23 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -650,7 +650,7 @@ static PyObject* Tilewe_PcTiles(PyObject* self, PyObject* args, PyObject* kwds) PyObject* list = PyList_New(0); - Tw_TileSet_FOR_EACH(Tw_RotPcInfos[Tw_ToRotPc(pc, Tw_Rot_N)].Tiles, tile, + Tw_TileSet_FOR_EACH(Tw_RotPcInfos[Tw_ToRotPc(pc, rot)].Tiles, tile, { PyList_Append(list, PyLong_FromLong((long) tile)); }); @@ -742,7 +742,7 @@ static PyObject* Tilewe_CreateMove(PyObject* self, PyObject* args, PyObject* kwd } Tw_Move move = Tw_MakeMove_Safe(pc, rot, con, tile); - + if (move == Tw_NoMove) { Py_RETURN_NONE; From 546ca28dc896eaa163739ae10746395328c638bd Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Wed, 22 Nov 2023 23:17:29 -0600 Subject: [PATCH 28/55] Fix tournament match results when there is only one player --- tilewe/tournament.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tilewe/tournament.py b/tilewe/tournament.py index 18dc433..a2d4ebe 100644 --- a/tilewe/tournament.py +++ b/tilewe/tournament.py @@ -332,6 +332,10 @@ def get_engine_rankings() -> str: player_scores = [scores[i] for i in game_players] winner_names = [self.engines[i].name for i in winners] + player_elos = [] + delta_elos = [] + new_elos = [] + # if there are enough players, compute elo changes if board.n_players > 1: player_elos = [elos[i] for i in game_players] From 3ea3d28100cce2031823f5296d6f6f3bfca36b30 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Wed, 22 Nov 2023 23:18:14 -0600 Subject: [PATCH 29/55] Update sample engines to use new API --- tilewe/engine.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tilewe/engine.py b/tilewe/engine.py index 54204b7..56ee251 100644 --- a/tilewe/engine.py +++ b/tilewe/engine.py @@ -113,10 +113,13 @@ def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: moves = board.generate_legal_moves() random.shuffle(moves) - best = max(moves, key=lambda m: - tilewe.n_piece_tiles(m.piece) * 100 + - tilewe.n_piece_corners(m.piece) * 10 + - tilewe.n_piece_contacts(m.piece)) + def score(m: tilewe.Move): + pc = tilewe.move_piece(m) + return tilewe.n_piece_tiles(pc) * 100 + \ + tilewe.n_piece_corners(pc) * 10 + \ + tilewe.n_piece_contacts(pc) + + best = max(moves, key=score) return best @@ -237,12 +240,11 @@ def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: cur_player = board.current_player def evaluate_move_weight(move: tilewe.Move) -> float: - total: float = 0.0 + total: float = 0 - to_coords = tilewe.tile_to_coords(move.to_tile) - for coords in tilewe.piece_tile_coords(move.piece, move.rotation, move.contact): - coords = (coords[0] + to_coords[0], coords[1] + to_coords[1]) - total += self.weights[tilewe.coords_to_tile(coords)] + offset = tilewe.move_tile(move) - tilewe.move_contact(move) + for tile in tilewe.piece_tiles(tilewe.move_piece(move), tilewe.move_rotation(move)): + total += self.weights[tile + offset] return total @@ -252,6 +254,6 @@ def evaluate_move_weight(move: tilewe.Move) -> float: if board.ply < board.n_players: # prune to one corner to reduce moves to evaluate corner = board.player_corners(cur_player)[0] - moves = [i for i in moves if i.to_tile == corner] + moves = [i for i in moves if tilewe.move_tile(i) == corner] return max(moves, key=evaluate_move_weight) From 2bcdd789a93c3b86f3b5f7244419e15e61b8fe88 Mon Sep 17 00:00:00 2001 From: Michael Conard Date: Thu, 23 Nov 2023 00:30:03 -0500 Subject: [PATCH 30/55] allow override estimated elo --- tilewe/engine.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tilewe/engine.py b/tilewe/engine.py index 0396285..cbb3a9c 100644 --- a/tilewe/engine.py +++ b/tilewe/engine.py @@ -72,8 +72,8 @@ class RandomEngine(Engine): Pretty bad, but makes moves really fast. """ - def __init__(self, name: str="Random"): - super().__init__(name, -100.0) + def __init__(self, name: str="Random", estimated_elo: float=None): + super().__init__(name, -100.0 if estimated_elo is None else estimated_elo) def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: return random.choice(board.generate_legal_moves()) @@ -86,8 +86,8 @@ class MostOpenCornersEngine(Engine): Fairly weak but does result in decent board coverage behavior. """ - def __init__(self, name: str="MostOpenCorners"): - super().__init__(name, 15.0) + def __init__(self, name: str="MostOpenCorners", estimated_elo: float=None): + super().__init__(name, 15.0 if estimated_elo is None else estimated_elo) def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: moves = board.generate_legal_moves() @@ -113,8 +113,8 @@ class LargestPieceEngine(Engine): ties, it's effectively a greedy form of RandomEngine. """ - def __init__(self, name: str="LargestPiece"): - super().__init__(name, 30.0) + def __init__(self, name: str="LargestPiece", estimated_elo: float=None): + super().__init__(name, 30.0 if estimated_elo is None else estimated_elo) def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: moves = board.generate_legal_moves() @@ -138,8 +138,8 @@ class MaximizeMoveDifferenceEngine(Engine): getting access to an open area on the board, etc. """ - def __init__(self, name: str="MaximizeMoveDifference"): - super().__init__(name, 50.0) + def __init__(self, name: str="MaximizeMoveDifference", estimated_elo: float=None): + super().__init__(name, 50.0 if estimated_elo is None else estimated_elo) def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: moves = board.generate_legal_moves() @@ -221,13 +221,13 @@ class TileWeightEngine(Engine): 'turtle': TURTLE_WEIGHTS } - def __init__(self, name: str="TileWeight", weight_map: str='wall_crawl', custom_weights: list[int | float]=None): + def __init__(self, name: str="TileWeight", weight_map: str='wall_crawl', custom_weights: list[int | float]=None, estimated_elo: float=None): """ Current `weight_map` built-in options are 'wall_crawl' and 'turtle' Can optionally provide a custom set of weights instead """ - super().__init__(name) + super().__init__(name, 0.0 if estimated_elo is None else estimated_elo) if custom_weights is not None: if len(custom_weights) != 20 * 20: From b38860c0e16de6728908855338f8ca6a70d94fea Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Wed, 22 Nov 2023 23:39:29 -0600 Subject: [PATCH 31/55] Allow gen_moves to accept arguments --- tilewe/src/ctilewemodule.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index 1985f23..a68fcf6 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -381,8 +381,8 @@ static PyGetSetDef Board_getsets[] = static PyMethodDef Board_methods[] = { - { "generate_legal_moves", Board_GenMoves, METH_NOARGS, "Returns a list of legal moves" }, - { "gen_moves", Board_GenMoves, METH_NOARGS, "Returns a list of legal moves" }, + { "generate_legal_moves", Board_GenMoves, METH_VARARGS | METH_KEYWORDS, "Returns a list of legal moves" }, + { "gen_moves", Board_GenMoves, METH_VARARGS | METH_KEYWORDS, "Returns a list of legal moves" }, { "push", Board_Push, METH_VARARGS | METH_KEYWORDS, "Plays a move" }, { "pop", Board_Pop, METH_NOARGS, "Undoes a move" }, { "color_at", Board_ColorAt, METH_VARARGS | METH_KEYWORDS, "Color that claimed the tile" }, From dbcb8de538fab9d63522874ece90210a4ef89722 Mon Sep 17 00:00:00 2001 From: Michael Conard Date: Thu, 23 Nov 2023 00:40:50 -0500 Subject: [PATCH 32/55] improve the fix for single player tournament elo handling --- tilewe/tournament.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tilewe/tournament.py b/tilewe/tournament.py index a2d4ebe..24d90f3 100644 --- a/tilewe/tournament.py +++ b/tilewe/tournament.py @@ -331,18 +331,16 @@ def get_engine_rankings() -> str: player_names = [self.engines[i].name for i in game_players] player_scores = [scores[i] for i in game_players] winner_names = [self.engines[i].name for i in winners] - - player_elos = [] - delta_elos = [] - new_elos = [] - # if there are enough players, compute elo changes + player_elos = [elos[i] for i in game_players] if board.n_players > 1: - player_elos = [elos[i] for i in game_players] + # if there are enough players, compute elo changes delta_elos = compute_elo_adjustment_n(player_elos, player_scores, K=8) for index, player in enumerate(game_players): elos[player] += delta_elos[index] - new_elos = [elos[i] for i in game_players] + else: + delta_elos = [0 for _ in game_players] + new_elos = [elos[i] for i in game_players] # save match data match_data = MatchData( From 818980bb4f84f543fcb62a2fd0d1dbbd0420dd35 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Wed, 22 Nov 2023 23:55:07 -0600 Subject: [PATCH 33/55] Change tests to use new API --- tilewe/tests/test_gameplay.py | 15 ++- tilewe/tests/test_moves.py | 191 +++++++++------------------------- 2 files changed, 58 insertions(+), 148 deletions(-) diff --git a/tilewe/tests/test_gameplay.py b/tilewe/tests/test_gameplay.py index 3a87e64..5f0d0fb 100644 --- a/tilewe/tests/test_gameplay.py +++ b/tilewe/tests/test_gameplay.py @@ -30,7 +30,7 @@ def test_no_moves_is_finished(self): # assert that when no more moves can be played, all players have can_play() == False for i in range(board.n_players): - self.assertFalse(board.can_play(player=i)) + self.assertFalse(board.can_play(for_player=i)) def test_finished_game_state(self): random.seed(0) @@ -39,7 +39,7 @@ def test_finished_game_state(self): # play a game until the state is marked finished tracked_ply = 0 while not board.finished: - board.push(random.choice(sorted(board.generate_legal_moves(), key=lambda m: str(m)))) + board.push(random.choice(sorted(board.generate_legal_moves(), key=lambda m: tilewe.move_str(m)))) # assert that the game finishes before 84 moves (i.e. no infinite loop) tracked_ply += 1 @@ -49,13 +49,10 @@ def test_finished_game_state(self): for i in range(board.n_players): # assert that when the game is marked finished, no moves can be played - self.assertEqual(board.n_legal_moves(unique=True, for_player=i), 0) - self.assertEqual(board.generate_legal_moves(unique=True, for_player=i), []) + self.assertEqual(board.n_legal_moves(for_player=i), 0) + self.assertEqual(board.generate_legal_moves(for_player=i), []) - self.assertEqual(board.n_legal_moves(unique=False, for_player=i), 0) - self.assertEqual(board.generate_legal_moves(unique=False, for_player=i), []) - - self.assertFalse(board.can_play(player=i)) + self.assertFalse(board.can_play(for_player=i)) # assert that when there are no moves, players have 0 open corners self.assertEqual(len(board.player_corners(i)), 0) @@ -71,7 +68,7 @@ def test_finished_game_state(self): 'T4w-b2b14', 'Z4nf-a1n17', 'F5e-b3o10', 'L3n-b1f1', 'U5n-a2q17', 'Z5ef-a3o7', 'T5s-c1f4', 'I2e-a1g20', 'T5e-c3m8', 'L4nf-b3c3', 'L5wf-d1n19', 'N5w-a2n14', 'T4n-a2r12' ] - all_moves = [str(move) for move in board.moves] + all_moves = [tilewe.move_str(move) for move in board.moves] # assert that the expected game was played unexpected_game_msg = "Expected game not played, was generate_legal_moves() changed intentionally?" diff --git a/tilewe/tests/test_moves.py b/tilewe/tests/test_moves.py index b760f77..ea54069 100644 --- a/tilewe/tests/test_moves.py +++ b/tilewe/tests/test_moves.py @@ -7,77 +7,39 @@ class TestMoves(unittest.TestCase): def test_unique_legal_move(self): board = tilewe.Board(4) - self.assertTrue(board.is_legal(tilewe.Move( + self.assertTrue(board.is_legal(tilewe.create_move( piece=tilewe.Z5, rotation=tilewe.NORTH, contact=tilewe.A03, to_tile=tilewe.A20 ))) - self.assertTrue(board.is_legal(tilewe.Move( + self.assertTrue(board.is_legal(tilewe.create_move( piece=tilewe.Z5, rotation=tilewe.EAST, contact=tilewe.A01, to_tile=tilewe.A01 ))) - self.assertTrue(board.is_legal(tilewe.Move( + self.assertTrue(board.is_legal(tilewe.create_move( piece=tilewe.T4, rotation=tilewe.NORTH, contact=tilewe.A02, to_tile=tilewe.A20 ))) - self.assertTrue(board.is_legal(tilewe.Move( + self.assertTrue(board.is_legal(tilewe.create_move( piece=tilewe.T4, rotation=tilewe.EAST, contact=tilewe.B03, to_tile=tilewe.T20 ))) - def test_nonunique_legal_move(self): - board = tilewe.Board(4) - - self.assertTrue(board.is_legal(tilewe.Move( - piece=tilewe.Z5, - rotation=tilewe.SOUTH, - contact=tilewe.A03, - to_tile=tilewe.A20 - ))) - - self.assertTrue(board.is_legal(tilewe.Move( - piece=tilewe.Z5, - rotation=tilewe.WEST, - contact=tilewe.A01, - to_tile=tilewe.A01 - ))) - - self.assertTrue(board.is_legal(tilewe.Move( - piece=tilewe.T4, - rotation=tilewe.NORTH_F, - contact=tilewe.A02, - to_tile=tilewe.A20 - ))) - - self.assertTrue(board.is_legal(tilewe.Move( - piece=tilewe.T4, - rotation=tilewe.WEST_F, - contact=tilewe.B03, - to_tile=tilewe.T20 - ))) - - self.assertTrue(board.is_legal(tilewe.Move( - piece=tilewe.O4, - rotation=tilewe.WEST_F, - contact=tilewe.B01, - to_tile=tilewe.T01 - ))) - def test_unique_illegal_move(self): board = tilewe.Board(4) # contact is valid, but tiles would be placed off the board - self.assertFalse(board.is_legal(tilewe.Move( + self.assertFalse(board.is_legal(tilewe.create_move( piece=tilewe.Z5, rotation=tilewe.NORTH, contact=tilewe.A03, @@ -85,136 +47,87 @@ def test_unique_illegal_move(self): ))) # contact is not valid - self.assertFalse(board.is_legal(tilewe.Move( + self.assertIsNone(tilewe.create_move( piece=tilewe.Z5, rotation=tilewe.NORTH, contact=tilewe.A01, to_tile=tilewe.A01 - ))) + )) # contact is invalid, and tiles would be placed off the board - self.assertFalse(board.is_legal(tilewe.Move( + self.assertIsNone(tilewe.create_move( piece=tilewe.T4, rotation=tilewe.SOUTH, contact=tilewe.C02, to_tile=tilewe.A20 - ))) + )) - self.assertFalse(board.is_legal(tilewe.Move( + self.assertRaises(TypeError, lambda: tilewe.create_move( piece=None, rotation=tilewe.EAST, contact=tilewe.B03, to_tile=tilewe.T20 - ))) + )) - self.assertFalse(board.is_legal(tilewe.Move( + self.assertRaises(TypeError, lambda: tilewe.create_move( piece=tilewe.T4, rotation=None, contact=tilewe.B03, to_tile=tilewe.T20 - ))) + )) - self.assertFalse(board.is_legal(tilewe.Move( + self.assertRaises(TypeError, lambda: tilewe.create_move( piece=tilewe.T4, rotation=tilewe.EAST, contact=None, to_tile=tilewe.T20 - ))) + )) - self.assertFalse(board.is_legal(tilewe.Move( + self.assertRaises(TypeError, lambda: tilewe.create_move( piece=tilewe.T4, rotation=tilewe.EAST, contact=tilewe.B03, to_tile=None - ))) - - def test_nonunique_illegal_move(self): - board = tilewe.Board(4) - - self.assertFalse(board.is_legal(tilewe.Move( - piece=tilewe.Z5, - rotation=tilewe.SOUTH, - contact=tilewe.A03, - to_tile=tilewe.A01 - ))) - - self.assertFalse(board.is_legal(tilewe.Move( - piece=tilewe.O4, - rotation=tilewe.WEST_F, - contact=tilewe.B01, - to_tile=tilewe.T20 - ))) - - self.assertFalse(board.is_legal(tilewe.Move( - piece=tilewe.T4, - rotation=tilewe.NORTH_F, - contact=tilewe.A02, - to_tile=tilewe.A19 - ))) - - self.assertFalse(board.is_legal(tilewe.Move( - piece=tilewe.T4, - rotation=tilewe.WEST_F, - contact=tilewe.B02, - to_tile=tilewe.T20 - ))) - - def test_nonunique_move_gen_legal(self): - board = tilewe.Board(4) - - moves = board.generate_legal_moves(unique=False) - - for move in moves: - self.assertTrue(board.is_legal(move)) - - def test_nonunique_move_gen_has_unique_move(self): - board = tilewe.Board(4) - - unique = board.generate_legal_moves(unique=True) - moves = board.generate_legal_moves(unique=False) - - for move in moves: - self.assertTrue(move.to_unique() in unique) + )) def test_n_legal_moves(self): board = tilewe.Board(4) - self.assertTrue(board.n_legal_moves(unique=True) == len(board.generate_legal_moves(unique=True))) - self.assertTrue(board.n_legal_moves(unique=False) == len(board.generate_legal_moves(unique=False))) - - def test_null_move_1_player(self): - board = tilewe.Board(1) - - self.assertEqual(board.current_player, tilewe.BLUE) - board.push_null() - self.assertEqual(board.current_player, tilewe.BLUE) - board.pop_null() - self.assertEqual(board.current_player, tilewe.BLUE) - - def test_null_move_2_player(self): - board = tilewe.Board(2) - - self.assertEqual(board.current_player, tilewe.BLUE) - board.push_null() - self.assertEqual(board.current_player, tilewe.YELLOW) - board.pop_null() - self.assertEqual(board.current_player, tilewe.BLUE) - - def test_null_move_3_player(self): - board = tilewe.Board(3) - - self.assertEqual(board.current_player, tilewe.BLUE) - board.push_null() - self.assertEqual(board.current_player, tilewe.YELLOW) - board.push_null() - self.assertEqual(board.current_player, tilewe.RED) - board.push_null() - self.assertEqual(board.current_player, tilewe.BLUE) - board.pop_null() - board.pop_null() - self.assertEqual(board.current_player, tilewe.YELLOW) - board.pop_null() - self.assertEqual(board.current_player, tilewe.BLUE) + self.assertTrue(board.n_legal_moves() == len(board.generate_legal_moves())) + + # def test_null_move_1_player(self): + # board = tilewe.Board(1) + + # self.assertEqual(board.current_player, tilewe.BLUE) + # board.push_null() + # self.assertEqual(board.current_player, tilewe.BLUE) + # board.pop_null() + # self.assertEqual(board.current_player, tilewe.BLUE) + + # def test_null_move_2_player(self): + # board = tilewe.Board(2) + + # self.assertEqual(board.current_player, tilewe.BLUE) + # board.push_null() + # self.assertEqual(board.current_player, tilewe.YELLOW) + # board.pop_null() + # self.assertEqual(board.current_player, tilewe.BLUE) + + # def test_null_move_3_player(self): + # board = tilewe.Board(3) + + # self.assertEqual(board.current_player, tilewe.BLUE) + # board.push_null() + # self.assertEqual(board.current_player, tilewe.YELLOW) + # board.push_null() + # self.assertEqual(board.current_player, tilewe.RED) + # board.push_null() + # self.assertEqual(board.current_player, tilewe.BLUE) + # board.pop_null() + # board.pop_null() + # self.assertEqual(board.current_player, tilewe.YELLOW) + # board.pop_null() + # self.assertEqual(board.current_player, tilewe.BLUE) def test_legal_move_gen_full_game(self): board = tilewe.Board(4) From 24a874e8ccfd76c24ac6f5a1f4d3963b20f99235 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Thu, 23 Nov 2023 00:03:15 -0600 Subject: [PATCH 34/55] Update style --- tilewe/__init__.py | 44 +++++++++++++++++++++++++++++--------------- tilewe/engine.py | 4 ++-- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/tilewe/__init__.py b/tilewe/__init__.py index 8e08c44..7d94874 100644 --- a/tilewe/__init__.py +++ b/tilewe/__init__.py @@ -22,33 +22,47 @@ ROTATION_NAMES: list[str] COLOR_NAMES: list[str] -def tile_to_coords(tile: Tile) -> tuple[int, int]: ... +def tile_to_coords(tile: Tile) -> tuple[int, int]: + ... -def tile_in_bounds(tile: Tile) -> bool: ... +def tile_in_bounds(tile: Tile) -> bool: + ... -def coords_to_tile(coords: tuple[int, int]) -> Tile: ... +def coords_to_tile(coords: tuple[int, int]) -> Tile: + ... -def coords_in_bounds(coords: tuple[int, int]) -> bool: ... +def coords_in_bounds(coords: tuple[int, int]) -> bool: + ... -def n_piece_tiles(piece: Piece) -> int: ... +def n_piece_tiles(piece: Piece) -> int: + ... -def n_piece_contacts(piece: Piece) -> int: ... +def n_piece_contacts(piece: Piece) -> int: + ... -def n_piece_corners(piece: Piece) -> int: ... +def n_piece_corners(piece: Piece) -> int: + ... -def piece_tiles(piece: Piece, rotation: Rotation) -> list[Tile]: ... +def piece_tiles(piece: Piece, rotation: Rotation) -> list[Tile]: + ... -def piece_contacts(piece: Piece, rotation: Rotation) -> list[Tile]: ... +def piece_contacts(piece: Piece, rotation: Rotation) -> list[Tile]: + ... -def create_move(piece: Piece, rotation: Rotation, contact: Tile, to_tile: Tile) -> Move: ... +def create_move(piece: Piece, rotation: Rotation, contact: Tile, to_tile: Tile) -> Move: + ... -def move_piece(move: Move) -> Piece: ... +def move_piece(move: Move) -> Piece: + ... -def move_rotation(move: Move) -> Rotation: ... +def move_rotation(move: Move) -> Rotation: + ... -def move_contact(move: Move) -> Tile: ... +def move_contact(move: Move) -> Tile: + ... -def move_tile(move: Move) -> Tile: ... +def move_tile(move: Move) -> Tile: + ... def move_str(move: Move) -> str: out = "" @@ -234,4 +248,4 @@ def is_legal(self, move: Move, for_player: Color=None) -> bool: "T5", "U5", "V5", "W5", "X5", "Y5", "Z5", ] -from ctilewe import * +from ctilewe import * # noqa: E402, F401, F403 diff --git a/tilewe/engine.py b/tilewe/engine.py index 56ee251..459bb6a 100644 --- a/tilewe/engine.py +++ b/tilewe/engine.py @@ -116,8 +116,8 @@ def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: def score(m: tilewe.Move): pc = tilewe.move_piece(m) return tilewe.n_piece_tiles(pc) * 100 + \ - tilewe.n_piece_corners(pc) * 10 + \ - tilewe.n_piece_contacts(pc) + tilewe.n_piece_corners(pc) * 10 + \ + tilewe.n_piece_contacts(pc) best = max(moves, key=score) From d024e1888b00c1d25460e4ee655cba81d2a6c57f Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Thu, 23 Nov 2023 00:11:59 -0600 Subject: [PATCH 35/55] Remove numpy from requirements --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fa02fb3..5d62b48 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ -numpy==1.26.2 scipy==1.11.4 \ No newline at end of file From 94bd391cb3b7e8511c84fab98fb7d6b0875a0860 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Thu, 23 Nov 2023 00:30:53 -0600 Subject: [PATCH 36/55] Use starting Elo for default tile weight engine weights --- tilewe/engine.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tilewe/engine.py b/tilewe/engine.py index 637ec77..a26427e 100644 --- a/tilewe/engine.py +++ b/tilewe/engine.py @@ -224,13 +224,22 @@ class TileWeightEngine(Engine): 'turtle': TURTLE_WEIGHTS } - def __init__(self, name: str="TileWeight", weight_map: str='wall_crawl', custom_weights: list[int | float]=None, estimated_elo: float=None): + weight_elos = { + 'wall_crawl': -10.0, + 'turtle': -40.0 + } + + def __init__(self, + name: str="TileWeight", + weight_map: str='wall_crawl', + custom_weights: list[int | float]=None, + estimated_elo: float=None): """ Current `weight_map` built-in options are 'wall_crawl' and 'turtle' Can optionally provide a custom set of weights instead """ - super().__init__(name, 0.0 if estimated_elo is None else estimated_elo) + est_elo: float = 0.0 if estimated_elo is None else estimated_elo if custom_weights is not None: if len(custom_weights) != 20 * 20: @@ -241,6 +250,9 @@ def __init__(self, name: str="TileWeight", weight_map: str='wall_crawl', custom_ if weight_map not in self.weight_maps: raise Exception("TileWeightEngine given invalid weight_map choice") self.weights = self.weight_maps[weight_map] + est_elo = self.weight_elos[weight_map] + + super().__init__(name, estimated_elo=est_elo) def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: From 8b1907b34005e9da4d11adf0141b990ace8b99f9 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Thu, 23 Nov 2023 00:33:33 -0600 Subject: [PATCH 37/55] Allow estimated Elo override for tile weight engine default weights --- tilewe/engine.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tilewe/engine.py b/tilewe/engine.py index a26427e..769d3d2 100644 --- a/tilewe/engine.py +++ b/tilewe/engine.py @@ -250,7 +250,8 @@ def __init__(self, if weight_map not in self.weight_maps: raise Exception("TileWeightEngine given invalid weight_map choice") self.weights = self.weight_maps[weight_map] - est_elo = self.weight_elos[weight_map] + if estimated_elo is None: + est_elo = self.weight_elos[weight_map] super().__init__(name, estimated_elo=est_elo) From b0229a052ab10424e9bc25c12bd3f1f5242af1d6 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Thu, 23 Nov 2023 15:59:36 -0600 Subject: [PATCH 38/55] Add estimated Elo calculation --- tilewe/elo.py | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tilewe/elo.py b/tilewe/elo.py index 2e5771b..21cfa5b 100644 --- a/tilewe/elo.py +++ b/tilewe/elo.py @@ -188,3 +188,68 @@ def compute_elo_error_margin(wins: int, draws: int, losses: int, confidence: flo max_log: float = -C * math.log10(max_recip - 1) return abs((max_log - min_log) / 2) + +def compute_estimated_elo(n: int, ppg: list[list[int]], rpg: list[list[float]], mean: float=0) -> list[float]: + """ + Computes Elo given a list of game players and results. Results should be: + - 1 for a win + - 0 for a loss + - 1/2 for a draw + + Parameters + ---------- + n : int + Total number of players + ppg : list[list[int]] + List of player indices for each game + rpg : list[list[float]] + List of player results for each game + mean : float + What the average rating should be + + Returns + ------- + Estimated Elo rating for all players + """ + + def expected_elo(results: list[float]) -> float: + wr = sum(results) / len(results) + + if wr >= 1: + return math.inf + elif wr <= 0: + return -math.inf + else: + # calculates total difference between the player and one virtual opponent + # the opponent will also have their elo adjusted so only return half of it + return -400 * math.log10(1.0 / wr - 1) / 2 + + elos = [] + games = len(ppg) + + # results per player + rpp = [[] for _ in range(n)] + + # game indices per player + gpp = [[] for _ in range(n)] + + for p in range(n): + for g in range(games): + if p in ppg[g]: + # player played in game g + gpp[p].append(g) + # player had result in game g + rpp[p].append(rpg[g][ppg[g].index(p)]) + + for p in range(n): + # update elo + elos.append(expected_elo(rpp[p])) + + # adjust ratings to prevent drift + finite_e = [e for e in elos if math.isfinite(e)] + if len(finite_e): + avg = sum(finite_e) / len(finite_e) + for i in range(n): + elos[i] += mean - avg + + return elos From b63108dbe5af6eeb76550f1075b938248e7f288b Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Thu, 23 Nov 2023 19:10:50 -0600 Subject: [PATCH 39/55] Return nan when there are no results, fix type hint for elo delta list --- tilewe/elo.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tilewe/elo.py b/tilewe/elo.py index 21cfa5b..e63798d 100644 --- a/tilewe/elo.py +++ b/tilewe/elo.py @@ -87,7 +87,7 @@ def compute_elo_adjustment_n(elos: list[float], scores: list[int], K: int = 32): player_count: int = len(elos) mod_K: float = K / (player_count - 1) - delta_elos: float = [0] * player_count + delta_elos: list[float] = [0] * player_count winning_score: int = max(scores) for player1 in range(player_count): @@ -213,6 +213,9 @@ def compute_estimated_elo(n: int, ppg: list[list[int]], rpg: list[list[float]], """ def expected_elo(results: list[float]) -> float: + if len(results) == 0: + return math.nan + wr = sum(results) / len(results) if wr >= 1: From b9ba6e8a0a6f91f61348596d004e03269bea0f94 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Thu, 23 Nov 2023 19:11:11 -0600 Subject: [PATCH 40/55] Add Elo options for tournament --- example_tournament.py | 2 +- tilewe/tournament.py | 37 ++++++++++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/example_tournament.py b/example_tournament.py index 4ed0339..79eb8e5 100644 --- a/example_tournament.py +++ b/example_tournament.py @@ -14,7 +14,7 @@ def run_tournament(): tilewe.engine.RandomEngine(), ]) - results = tournament.play(100, n_threads=multiprocessing.cpu_count(), move_seconds=15) + results = tournament.play(100, n_threads=multiprocessing.cpu_count(), move_seconds=1, elo_mode="estimated") # print the result of game 1 print(results.match_data[0].board) diff --git a/tilewe/tournament.py b/tilewe/tournament.py index 2c6d14c..ba14e78 100644 --- a/tilewe/tournament.py +++ b/tilewe/tournament.py @@ -9,7 +9,7 @@ import tilewe from tilewe.engine import Engine -from tilewe.elo import compute_elo_adjustment_n, compute_elo_error_margin +from tilewe.elo import compute_elo_adjustment_n, compute_elo_error_margin, compute_estimated_elo @dataclass class MatchData: @@ -232,7 +232,7 @@ def __init__(self, engines: list[Engine], move_seconds: int=60): self._seconds = move_seconds self.move_seconds = self._seconds - def play( + def play( # noqa: C901 self, n_games: int, n_threads: int=1, @@ -240,8 +240,8 @@ def play( move_seconds: int=None, verbose_board: bool=False, verbose_rankings: bool=True, - use_starting_elos: bool=False, - ): + elo_mode: str="estimated", + ): """ Used to launch a series of games in an initialized Tournament. @@ -269,6 +269,8 @@ def play( raise Exception("Must use at least one thread") if players_per_game < 1 or players_per_game > 4: raise Exception("Must have 1 to 4 players per game") + if elo_mode not in ["estimated", "live", "live_initial"]: + raise Exception("Elo mode must be 'estimated', 'live', or 'live_initial'") self.move_seconds = move_seconds if move_seconds is not None else self._seconds if self.move_seconds <= 0: @@ -282,7 +284,7 @@ def play( games = [0] * N totals = [0] * N - if use_starting_elos: + if elo_mode == "live_initial": elos = [0 if self.engines[i].estimated_elo is None else self.engines[i].estimated_elo for i in range(N)] else: elos = [0] * N @@ -290,12 +292,17 @@ def play( initial_elos = [i for i in elos] match_results: list[MatchData] = [] + player_inds_per_game: list[list[int]] = [] + player_results_per_game: list[list[float]] = [] + # helper for getting engine rank summaries def get_engine_rankings() -> str: len_name = max(5, min(24, max([len(x.name) for x in self.engines]) + 1)) len_score = max(6, max([math.floor(math.log10(max(1, totals[i])) + 1) for i in range(N)]) + 1) len_games = max(7, max([math.floor(math.log10(max(1, games[i])) + 1) for i in range(N)]) + 1) - len_elo = max(4, max([math.floor(math.log10(max(1, abs(elos[i]))) + 1) for i in range(N)]) + 1) + len_elo = max(4, max([math.floor( + math.log10(max(1, abs(elos[i] if math.isfinite(elos[i]) else 0))) + 1 + ) for i in range(N)]) + 1) out = f"\n{'Rank':4} {'Name':{len_name}} {'Elo':^{len_elo + 9}} {'Score':>{len_score}} {'Avg Score':>10} " out += f"{'Games':>{len_games}} {'Wins':>{len_games}} {'Draws':>{len_games}} " @@ -344,6 +351,9 @@ def get_engine_rankings() -> str: # at least one player always wins, otherwise the game crashed if len(winners) > 0: + player_inds_per_game.append(player_to_engine) + results = [0] * len(player_to_engine) + # track games played total_games += 1 for p in player_to_engine: @@ -352,9 +362,13 @@ def get_engine_rankings() -> str: # track wins and draws if len(winners) == 1: wins[winners[0]] += 1 + results[player_to_engine.index(winners[0])] = 1 else: for p in winners: draws[p] += 1 + results[player_to_engine.index(p)] = 0.5 + + player_results_per_game.append(results) # track scores and time for p, s in enumerate(scores): @@ -370,9 +384,14 @@ def get_engine_rankings() -> str: player_elos = [elos[i] for i in game_players] if board.n_players > 1: # if there are enough players, compute elo changes - delta_elos = compute_elo_adjustment_n(player_elos, player_scores, K=8) - for index, player in enumerate(game_players): - elos[player] += delta_elos[index] + if elo_mode == "estimated": + est_elos = compute_estimated_elo(N, player_inds_per_game, player_results_per_game) + delta_elos = [est_elos[i] - elos[i] for i in game_players] + elos = est_elos + else: + delta_elos = compute_elo_adjustment_n(player_elos, player_scores, K=8) + for index, player in enumerate(game_players): + elos[player] += delta_elos[index] else: delta_elos = [0 for _ in game_players] new_elos = [elos[i] for i in game_players] From 11fa8984e2b36a3ab0696ef088072f10321a049b Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Thu, 23 Nov 2023 20:45:56 -0600 Subject: [PATCH 41/55] Add erfinv implementation and remove scipy from requirements --- requirements.txt | 1 - tilewe/elo.py | 38 +++++++++++++++++++++++++++++++++++++- tilewe/tournament.py | 4 +++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5d62b48..e69de29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +0,0 @@ -scipy==1.11.4 \ No newline at end of file diff --git a/tilewe/elo.py b/tilewe/elo.py index e63798d..b7850b6 100644 --- a/tilewe/elo.py +++ b/tilewe/elo.py @@ -1,5 +1,41 @@ import math -from scipy.special import erfinv + +_c_k = { 0: 1.0 } + +def c_k(k: int) -> float: + if k < 0: + raise Exception("k cannot be less than 0") + if k in _c_k: + return _c_k[k] + + total = 0 + for m in range(k): + total += c_k(m) * c_k(k - 1 - m) / ((m + 1) * (2 * m + 1)) + + _c_k[k] = total + return total + +def erfinv(x: float) -> float: + if x <= -1: + return math.nan + elif x >= 1: + return math.nan + + last = 0.0 + out = 0.0 + k = 0 + r_pi_2 = math.sqrt(math.pi) * 0.5 + + while True: + out += c_k(k) / (2 * k + 1) * ((r_pi_2 * x) ** (2 * k + 1)) + + if abs(out - last) < 0.00001: + break + + last = out + k += 1 + + return out def elo_win_probability(elo1: float, elo2: float, C: int=400): """ diff --git a/tilewe/tournament.py b/tilewe/tournament.py index ba14e78..6cce4f9 100644 --- a/tilewe/tournament.py +++ b/tilewe/tournament.py @@ -169,7 +169,9 @@ def get_engine_rankings_display(self, sort_by: str = 'elo_end', sort_dir: str = len_names = max(5, min(24, max([len(x) for x in self.engine_names]) + 1)) len_score = max(6, max([math.floor(math.log10(max(1, self.total_scores[i])) + 1) for i in range(N)]) + 1) len_games = max(7, max([math.floor(math.log10(max(1, self.game_counts[i])) + 1) for i in range(N)]) + 1) - len_elo = max(4, max([math.floor(math.log10(max(1, abs(self.elo_end[i]))) + 1) for i in range(N)]) + 1) + len_elo = max(4, max([math.floor(math.log10(max(1, abs( + self.elo_end[i] if math.isfinite(self.elo_end[i]) else 0 + ))) + 1) for i in range(N)]) + 1) out = f"Ranking by {sort_by} {sort_dir}:\n" out += f"{'Rank':4} {'Name':{len_names}} {'Elo':^{len_elo + 9}} {'Score':>{len_score}} {'Avg Score':>10} " From 62361ff243e55e975956740c940877f38fcab383 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Thu, 23 Nov 2023 21:35:54 -0600 Subject: [PATCH 42/55] Add documentation for erfinv functions --- tilewe/elo.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tilewe/elo.py b/tilewe/elo.py index b7850b6..6aaa20d 100644 --- a/tilewe/elo.py +++ b/tilewe/elo.py @@ -3,6 +3,14 @@ _c_k = { 0: 1.0 } def c_k(k: int) -> float: + """ + Computes a memoized c_k value, used for erfinv. + + References + ---------- + https://en.wikipedia.org/wiki/Error_function#Inverse_functions + """ + if k < 0: raise Exception("k cannot be less than 0") if k in _c_k: @@ -16,6 +24,23 @@ def c_k(k: int) -> float: return total def erfinv(x: float) -> float: + """ + Inverse error function. Only defined for -1 < x < 1. + + Parameters + ---------- + x : float + Desired output of (non-inverse) error function. + + Returns + ------- + e_x : float + Input to the error function that results in an output of x + + References + ---------- + https://en.wikipedia.org/wiki/Error_function#Inverse_functions + """ if x <= -1: return math.nan elif x >= 1: From 663d4727b05c0170abab1ba93393b398b9c63599 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Thu, 23 Nov 2023 23:34:52 -0600 Subject: [PATCH 43/55] Make move class --- tilewe/__init__.py | 37 ++++------ tilewe/src/ctilewemodule.c | 146 ++++++++++++++++++++++++++++++++----- 2 files changed, 144 insertions(+), 39 deletions(-) diff --git a/tilewe/__init__.py b/tilewe/__init__.py index 7d94874..87df76d 100644 --- a/tilewe/__init__.py +++ b/tilewe/__init__.py @@ -7,7 +7,6 @@ Piece = int Rotation = int Color = int -Move = int TILES: list[Tile] PIECES: list[Piece] @@ -49,38 +48,32 @@ def piece_tiles(piece: Piece, rotation: Rotation) -> list[Tile]: def piece_contacts(piece: Piece, rotation: Rotation) -> list[Tile]: ... -def create_move(piece: Piece, rotation: Rotation, contact: Tile, to_tile: Tile) -> Move: - ... +class Move: -def move_piece(move: Move) -> Piece: - ... + def __init__(piece: Piece, rotation: Rotation, contact: Tile, to_tile: Tile): + ... -def move_rotation(move: Move) -> Rotation: - ... + @property + def piece(self) -> Piece: + ... -def move_contact(move: Move) -> Tile: - ... + @property + def rotation(self) -> Rotation: + ... -def move_tile(move: Move) -> Tile: - ... + @property + def contact(self) -> Tile: + ... -def move_str(move: Move) -> str: - out = "" - out += PIECE_NAMES[move_piece(move)] - out += ROTATION_NAMES[move_rotation(move)] - out += "-" - out += TILE_NAMES[move_contact(move)] - out += TILE_NAMES[move_tile(move)] - return out + @property + def to_tile(self) -> Tile: + ... class Board: def __init__(self, n_players: int=4): ... - def __str__(self) -> str: - ... - @property def ply(self) -> int: ... diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index a68fcf6..7bff60a 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -5,6 +5,100 @@ #include "Tilewe/Tilewe.h" +typedef struct MoveObject MoveObject; + +struct MoveObject +{ + PyObject_HEAD + Tw_Move Move; +}; + +static int Move_init(MoveObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = + { + "piece", + "rotation", + "contact", + "to_tile", + NULL + }; + + unsigned pc, rot, con, tile; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "IIII", kwlist, &pc, &rot, &con, &tile)) + { + PyErr_SetString(PyExc_AttributeError, "all parameters must be used"); + return false; + } + + self->Move = Tw_MakeMove_Safe(pc, rot, con, tile); + + if (self->Move == Tw_NoMove) + { + PyErr_SetString(PyExc_AttributeError, "move must have a valid piece, rotation, contact, and tile combination"); + return -1; + } + + return 0; +} + +static PyObject* Move_str(MoveObject* self, PyObject* Py_UNUSED(ignored)) +{ + char buf[32]; + snprintf(buf, 32, "%s%s-%s%s", + Tw_Pc_Str(Tw_Move_Pc(self->Move)), + Tw_Rot_Str(Tw_Move_Rot(self->Move)), + Tw_Tile_Str(Tw_Move_Con(self->Move)), + Tw_Tile_Str(Tw_Move_ToTile(self->Move)) + ); + + return PyUnicode_FromString(buf); +} + +static PyObject* Move_Piece(MoveObject* self, void* closure) +{ + return PyLong_FromLong(Tw_Move_Pc(self->Move)); +} + +static PyObject* Move_Rotation(MoveObject* self, void* closure) +{ + return PyLong_FromLong(Tw_Move_Rot(self->Move)); +} + +static PyObject* Move_Contact(MoveObject* self, void* closure) +{ + return PyLong_FromLong(Tw_Move_Con(self->Move)); +} + +static PyObject* Move_Tile(MoveObject* self, void* closure) +{ + return PyLong_FromLong(Tw_Move_ToTile(self->Move)); +} + +static PyGetSetDef Move_getsets[] = +{ + { "piece", Move_Piece, NULL, "Gets the move piece", NULL }, + { "rotation", Move_Rotation, NULL, "Gets the move rotation", NULL }, + { "contact", Move_Contact, NULL, "Gets the move contact tile", NULL }, + { "to_tile", Move_Tile, NULL, "Gets the move open corner", NULL }, + { NULL } +}; + +static PyTypeObject MoveType = +{ + .ob_base = PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "ctilewe.Move", + .tp_doc = PyDoc_STR("Representation of a Move."), + .tp_basicsize = sizeof(MoveObject), + .tp_itemsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_new = PyType_GenericNew, + .tp_init = Move_init, + .tp_str = Move_str, + .tp_repr = Move_str, + .tp_getset = Move_getsets +}; + typedef struct BoardObject BoardObject; struct BoardObject @@ -119,11 +213,10 @@ static PyObject* Board_GenMoves(BoardObject* self, PyObject* Py_UNUSED(ignored)) for (int i = 0; i < moves.Count; i++) { - PyList_SetItem( - list, - i, - PyLong_FromUnsignedLong((unsigned long) moves.Elements[i]) - ); + MoveObject* mv = PyObject_New(MoveObject, &MoveType); + mv->Move = moves.Elements[i]; + + PyList_SetItem(list, i, mv); } return list; @@ -137,14 +230,20 @@ static PyObject* Board_Push(BoardObject* self, PyObject* args, PyObject* kwds) NULL }; - unsigned long long move; + MoveObject* move; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "L", kwlist, &move)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &move)) { return NULL; } - Tw_Board_Push(&self->Board, (Tw_Move) move); + if (!PyObject_TypeCheck(move, &MoveType)) + { + PyErr_SetString(PyExc_AttributeError, "Must be a move"); + return NULL; + } + + Tw_Board_Push(&self->Board, move->Move); Py_RETURN_NONE; } @@ -199,7 +298,7 @@ static bool ForPlayerArgHandler(BoardObject* self, PyObject* args, PyObject* kwd } // TODO use better way that doesn't duplicate so much code -static bool ForPlayerAndMoveArgHandler(BoardObject* self, PyObject* args, PyObject* kwds, unsigned* move, int* player) +static bool ForPlayerAndMoveArgHandler(BoardObject* self, PyObject* args, PyObject* kwds, Tw_Move* move, int* player) { static const char* kwlist[] = { @@ -208,15 +307,24 @@ static bool ForPlayerAndMoveArgHandler(BoardObject* self, PyObject* args, PyObje NULL }; + MoveObject* moveObj; *player = Tw_Color_None; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "I|i", kwlist, move, player)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|i", kwlist, &moveObj, player)) { *move = (unsigned) Tw_NoMove; *player = -1; return false; } + if (!PyObject_TypeCheck(moveObj, &MoveType)) + { + PyErr_SetString(PyExc_AttributeError, "Must be a move"); + return false; + } + + *move = moveObj->Move; + if (*player == Tw_Color_None) { *player = self->Board.CurTurn; @@ -343,13 +451,13 @@ static PyObject* Board_NumPlayerCorners(BoardObject* self, PyObject* args, PyObj static PyObject* Board_IsLegal(BoardObject* self, PyObject* args, PyObject* kwds) { int player; - unsigned move; + Tw_Move move; if (!ForPlayerAndMoveArgHandler(self, args, kwds, &move, &player)) { return NULL; } - return PyBool_FromLong(Tw_Board_IsLegalForPlayer(&self->Board, (Tw_Color) player, (Tw_Move) move)); + return PyBool_FromLong(Tw_Board_IsLegalForPlayer(&self->Board, (Tw_Color) player, move)); } static PyObject* Board_Pop(BoardObject* self, PyObject* Py_UNUSED(ignored)) @@ -783,11 +891,6 @@ static PyMethodDef TileweMethods[] = { "n_piece_corners", Tilewe_NumPcCorners, METH_VARARGS | METH_KEYWORDS, "Gets number of corners in a piece" }, { "piece_tiles", Tilewe_PcTiles, METH_VARARGS | METH_KEYWORDS, "Gets tiles in a rotated piece" }, { "piece_contacts", Tilewe_PcContacts, METH_VARARGS | METH_KEYWORDS, "Gets contacts in a rotated piece" }, - { "move_piece", Tilewe_MovePc, METH_VARARGS | METH_KEYWORDS, "Gets the piece used in a move" }, - { "move_rotation", Tilewe_MoveRot, METH_VARARGS | METH_KEYWORDS, "Gets the piece rotation used in a move" }, - { "move_contact", Tilewe_MoveCon, METH_VARARGS | METH_KEYWORDS, "Gets the contact tile used in a move" }, - { "move_tile", Tilewe_MoveTile, METH_VARARGS | METH_KEYWORDS, "Gets the open corner used in a move" }, - { "create_move", Tilewe_CreateMove, METH_VARARGS | METH_KEYWORDS, "Creates a move from a piece, rotation, contact, and tile" }, { NULL, NULL, 0, NULL } }; @@ -807,6 +910,7 @@ PyMODINIT_FUNC PyInit_ctilewe(void) PyObject* m; if (PyType_Ready(&BoardType) < 0) return NULL; + if (PyType_Ready(&MoveType) < 0) return NULL; if (!(m = PyModule_Create(&TileweModule))) return NULL; @@ -818,5 +922,13 @@ PyMODINIT_FUNC PyInit_ctilewe(void) return NULL; } + Py_INCREF(&MoveType); + if (PyModule_AddObject(m, "Move", (PyObject*) &MoveType) < 0) + { + Py_DECREF(&MoveType); + Py_DECREF(m); + return NULL; + } + return m; } From 74fc409e2cfa04fb844a15fc7e61099a7262413f Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Fri, 24 Nov 2023 00:21:22 -0600 Subject: [PATCH 44/55] Update pytest, fix C move/board functions --- tilewe/src/ctilewemodule.c | 11 +++++------ tilewe/tests/test_gameplay.py | 4 ++-- tilewe/tests/test_moves.py | 22 +++++++++++----------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index 7bff60a..079e54c 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -28,7 +28,7 @@ static int Move_init(MoveObject* self, PyObject* args, PyObject* kwds) if (!PyArg_ParseTupleAndKeywords(args, kwds, "IIII", kwlist, &pc, &rot, &con, &tile)) { PyErr_SetString(PyExc_AttributeError, "all parameters must be used"); - return false; + return -1; } self->Move = Tw_MakeMove_Safe(pc, rot, con, tile); @@ -153,11 +153,10 @@ static PyObject* Board_Moves(BoardObject* self, void* closure) for (int i = 0; i < self->Board.Ply; i++) { - PyList_SetItem( - list, - i, - PyLong_FromUnsignedLong(self->Board.History[i].Move) - ); + MoveObject* mv = PyObject_New(MoveObject, &MoveType); + mv->Move = self->Board.History[i].Move; + + PyList_SetItem(list, i, mv); } return list; diff --git a/tilewe/tests/test_gameplay.py b/tilewe/tests/test_gameplay.py index 5f0d0fb..81470bb 100644 --- a/tilewe/tests/test_gameplay.py +++ b/tilewe/tests/test_gameplay.py @@ -39,7 +39,7 @@ def test_finished_game_state(self): # play a game until the state is marked finished tracked_ply = 0 while not board.finished: - board.push(random.choice(sorted(board.generate_legal_moves(), key=lambda m: tilewe.move_str(m)))) + board.push(random.choice(sorted(board.generate_legal_moves(), key=lambda m: str(m)))) # assert that the game finishes before 84 moves (i.e. no infinite loop) tracked_ply += 1 @@ -68,7 +68,7 @@ def test_finished_game_state(self): 'T4w-b2b14', 'Z4nf-a1n17', 'F5e-b3o10', 'L3n-b1f1', 'U5n-a2q17', 'Z5ef-a3o7', 'T5s-c1f4', 'I2e-a1g20', 'T5e-c3m8', 'L4nf-b3c3', 'L5wf-d1n19', 'N5w-a2n14', 'T4n-a2r12' ] - all_moves = [tilewe.move_str(move) for move in board.moves] + all_moves = [str(move) for move in board.moves] # assert that the expected game was played unexpected_game_msg = "Expected game not played, was generate_legal_moves() changed intentionally?" diff --git a/tilewe/tests/test_moves.py b/tilewe/tests/test_moves.py index ea54069..02d0e34 100644 --- a/tilewe/tests/test_moves.py +++ b/tilewe/tests/test_moves.py @@ -7,28 +7,28 @@ class TestMoves(unittest.TestCase): def test_unique_legal_move(self): board = tilewe.Board(4) - self.assertTrue(board.is_legal(tilewe.create_move( + self.assertTrue(board.is_legal(tilewe.Move( piece=tilewe.Z5, rotation=tilewe.NORTH, contact=tilewe.A03, to_tile=tilewe.A20 ))) - self.assertTrue(board.is_legal(tilewe.create_move( + self.assertTrue(board.is_legal(tilewe.Move( piece=tilewe.Z5, rotation=tilewe.EAST, contact=tilewe.A01, to_tile=tilewe.A01 ))) - self.assertTrue(board.is_legal(tilewe.create_move( + self.assertTrue(board.is_legal(tilewe.Move( piece=tilewe.T4, rotation=tilewe.NORTH, contact=tilewe.A02, to_tile=tilewe.A20 ))) - self.assertTrue(board.is_legal(tilewe.create_move( + self.assertTrue(board.is_legal(tilewe.Move( piece=tilewe.T4, rotation=tilewe.EAST, contact=tilewe.B03, @@ -39,7 +39,7 @@ def test_unique_illegal_move(self): board = tilewe.Board(4) # contact is valid, but tiles would be placed off the board - self.assertFalse(board.is_legal(tilewe.create_move( + self.assertFalse(board.is_legal(tilewe.Move( piece=tilewe.Z5, rotation=tilewe.NORTH, contact=tilewe.A03, @@ -47,7 +47,7 @@ def test_unique_illegal_move(self): ))) # contact is not valid - self.assertIsNone(tilewe.create_move( + self.assertRaises(AttributeError, lambda: tilewe.Move( piece=tilewe.Z5, rotation=tilewe.NORTH, contact=tilewe.A01, @@ -55,35 +55,35 @@ def test_unique_illegal_move(self): )) # contact is invalid, and tiles would be placed off the board - self.assertIsNone(tilewe.create_move( + self.assertRaises(AttributeError, lambda: tilewe.Move( piece=tilewe.T4, rotation=tilewe.SOUTH, contact=tilewe.C02, to_tile=tilewe.A20 )) - self.assertRaises(TypeError, lambda: tilewe.create_move( + self.assertRaises(AttributeError, lambda: tilewe.Move( piece=None, rotation=tilewe.EAST, contact=tilewe.B03, to_tile=tilewe.T20 )) - self.assertRaises(TypeError, lambda: tilewe.create_move( + self.assertRaises(AttributeError, lambda: tilewe.Move( piece=tilewe.T4, rotation=None, contact=tilewe.B03, to_tile=tilewe.T20 )) - self.assertRaises(TypeError, lambda: tilewe.create_move( + self.assertRaises(AttributeError, lambda: tilewe.Move( piece=tilewe.T4, rotation=tilewe.EAST, contact=None, to_tile=tilewe.T20 )) - self.assertRaises(TypeError, lambda: tilewe.create_move( + self.assertRaises(AttributeError, lambda: tilewe.Move( piece=tilewe.T4, rotation=tilewe.EAST, contact=tilewe.B03, From 1f100987656a3ceae571667f180339fc2870a82a Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Fri, 24 Nov 2023 00:52:20 -0600 Subject: [PATCH 45/55] Add move pickling --- tilewe/src/ctilewemodule.c | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index 079e54c..be04a17 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -42,6 +42,35 @@ static int Move_init(MoveObject* self, PyObject* args, PyObject* kwds) return 0; } +static PyObject* Move_getstate(MoveObject* self, PyObject* Py_UNUSED(ignored)) +{ + return Py_BuildValue("I", (unsigned) self->Move); +} + +static PyObject* Move_setstate(MoveObject* self, PyObject* state) +{ + if (!PyLong_CheckExact(state)) + { + PyErr_SetString(PyExc_ValueError, "Pickled object is not an int."); + return NULL; + } + + Tw_Move move = (Tw_Move) PyLong_AsUnsignedLong(state); + + if (move == Tw_NoMove || move != Tw_MakeMove_Safe( + Tw_Move_Pc(move), + Tw_Move_Rot(move), + Tw_Move_Con(move), + Tw_Move_ToTile(move) + )) + { + PyErr_SetString(PyExc_ValueError, "Pickled object is not a valid move."); + return NULL; + } + + Py_RETURN_NONE; +} + static PyObject* Move_str(MoveObject* self, PyObject* Py_UNUSED(ignored)) { char buf[32]; @@ -84,6 +113,13 @@ static PyGetSetDef Move_getsets[] = { NULL } }; +static PyMethodDef Move_methods[] = +{ + { "__getstate__", Move_getstate, METH_NOARGS, "Pickle the move" }, + { "__setstate__", Move_setstate, METH_O, "Un-pickle the move" }, + { NULL } +}; + static PyTypeObject MoveType = { .ob_base = PyVarObject_HEAD_INIT(NULL, 0) @@ -96,6 +132,7 @@ static PyTypeObject MoveType = .tp_init = Move_init, .tp_str = Move_str, .tp_repr = Move_str, + .tp_methods = Move_methods, .tp_getset = Move_getsets }; From e3ea28ec48af7021c21b3c0764e57dac6c69a285 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Fri, 24 Nov 2023 00:52:49 -0600 Subject: [PATCH 46/55] Modify engines to use move properties --- tilewe/engine.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tilewe/engine.py b/tilewe/engine.py index 769d3d2..97a773f 100644 --- a/tilewe/engine.py +++ b/tilewe/engine.py @@ -121,7 +121,7 @@ def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: random.shuffle(moves) def score(m: tilewe.Move): - pc = tilewe.move_piece(m) + pc = m.piece return tilewe.n_piece_tiles(pc) * 100 + \ tilewe.n_piece_corners(pc) * 10 + \ tilewe.n_piece_contacts(pc) @@ -262,8 +262,8 @@ def on_search(self, board: tilewe.Board, _seconds: float) -> tilewe.Move: def evaluate_move_weight(move: tilewe.Move) -> float: total: float = 0 - offset = tilewe.move_tile(move) - tilewe.move_contact(move) - for tile in tilewe.piece_tiles(tilewe.move_piece(move), tilewe.move_rotation(move)): + offset = move.to_tile - move.contact + for tile in tilewe.piece_tiles(move.piece, move.rotation): total += self.weights[tile + offset] return total @@ -274,6 +274,6 @@ def evaluate_move_weight(move: tilewe.Move) -> float: if board.ply < board.n_players: # prune to one corner to reduce moves to evaluate corner = board.player_corners(cur_player)[0] - moves = [i for i in moves if tilewe.move_tile(i) == corner] + moves = [i for i in moves if i.to_tile == corner] return max(moves, key=evaluate_move_weight) From 1e2b18b24be4caabc3d133031e59c8b3ca6aa43d Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Fri, 24 Nov 2023 01:13:13 -0600 Subject: [PATCH 47/55] Add move comparison and hashing --- tilewe/src/ctilewemodule.c | 44 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tilewe/src/ctilewemodule.c b/tilewe/src/ctilewemodule.c index be04a17..d47f784 100644 --- a/tilewe/src/ctilewemodule.c +++ b/tilewe/src/ctilewemodule.c @@ -71,6 +71,13 @@ static PyObject* Move_setstate(MoveObject* self, PyObject* state) Py_RETURN_NONE; } +static PyObject* Move_richcompare(MoveObject* self, PyObject* obj, int op); + +static PyObject* Move_hash(MoveObject* self, PyObject* Py_UNUSED(ignored)) +{ + return PyLong_FromUnsignedLong(self->Move); +} + static PyObject* Move_str(MoveObject* self, PyObject* Py_UNUSED(ignored)) { char buf[32]; @@ -132,10 +139,47 @@ static PyTypeObject MoveType = .tp_init = Move_init, .tp_str = Move_str, .tp_repr = Move_str, + .tp_hash = Move_hash, + .tp_richcompare = Move_richcompare, .tp_methods = Move_methods, .tp_getset = Move_getsets }; +static PyObject* Move_richcompare(MoveObject* self, PyObject* obj, int op) +{ + if (!PyObject_TypeCheck(obj, &MoveType)) + { + return PyBool_FromLong(false); + } + + PyObject* out = NULL; + + switch (op) + { + case Py_LT: + out = self->Move < ((MoveObject*) obj)->Move ? Py_True : Py_False; + break; + case Py_LE: + out = self->Move <= ((MoveObject*) obj)->Move ? Py_True : Py_False; + break; + case Py_EQ: + out = self->Move == ((MoveObject*) obj)->Move ? Py_True : Py_False; + break; + case Py_NE: + out = self->Move != ((MoveObject*) obj)->Move ? Py_True : Py_False; + break; + case Py_GT: + out = self->Move > ((MoveObject*) obj)->Move ? Py_True : Py_False; + break; + case Py_GE: + out = self->Move >= ((MoveObject*) obj)->Move ? Py_True : Py_False; + break; + } + + Py_XINCREF(out); + return out; +} + typedef struct BoardObject BoardObject; struct BoardObject From 56db6255fac27ec9bac7fcbf23389c92ecced937 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Mon, 27 Nov 2023 17:20:39 -0600 Subject: [PATCH 48/55] Update types and constants --- tilewe/__init__.py | 189 +++++++++++++++++++++------------------------ 1 file changed, 88 insertions(+), 101 deletions(-) diff --git a/tilewe/__init__.py b/tilewe/__init__.py index 87df76d..d16ab04 100644 --- a/tilewe/__init__.py +++ b/tilewe/__init__.py @@ -8,18 +8,96 @@ Rotation = int Color = int -TILES: list[Tile] -PIECES: list[Piece] -ROTATIONS: list[Rotation] -COLOR: list[Color] +# game details related constant declarations +TILES = [ + A01, B01, C01, D01, E01, F01, G01, H01, I01, J01, K01, L01, M01, N01, O01, P01, Q01, R01, S01, T01, + A02, B02, C02, D02, E02, F02, G02, H02, I02, J02, K02, L02, M02, N02, O02, P02, Q02, R02, S02, T02, + A03, B03, C03, D03, E03, F03, G03, H03, I03, J03, K03, L03, M03, N03, O03, P03, Q03, R03, S03, T03, + A04, B04, C04, D04, E04, F04, G04, H04, I04, J04, K04, L04, M04, N04, O04, P04, Q04, R04, S04, T04, + A05, B05, C05, D05, E05, F05, G05, H05, I05, J05, K05, L05, M05, N05, O05, P05, Q05, R05, S05, T05, + A06, B06, C06, D06, E06, F06, G06, H06, I06, J06, K06, L06, M06, N06, O06, P06, Q06, R06, S06, T06, + A07, B07, C07, D07, E07, F07, G07, H07, I07, J07, K07, L07, M07, N07, O07, P07, Q07, R07, S07, T07, + A08, B08, C08, D08, E08, F08, G08, H08, I08, J08, K08, L08, M08, N08, O08, P08, Q08, R08, S08, T08, + A09, B09, C09, D09, E09, F09, G09, H09, I09, J09, K09, L09, M09, N09, O09, P09, Q09, R09, S09, T09, + A10, B10, C10, D10, E10, F10, G10, H10, I10, J10, K10, L10, M10, N10, O10, P10, Q10, R10, S10, T10, + A11, B11, C11, D11, E11, F11, G11, H11, I11, J11, K11, L11, M11, N11, O11, P11, Q11, R11, S11, T11, + A12, B12, C12, D12, E12, F12, G12, H12, I12, J12, K12, L12, M12, N12, O12, P12, Q12, R12, S12, T12, + A13, B13, C13, D13, E13, F13, G13, H13, I13, J13, K13, L13, M13, N13, O13, P13, Q13, R13, S13, T13, + A14, B14, C14, D14, E14, F14, G14, H14, I14, J14, K14, L14, M14, N14, O14, P14, Q14, R14, S14, T14, + A15, B15, C15, D15, E15, F15, G15, H15, I15, J15, K15, L15, M15, N15, O15, P15, Q15, R15, S15, T15, + A16, B16, C16, D16, E16, F16, G16, H16, I16, J16, K16, L16, M16, N16, O16, P16, Q16, R16, S16, T16, + A17, B17, C17, D17, E17, F17, G17, H17, I17, J17, K17, L17, M17, N17, O17, P17, Q17, R17, S17, T17, + A18, B18, C18, D18, E18, F18, G18, H18, I18, J18, K18, L18, M18, N18, O18, P18, Q18, R18, S18, T18, + A19, B19, C19, D19, E19, F19, G19, H19, I19, J19, K19, L19, M19, N19, O19, P19, Q19, R19, S19, T19, + A20, B20, C20, D20, E20, F20, G20, H20, I20, J20, K20, L20, M20, N20, O20, P20, Q20, R20, S20, T20 +] = [ + Tile(i) for i in range(20 * 20) +] + +TILE_COORDS = [ + (x, y) for y in range(20) for x in range(20) +] + +TILE_NAMES = [ + "a1", "b1", "c1", "d1", "e1", "f1", "g1", "h1", "i1", "j1", "k1", "l1", "m1", "n1", "o1", "p1", "q1", "r1", "s1", "t1", # noqa: 501 + "a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2", "i2", "j2", "k2", "l2", "m2", "n2", "o2", "p2", "q2", "r2", "s2", "t2", # noqa: 501 + "a3", "b3", "c3", "d3", "e3", "f3", "g3", "h3", "i3", "j3", "k3", "l3", "m3", "n3", "o3", "p3", "q3", "r3", "s3", "t3", # noqa: 501 + "a4", "b4", "c4", "d4", "e4", "f4", "g4", "h4", "i4", "j4", "k4", "l4", "m4", "n4", "o4", "p4", "q4", "r4", "s4", "t4", # noqa: 501 + "a5", "b5", "c5", "d5", "e5", "f5", "g5", "h5", "i5", "j5", "k5", "l5", "m5", "n5", "o5", "p5", "q5", "r5", "s5", "t5", # noqa: 501 + "a6", "b6", "c6", "d6", "e6", "f6", "g6", "h6", "i6", "j6", "k6", "l6", "m6", "n6", "o6", "p6", "q6", "r6", "s6", "t6", # noqa: 501 + "a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7", "i7", "j7", "k7", "l7", "m7", "n7", "o7", "p7", "q7", "r7", "s7", "t7", # noqa: 501 + "a8", "b8", "c8", "d8", "e8", "f8", "g8", "h8", "i8", "j8", "k8", "l8", "m8", "n8", "o8", "p8", "q8", "r8", "s8", "t8", # noqa: 501 + "a9", "b9", "c9", "d9", "e9", "f9", "g9", "h9", "i9", "j9", "k9", "l9", "m9", "n9", "o9", "p9", "q9", "r9", "s9", "t9", # noqa: 501 + "a10", "b10", "c10", "d10", "e10", "f10", "g10", "h10", "i10", "j10", "k10", "l10", "m10", "n10", "o10", "p10", "q10", "r10", "s10", "t10", # noqa: 501 + "a11", "b11", "c11", "d11", "e11", "f11", "g11", "h11", "i11", "j11", "k11", "l11", "m11", "n11", "o11", "p11", "q11", "r11", "s11", "t11", # noqa: 501 + "a12", "b12", "c12", "d12", "e12", "f12", "g12", "h12", "i12", "j12", "k12", "l12", "m12", "n12", "o12", "p12", "q12", "r12", "s12", "t12", # noqa: 501 + "a13", "b13", "c13", "d13", "e13", "f13", "g13", "h13", "i13", "j13", "k13", "l13", "m13", "n13", "o13", "p13", "q13", "r13", "s13", "t13", # noqa: 501 + "a14", "b14", "c14", "d14", "e14", "f14", "g14", "h14", "i14", "j14", "k14", "l14", "m14", "n14", "o14", "p14", "q14", "r14", "s14", "t14", # noqa: 501 + "a15", "b15", "c15", "d15", "e15", "f15", "g15", "h15", "i15", "j15", "k15", "l15", "m15", "n15", "o15", "p15", "q15", "r15", "s15", "t15", # noqa: 501 + "a16", "b16", "c16", "d16", "e16", "f16", "g16", "h16", "i16", "j16", "k16", "l16", "m16", "n16", "o16", "p16", "q16", "r16", "s16", "t16", # noqa: 501 + "a17", "b17", "c17", "d17", "e17", "f17", "g17", "h17", "i17", "j17", "k17", "l17", "m17", "n17", "o17", "p17", "q17", "r17", "s17", "t17", # noqa: 501 + "a18", "b18", "c18", "d18", "e18", "f18", "g18", "h18", "i18", "j18", "k18", "l18", "m18", "n18", "o18", "p18", "q18", "r18", "s18", "t18", # noqa: 501 + "a19", "b19", "c19", "d19", "e19", "f19", "g19", "h19", "i19", "j19", "k19", "l19", "m19", "n19", "o19", "p19", "q19", "r19", "s19", "t19", # noqa: 501 + "a20", "b20", "c20", "d20", "e20", "f20", "g20", "h20", "i20", "j20", "k20", "l20", "m20", "n20", "o20", "p20", "q20", "r20", "s20", "t20" # noqa: 501 +] + +ROTATIONS = [ + NORTH, EAST, SOUTH, WEST, NORTH_F, EAST_F, SOUTH_F, WEST_F +] = [Rotation(x) for x in range(8)] + +ROTATION_COUNT: int = len(ROTATIONS) + +ROTATION_NAMES = [ + 'n', 'e', 's', 'w', 'nf', 'ef', 'sf', 'wf' +] + +COLORS = [ + BLUE, YELLOW, RED, GREEN +] = [Color(x) for x in range(4)] -NO_PIECES: Piece -NO_COLOR: Color +NO_COLOR: Color = Color(len(COLORS)) -TILE_NAMES: list[str] -PIECE_NAMES: list[str] -ROTATION_NAMES: list[str] -COLOR_NAMES: list[str] +COLOR_COUNT: int = len(COLORS) + +COLOR_NAMES = [ + 'blue', 'yellow', 'red', 'green' +] + +PIECES = [ + O1, I2, I3, L3, O4, I4, L4, + Z4, T4, F5, I5, L5, N5, P5, + T5, U5, V5, W5, X5, Y5, Z5, +] = [Piece(x) for x in range(21)] + +NO_PIECE: Piece = Piece(len(PIECES)) + +PIECE_COUNT: int = len(PIECES) + +PIECE_NAMES = [ + "O1", "I2", "I3", "L3", "O4", "I4", "L4", + "Z4", "T4", "F5", "I5", "L5", "N5", "P5", + "T5", "U5", "V5", "W5", "X5", "Y5", "Z5", +] def tile_to_coords(tile: Tile) -> tuple[int, int]: ... @@ -150,95 +228,4 @@ def is_legal(self, move: Move, for_player: Color=None) -> bool: """Whether a move is legal for a player""" ... -Tile = int -Piece = int -Rotation = int -Color = int -Move = int - -# game details related constant declarations -TILES = [ - A01, B01, C01, D01, E01, F01, G01, H01, I01, J01, K01, L01, M01, N01, O01, P01, Q01, R01, S01, T01, - A02, B02, C02, D02, E02, F02, G02, H02, I02, J02, K02, L02, M02, N02, O02, P02, Q02, R02, S02, T02, - A03, B03, C03, D03, E03, F03, G03, H03, I03, J03, K03, L03, M03, N03, O03, P03, Q03, R03, S03, T03, - A04, B04, C04, D04, E04, F04, G04, H04, I04, J04, K04, L04, M04, N04, O04, P04, Q04, R04, S04, T04, - A05, B05, C05, D05, E05, F05, G05, H05, I05, J05, K05, L05, M05, N05, O05, P05, Q05, R05, S05, T05, - A06, B06, C06, D06, E06, F06, G06, H06, I06, J06, K06, L06, M06, N06, O06, P06, Q06, R06, S06, T06, - A07, B07, C07, D07, E07, F07, G07, H07, I07, J07, K07, L07, M07, N07, O07, P07, Q07, R07, S07, T07, - A08, B08, C08, D08, E08, F08, G08, H08, I08, J08, K08, L08, M08, N08, O08, P08, Q08, R08, S08, T08, - A09, B09, C09, D09, E09, F09, G09, H09, I09, J09, K09, L09, M09, N09, O09, P09, Q09, R09, S09, T09, - A10, B10, C10, D10, E10, F10, G10, H10, I10, J10, K10, L10, M10, N10, O10, P10, Q10, R10, S10, T10, - A11, B11, C11, D11, E11, F11, G11, H11, I11, J11, K11, L11, M11, N11, O11, P11, Q11, R11, S11, T11, - A12, B12, C12, D12, E12, F12, G12, H12, I12, J12, K12, L12, M12, N12, O12, P12, Q12, R12, S12, T12, - A13, B13, C13, D13, E13, F13, G13, H13, I13, J13, K13, L13, M13, N13, O13, P13, Q13, R13, S13, T13, - A14, B14, C14, D14, E14, F14, G14, H14, I14, J14, K14, L14, M14, N14, O14, P14, Q14, R14, S14, T14, - A15, B15, C15, D15, E15, F15, G15, H15, I15, J15, K15, L15, M15, N15, O15, P15, Q15, R15, S15, T15, - A16, B16, C16, D16, E16, F16, G16, H16, I16, J16, K16, L16, M16, N16, O16, P16, Q16, R16, S16, T16, - A17, B17, C17, D17, E17, F17, G17, H17, I17, J17, K17, L17, M17, N17, O17, P17, Q17, R17, S17, T17, - A18, B18, C18, D18, E18, F18, G18, H18, I18, J18, K18, L18, M18, N18, O18, P18, Q18, R18, S18, T18, - A19, B19, C19, D19, E19, F19, G19, H19, I19, J19, K19, L19, M19, N19, O19, P19, Q19, R19, S19, T19, - A20, B20, C20, D20, E20, F20, G20, H20, I20, J20, K20, L20, M20, N20, O20, P20, Q20, R20, S20, T20 -] = [ - Tile(i) for i in range(20 * 20) -] - -TILE_COORDS = [ - (x, y) for y in range(20) for x in range(20) -] - -TILE_NAMES = [ - "a1", "b1", "c1", "d1", "e1", "f1", "g1", "h1", "i1", "j1", "k1", "l1", "m1", "n1", "o1", "p1", "q1", "r1", "s1", "t1", # noqa: 501 - "a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2", "i2", "j2", "k2", "l2", "m2", "n2", "o2", "p2", "q2", "r2", "s2", "t2", # noqa: 501 - "a3", "b3", "c3", "d3", "e3", "f3", "g3", "h3", "i3", "j3", "k3", "l3", "m3", "n3", "o3", "p3", "q3", "r3", "s3", "t3", # noqa: 501 - "a4", "b4", "c4", "d4", "e4", "f4", "g4", "h4", "i4", "j4", "k4", "l4", "m4", "n4", "o4", "p4", "q4", "r4", "s4", "t4", # noqa: 501 - "a5", "b5", "c5", "d5", "e5", "f5", "g5", "h5", "i5", "j5", "k5", "l5", "m5", "n5", "o5", "p5", "q5", "r5", "s5", "t5", # noqa: 501 - "a6", "b6", "c6", "d6", "e6", "f6", "g6", "h6", "i6", "j6", "k6", "l6", "m6", "n6", "o6", "p6", "q6", "r6", "s6", "t6", # noqa: 501 - "a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7", "i7", "j7", "k7", "l7", "m7", "n7", "o7", "p7", "q7", "r7", "s7", "t7", # noqa: 501 - "a8", "b8", "c8", "d8", "e8", "f8", "g8", "h8", "i8", "j8", "k8", "l8", "m8", "n8", "o8", "p8", "q8", "r8", "s8", "t8", # noqa: 501 - "a9", "b9", "c9", "d9", "e9", "f9", "g9", "h9", "i9", "j9", "k9", "l9", "m9", "n9", "o9", "p9", "q9", "r9", "s9", "t9", # noqa: 501 - "a10", "b10", "c10", "d10", "e10", "f10", "g10", "h10", "i10", "j10", "k10", "l10", "m10", "n10", "o10", "p10", "q10", "r10", "s10", "t10", # noqa: 501 - "a11", "b11", "c11", "d11", "e11", "f11", "g11", "h11", "i11", "j11", "k11", "l11", "m11", "n11", "o11", "p11", "q11", "r11", "s11", "t11", # noqa: 501 - "a12", "b12", "c12", "d12", "e12", "f12", "g12", "h12", "i12", "j12", "k12", "l12", "m12", "n12", "o12", "p12", "q12", "r12", "s12", "t12", # noqa: 501 - "a13", "b13", "c13", "d13", "e13", "f13", "g13", "h13", "i13", "j13", "k13", "l13", "m13", "n13", "o13", "p13", "q13", "r13", "s13", "t13", # noqa: 501 - "a14", "b14", "c14", "d14", "e14", "f14", "g14", "h14", "i14", "j14", "k14", "l14", "m14", "n14", "o14", "p14", "q14", "r14", "s14", "t14", # noqa: 501 - "a15", "b15", "c15", "d15", "e15", "f15", "g15", "h15", "i15", "j15", "k15", "l15", "m15", "n15", "o15", "p15", "q15", "r15", "s15", "t15", # noqa: 501 - "a16", "b16", "c16", "d16", "e16", "f16", "g16", "h16", "i16", "j16", "k16", "l16", "m16", "n16", "o16", "p16", "q16", "r16", "s16", "t16", # noqa: 501 - "a17", "b17", "c17", "d17", "e17", "f17", "g17", "h17", "i17", "j17", "k17", "l17", "m17", "n17", "o17", "p17", "q17", "r17", "s17", "t17", # noqa: 501 - "a18", "b18", "c18", "d18", "e18", "f18", "g18", "h18", "i18", "j18", "k18", "l18", "m18", "n18", "o18", "p18", "q18", "r18", "s18", "t18", # noqa: 501 - "a19", "b19", "c19", "d19", "e19", "f19", "g19", "h19", "i19", "j19", "k19", "l19", "m19", "n19", "o19", "p19", "q19", "r19", "s19", "t19", # noqa: 501 - "a20", "b20", "c20", "d20", "e20", "f20", "g20", "h20", "i20", "j20", "k20", "l20", "m20", "n20", "o20", "p20", "q20", "r20", "s20", "t20" # noqa: 501 -] - -ROTATIONS = [ - NORTH, EAST, SOUTH, WEST, NORTH_F, EAST_F, SOUTH_F, WEST_F -] = [Rotation(x) for x in range(8)] - -ROTATION_NAMES = [ - 'n', 'e', 's', 'w', 'nf', 'ef', 'sf', 'wf' -] - -COLORS = [ - BLUE, YELLOW, RED, GREEN -] = [Color(x) for x in range(4)] - -NO_COLOR: Color = len(COLORS) - -COLOR_NAMES = [ - 'blue', 'yellow', 'red', 'green' -] - -PIECES = [ - O1, I2, I3, L3, O4, I4, L4, - Z4, T4, F5, I5, L5, N5, P5, - T5, U5, V5, W5, X5, Y5, Z5, -] = [Piece(x) for x in range(21)] - -NO_PIECE: Piece = len(PIECES) - -PIECE_NAMES = [ - "O1", "I2", "I3", "L3", "O4", "I4", "L4", - "Z4", "T4", "F5", "I5", "L5", "N5", "P5", - "T5", "U5", "V5", "W5", "X5", "Y5", "Z5", -] - from ctilewe import * # noqa: E402, F401, F403 From cc1a0d445794e13a8833193d3e5f8a2b138ac6d7 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Mon, 27 Nov 2023 20:45:31 -0600 Subject: [PATCH 49/55] Update C tilewe commit --- tilewe/src/tilewe | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tilewe/src/tilewe b/tilewe/src/tilewe index 16eed61..957f097 160000 --- a/tilewe/src/tilewe +++ b/tilewe/src/tilewe @@ -1 +1 @@ -Subproject commit 16eed61bd3f90fe496aaf053390df1970e09efd7 +Subproject commit 957f097e6b2eb4443bdf022a12469f9dbd2db4f6 From 067828bada140014d2950a66ca76e41ecada02c2 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Mon, 27 Nov 2023 20:47:15 -0600 Subject: [PATCH 50/55] Add wheel to dev requirements --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index aad120b..b121004 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,3 @@ +wheel flake8 pytest \ No newline at end of file From 686c46ccd9ed8594fb197362d13f8f6a9634ad6c Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Mon, 27 Nov 2023 20:48:15 -0600 Subject: [PATCH 51/55] Change version to 0.0.2 --- pyproject.toml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2b9153c..a64412c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "tilewe" -version = "0.0.1" +version = "0.0.2" authors = [ { name="Nicholas Hamilton", email="nh.contact.1@gmail.com" }, { name="Michael Conard", email="michaelaconard314@gmail.com" }, diff --git a/setup.py b/setup.py index 281dc10..ed276a6 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name='tilewe', - version='0.0.1', + version='0.0.2', packages=find_packages(), ext_modules=ext_modules ) From 64a118fac138ae0085a793c20db99dc6c7ec5a02 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Mon, 27 Nov 2023 21:11:51 -0600 Subject: [PATCH 52/55] Update GitHub workflow --- .github/workflows/python-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 7ebabbf..d75f102 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -28,7 +28,7 @@ jobs: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi # install the tilewe package locally - python -m pip install -e ./tilewe + python -m pip install . - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names From 8e4d4c8ed8153e9df6af44c0cbad1fac5fa7034b Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Mon, 27 Nov 2023 21:18:35 -0600 Subject: [PATCH 53/55] Checkout submodule in workflow --- .github/workflows/python-app.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index d75f102..1bb5a45 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -16,6 +16,8 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Checkout submodules + run: git submodule update --init --recursive - name: Set up Python 3.10 uses: actions/setup-python@v3 with: From e7a13b7bf66a2538ca0fbb6f5d6b5db12e6a71fe Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Mon, 27 Nov 2023 21:22:17 -0600 Subject: [PATCH 54/55] Reduce line length --- setup.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index ed276a6..e9b031b 100644 --- a/setup.py +++ b/setup.py @@ -3,8 +3,15 @@ ext_modules = [ Extension( name="ctilewe", - sources=["tilewe/src/ctilewemodule.c", "tilewe/src/tilewe/Source/Tilewe/Piece.c", "tilewe/src/tilewe/Source/Tilewe/Tables.c"], - include_dirs=["tilewe/src/tilewe/Source", "tilewe/src/tilewe/Source/Tilewe"], + sources=[ + "tilewe/src/ctilewemodule.c", + "tilewe/src/tilewe/Source/Tilewe/Piece.c", + "tilewe/src/tilewe/Source/Tilewe/Tables.c" + ], + include_dirs=[ + "tilewe/src/tilewe/Source", + "tilewe/src/tilewe/Source/Tilewe" + ], extra_compile_args=["-O3", "-funroll-loops"] ) ] From 8d3987284a4b8bfbfb08bfd77cce1192b2a5657a Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Mon, 27 Nov 2023 21:34:53 -0600 Subject: [PATCH 55/55] Update docstrings --- tilewe/__init__.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tilewe/__init__.py b/tilewe/__init__.py index d16ab04..408d817 100644 --- a/tilewe/__init__.py +++ b/tilewe/__init__.py @@ -100,60 +100,84 @@ ] def tile_to_coords(tile: Tile) -> tuple[int, int]: + """Converts a valid tile to xy coordinates""" ... def tile_in_bounds(tile: Tile) -> bool: + """Checks if a tile value is valid""" ... def coords_to_tile(coords: tuple[int, int]) -> Tile: + """Converts valid xy coordinates to a tile""" ... def coords_in_bounds(coords: tuple[int, int]) -> bool: + """Checks if xy coordinates are valid""" ... def n_piece_tiles(piece: Piece) -> int: + """Returns number of tiles in a piece""" ... def n_piece_contacts(piece: Piece) -> int: + """Returns number of contacts in a piece""" ... def n_piece_corners(piece: Piece) -> int: + """Returns number of open corners a piece provides""" ... def piece_tiles(piece: Piece, rotation: Rotation) -> list[Tile]: + """ + Returns piece tiles relative to a rotation, where A01 is the bottom-left + coordinate of that rotations's bounding box + """ ... def piece_contacts(piece: Piece, rotation: Rotation) -> list[Tile]: + """ + Returns piece contacts relative to a rotation, where A01 is the bottom-left + coordinate of that rotations's bounding box + """ ... class Move: + """Represents a board move""" def __init__(piece: Piece, rotation: Rotation, contact: Tile, to_tile: Tile): + """Creates a move""" ... @property def piece(self) -> Piece: + """Piece used by the move""" ... @property def rotation(self) -> Rotation: + """Rotation used by the move""" ... @property def contact(self) -> Tile: + """Contact used by the move""" ... @property def to_tile(self) -> Tile: + """Tile that the move's contact will be placed at""" ... class Board: + """Represents a tilewe board""" def __init__(self, n_players: int=4): + """Creates a board""" ... @property def ply(self) -> int: + """Current ply, where each move increments the ply""" ... @property @@ -173,10 +197,12 @@ def n_players(self) -> int: @property def scores(self) -> list[int]: + """List of each player's score""" ... @property def winners(self) -> list[int]: + """List of current winners""" ... @property