From c8abd79a1dbbbea7e2c41559d980d343746f4901 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Fri, 17 Nov 2023 19:13:54 -0600 Subject: [PATCH 1/7] Make tiles ints --- tilewe/__init__.py | 150 ++++++++++++++++++++----------------- tilewe/engine.py | 2 +- tilewe/tests/test_tiles.py | 9 +++ 3 files changed, 92 insertions(+), 69 deletions(-) create mode 100644 tilewe/tests/test_tiles.py diff --git a/tilewe/__init__.py b/tilewe/__init__.py index 16a99fa..19f0754 100644 --- a/tilewe/__init__.py +++ b/tilewe/__init__.py @@ -9,10 +9,8 @@ print_color = True -# type wrappers to make use of primitives more clear -Tile = tuple[int, int] - # internally int, so copies value not reference +Tile = int Piece = int Rotation = int Color = int @@ -41,7 +39,11 @@ 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((y, x)) for y in range(20) for x in range(20) + Tile(i) for i in range(20 * 20) +] + +TILE_COORDS = [ + (y, x) for y in range(20) for x in range(20) ] TILE_NAMES = [ @@ -67,6 +69,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] * 20 + coords[1] + +def tile_to_index(tile: Tile) -> int: + return tile + +def out_of_bounds(coords: tuple[int, int]) -> bool: + 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)] @@ -132,8 +149,10 @@ def __init__(self, name: str, pc: _Piece, rot: Rotation, shape: np.ndarray): self.name = name self.shape = np.array(shape, dtype=np.uint8) - self.tiles: list[Tile] = [] - self.contacts: list[Tile] = [] + # 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 @@ -143,7 +162,7 @@ def __init__(self, name: str, pc: _Piece, rot: Rotation, shape: np.ndarray): for x in range(W): # check each tile in piece if shape[y, x] != 0: - self.tiles.append((y, x)) + self.rel_tiles.append((y, x)) v_neighbors = 0 h_neighbors = 0 @@ -155,13 +174,15 @@ def __init__(self, name: str, pc: _Piece, rot: Rotation, shape: np.ndarray): n_neighbors = v_neighbors + h_neighbors if (n_neighbors <= 1) or (v_neighbors == 1 and h_neighbors == 1): - self.contacts.append((y, x)) + self.rel_contacts.append((y, x)) self.contact_shape[y, x] = 1 - for coord in self.contacts: - self.prps[coord] = _PieceRotationPoint(name, self, coord) + 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].corners) + 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: """ @@ -178,7 +199,7 @@ class _PieceRotationPoint: Which contact of the piece is used by this piece-rotation-point """ - def __init__(self, name: str, rot: _PieceRotation, pt: Tile): + 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 @@ -187,32 +208,33 @@ def __init__(self, name: str, rot: _PieceRotation, pt: Tile): self.rotation = rot self.piece_id = self.piece.id self.name = name - self.contact = pt + self.contact = coords_to_tile(pt) _PIECE_ROTATION_POINTS.append(self) - dy, dx = pt + dy, dx = pt - self.tiles: list[Tile] = [] - self.adjacent: set[Tile] = set() - self.corners: set[Tile] = set() + # 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 y, x in rot.tiles: - self.tiles.append((y - dy, x - dx)) + for y, x in rot.rel_tiles: + self.rel_tiles.append((y - dy, x - dx)) - for y, x in self.tiles: + for y, x in self.rel_tiles: for cy, cx in [(-1, 0), (1, 0), (0, -1), (0, 1)]: rel = (y + cy, x + cx) - if rel not in self.tiles: - self.adjacent.add(rel) + if rel not in self.rel_tiles: + self.rel_adjacent.add(rel) - for y, x in self.tiles: + for y, x in self.rel_tiles: for cy, cx in [(-1, -1), (1, -1), (-1, 1), (1, 1)]: rel = (y + cy, x + cx) - if rel not in self.tiles and rel not in self.adjacent: - self.corners.add(rel) + if rel not in self.rel_tiles and rel not in self.rel_adjacent: + self.rel_corners.add(rel) - self.adjacent = sorted(list(self.adjacent)) - self.corners = sorted(list(self.corners)) + 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 @@ -410,12 +432,12 @@ def add(suffix: str, arr: np.ndarray): # compute relative coordinates for the pieces _PRP_WITH_REL_COORD: dict[Tile, _PrpSet] = defaultdict(_PrpSet) for _pt in _PIECE_ROTATION_POINTS: - for _tile in _pt.tiles: + for _tile in _pt.rel_tiles: _PRP_WITH_REL_COORD[_tile] |= _pt.as_set _PRP_WITH_ADJ_REL_COORD: dict[Tile, _PrpSet] = defaultdict(_PrpSet) for _pt in _PIECE_ROTATION_POINTS: - for _tile in _pt.adjacent: + for _tile in _pt.rel_adjacent: _PRP_WITH_ADJ_REL_COORD[_tile] |= _pt.as_set _PRP_REL_COORDS: set[Tile] = set() @@ -429,38 +451,24 @@ def add(suffix: str, arr: np.ndarray): for _pt in _PIECE_ROTATION_POINTS: _PRP_WITH_PC_ID[_pt.piece_id] |= _pt.as_set -def tile_to_coords(tile: Tile) -> tuple[int, int]: - return tile - -def coords_to_tile(coords: tuple[int, int]) -> Tile: - return coords - -def tile_to_index(tile: Tile) -> int: - # y * width + x - return tile[0] * 20 + tile[1] - -def out_of_bounds(tile: Tile) -> bool: - return not (0 <= tile[0] < 20 and 0 <= tile[1] < 20) - # helpers for retrieving information about game pieces def n_piece_contacts(piece: Piece) -> int: - return len(_PIECES[piece].rotations[0].contacts) + return len(_PIECES[piece].rotations[0].rel_contacts) def n_piece_tiles(piece: Piece) -> int: - return len(_PIECES[piece].rotations[0].tiles) + 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, contact: Tile=None) -> list[Tile]: - """WARNING: The output of this function may change in the future""" - if contact is None: - return list(_PIECES[piece].rotations[rotation].tiles) - else: - return list(_PIECES[piece].rotations[rotation].prps[contact].tiles) +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]]: - return piece_tiles(piece, rotation, contact) + 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: @@ -587,9 +595,11 @@ def on_tiles_filled(self, tiles: list[Tile]) -> None: # 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: - rel = (tile[0] - corner[0], tile[1] - corner[1]) + c = TILE_COORDS[tile] + rel = (c[0] - cy, c[1] - cx) invalid |= _PRP_WITH_REL_COORD[rel] prps &= ~invalid if prps == 0: @@ -601,17 +611,19 @@ def on_tiles_filled(self, tiles: list[Tile]) -> None: del self.corners[r] def add_corner(self, tile: Tile) -> None: - if tile in self.corners or out_of_bounds(tile): + if tile in self.corners: return bad: _PrpSet = 0 + y, x = tile_to_coords(tile) for rel in _PRP_REL_COORDS: - pt = (rel[0] + tile[0], rel[1] + tile[1]) + pt = (rel[0] + y, rel[1] + x) + t = coords_to_tile(pt) oob = out_of_bounds(pt) - if oob or self.board._tiles[pt] != 0: + if oob or self.board._tiles[t] != 0: bad |= _PRP_WITH_REL_COORD[rel] - if not oob and self.board._tiles[pt] == self.id + 1: + if not oob and self.board._tiles[t] == self.id + 1: bad |= _PRP_WITH_ADJ_REL_COORD[rel] prps = self._prps & ~bad @@ -644,17 +656,15 @@ def __str__(self): return _PIECES[self.piece].name + \ ROTATION_NAMES[self.rotation] + \ "-" + \ - TILE_NAMES[TILES.index(self.contact)] + \ - TILE_NAMES[TILES.index(self.to_tile)] + 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[0] * 571 + \ - self.contact[1] * 683 + \ - self.to_tile[0] * 1607 + \ - self.to_tile[1] * 1741 + self.contact * 571 + \ + self.to_tile * 1607 def is_equal(self, value: 'Move') -> bool: return \ @@ -715,7 +725,7 @@ def __init__(self, n_players: int): raise Exception("Number of players must be between 1 and 4") self._state: list[_BoardState] = [] - self._tiles = np.zeros((20, 20), dtype=np.uint8) + self._tiles = np.zeros((400,), dtype=np.uint8) self._n_players = n_players self._players: list[_Player] = [] @@ -965,9 +975,13 @@ def _push_prp(self, move: Move, prp: _PieceRotationPoint, tile: Tile) -> None: p.push_state() # absolute position of tiles - tiles = [(t[0] + tile[0], t[1] + tile[1]) for t in prp.tiles] - corners = [(t[0] + tile[0], t[1] + tile[1]) for t in prp.corners] - adj = [(t[0] + tile[0], t[1] + tile[1]) for t in prp.adjacent] + ty, tx = TILE_COORDS[tile] + tiles = [coords_to_tile((t[0] + ty, t[1] + tx)) for t in prp.rel_tiles] + corners = [(t[0] + ty, t[1] + tx) for t in prp.rel_corners] + adj = [(t[0] + ty, t[1] + tx) 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) @@ -989,7 +1003,7 @@ def _push_prp(self, move: Move, prp: _PieceRotationPoint, tile: Tile) -> None: player.corners.pop(T20, None) player.has_played = True - player.score += len(prp.tiles) + player.score += len(prp.rel_tiles) # inc turn and make sure player can move cur_turn = self._incr_player() @@ -999,7 +1013,7 @@ def _push_prp(self, move: Move, prp: _PieceRotationPoint, tile: Tile) -> None: def __str__(self): out = "" - board = self._tiles[::-1] + board = self._tiles.reshape((20, 20))[::-1] chars = None if print_color: diff --git a/tilewe/engine.py b/tilewe/engine.py index 197dfff..9dabee0 100644 --- a/tilewe/engine.py +++ b/tilewe/engine.py @@ -241,7 +241,7 @@ def evaluate_move_weight(move: tilewe.Move) -> float: 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[coords[1] * 20 + coords[0]] + total += self.weights[tilewe.coords_to_tile(coords)] return total diff --git a/tilewe/tests/test_tiles.py b/tilewe/tests/test_tiles.py new file mode 100644 index 0000000..18b60dd --- /dev/null +++ b/tilewe/tests/test_tiles.py @@ -0,0 +1,9 @@ +import unittest + +import tilewe + +class TestTilewe(unittest.TestCase): + + def test_tile_tuple_conversion(self): + for tile in tilewe.TILES: + self.assertEqual(tile, tilewe.coords_to_tile(tilewe.tile_to_coords(tile))) From 5b1676110cda8df9a0ea889fd10f4cfc762abd72 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Fri, 17 Nov 2023 21:39:28 -0600 Subject: [PATCH 2/7] Improve add_corner speed --- tilewe/__init__.py | 81 +++++++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/tilewe/__init__.py b/tilewe/__init__.py index 19f0754..9739645 100644 --- a/tilewe/__init__.py +++ b/tilewe/__init__.py @@ -1,4 +1,3 @@ -from collections import defaultdict from dataclasses import dataclass import sys @@ -78,7 +77,7 @@ def coords_to_tile(coords: tuple[int, int]) -> Tile: def tile_to_index(tile: Tile) -> int: return tile -def out_of_bounds(coords: tuple[int, int]) -> bool: +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: @@ -238,7 +237,7 @@ def __init__(self, name: str, rot: _PieceRotation, pt: tuple[int, int]): # internal global data for all game pieces # initialized on library load one time below -PIECE_COUNT = 0 +N_PIECES = 0 _PIECES: list[_Piece] = [] _PIECE_ROTATIONS: list[_PieceRotation] = [] _PIECE_ROTATION_POINTS: list[_PieceRotationPoint] = [] @@ -262,9 +261,9 @@ def _create_piece(name: str, shape: list[list[int]]) -> Piece: The new id assigned to this piece """ - global PIECE_COUNT - id = PIECE_COUNT - PIECE_COUNT += 1 + global N_PIECES + id = N_PIECES + N_PIECES += 1 pc = _Piece(name, id) _PIECES.append(pc) f_names = [] @@ -429,25 +428,34 @@ def add(suffix: str, arr: np.ndarray): [0, 1, 1] ]) +def create_rel_tile(pt: tuple[int, int]) -> Tile: + return ((pt[0] + 32) << 6) + pt[1] + 32 + +_REL_TILE_COORDS = [ + (y - 32, x - 32) for y in range(64) for x in range(64) +] + # compute relative coordinates for the pieces -_PRP_WITH_REL_COORD: dict[Tile, _PrpSet] = defaultdict(_PrpSet) +_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[_tile] |= _pt.as_set + _PRP_WITH_REL_COORD[create_rel_tile(_tile)] |= _pt.as_set -_PRP_WITH_ADJ_REL_COORD: dict[Tile, _PrpSet] = defaultdict(_PrpSet) +_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[_tile] |= _pt.as_set - -_PRP_REL_COORDS: set[Tile] = set() -for _pt in _PRP_WITH_REL_COORD: - _PRP_REL_COORDS.add(_pt) -for _pt in _PRP_WITH_ADJ_REL_COORD: - _PRP_REL_COORDS.add(_pt) + _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: dict[int, _PrpSet] = defaultdict(_PrpSet) +_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 @@ -522,10 +530,12 @@ def __init__(self, name: str, id: Color, board: 'Board'): 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 @@ -567,14 +577,14 @@ def pop_state(self) -> bool: def remove_piece(self, piece_id: int) -> None: # remove piece permutations from availability list - prps = _PRP_WITH_PC_ID[piece_id] - self._prps &= ~prps + 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 &= ~prps + corner &= not_prps if corner == 0: remove.append(key) else: @@ -599,7 +609,7 @@ def on_tiles_filled(self, tiles: list[Tile]) -> None: invalid: _PrpSet = 0 for tile in tiles: c = TILE_COORDS[tile] - rel = (c[0] - cy, c[1] - cx) + rel = create_rel_tile((c[0] - cy, c[1] - cx)) invalid |= _PRP_WITH_REL_COORD[rel] prps &= ~invalid if prps == 0: @@ -614,21 +624,34 @@ def add_corner(self, tile: Tile) -> None: if tile in self.corners: return - bad: _PrpSet = 0 + bad: _PrpSet = ~_PRP_SET_ALL + tgt = self._tgt y, x = tile_to_coords(tile) + for rel in _PRP_REL_COORDS: - pt = (rel[0] + y, rel[1] + x) - t = coords_to_tile(pt) - oob = out_of_bounds(pt) - if oob or self.board._tiles[t] != 0: + pt = _REL_TILE_COORDS[rel] + pt = (pt[0] + y, pt[1] + x) + # pt = (rel[0] + y, rel[1] + x) + t = pt[0] * 20 + pt[1] + 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] - if not oob and self.board._tiles[t] == self.id + 1: - bad |= _PRP_WITH_ADJ_REL_COORD[rel] + + # if not ib or self.board._tiles[t]: + # bad |= _PRP_WITH_REL_COORD[rel] + # if ib and not (self.board._tiles[t] - tgt): + # bad |= _PRP_WITH_ADJ_REL_COORD[rel] prps = self._prps & ~bad if prps > 0: - self.corners[tile] = self._prps & ~bad + self.corners[tile] = prps class Move: """ From e0082b81bafa09aa04fe4c0ac74df6ce9d043cee Mon Sep 17 00:00:00 2001 From: Michael Conard Date: Sat, 18 Nov 2023 01:21:58 -0500 Subject: [PATCH 3/7] remove more unnecessary != 0 --- tilewe/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tilewe/__init__.py b/tilewe/__init__.py index 9739645..9a23ae1 100644 --- a/tilewe/__init__.py +++ b/tilewe/__init__.py @@ -816,7 +816,7 @@ def _remaining_piece_set(self, player: Color) -> set[Piece]: pieces = set() prps = self._players[player]._prps - while prps != 0: + while prps: # get least significant bit prp = (prps & -prps).bit_length() - 1 # remove it so the next LSB is another PRP @@ -842,7 +842,7 @@ def _is_legal(self, prp_id: int, tile: Tile, player: Color=None) -> bool: player = self._players[player] prps = player.corners.get(tile, 0) - return (prps & (1 << prp_id)) != 0 + return prps & (1 << prp_id) 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] @@ -872,7 +872,7 @@ def is_legal(self, move: Move, for_player: Color=None) -> bool: return False # permutation must fit at the corner square - return (prps & prp.as_set) != 0 + return prps & prp.as_set def n_legal_moves(self, unique: bool=True, for_player: Color=None): player = self._players[self.current_player if for_player is None else for_player] @@ -883,7 +883,7 @@ def n_legal_moves(self, unique: bool=True, for_player: Color=None): total += prps.bit_count() else: for prps in player.corners.values(): - while prps != 0: + while prps: # get least significant bit prp_id = (prps & -prps).bit_length() - 1 # remove it so the next LSB is another PRP @@ -903,7 +903,7 @@ def generate_legal_moves(self, unique: bool=True, for_player: Color=None): # 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 != 0: + while prps: # get least significant bit prp_id = (prps & -prps).bit_length() - 1 # remove it so the next LSB is another PRP @@ -919,7 +919,7 @@ def generate_legal_moves(self, unique: bool=True, for_player: Color=None): )) else: for to_sq, prps in player.corners.items(): - while prps != 0: + while prps: # get least significant bit prp_id = (prps & -prps).bit_length() - 1 # remove it so the next LSB is another PRP From f89075e5388ae602ff4f08167de0e0621408da57 Mon Sep 17 00:00:00 2001 From: Michael Conard Date: Sun, 19 Nov 2023 00:27:37 -0500 Subject: [PATCH 4/7] return bool --- tilewe/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tilewe/__init__.py b/tilewe/__init__.py index 9a23ae1..e1a0c03 100644 --- a/tilewe/__init__.py +++ b/tilewe/__init__.py @@ -842,7 +842,7 @@ def _is_legal(self, prp_id: int, tile: Tile, player: Color=None) -> bool: player = self._players[player] prps = player.corners.get(tile, 0) - return prps & (1 << prp_id) + 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] @@ -872,7 +872,7 @@ def is_legal(self, move: Move, for_player: Color=None) -> bool: return False # permutation must fit at the corner square - return prps & prp.as_set + return (prps & prp.as_set) != 0 def n_legal_moves(self, unique: bool=True, for_player: Color=None): player = self._players[self.current_player if for_player is None else for_player] From dd882d7335b0e6accf9737d2b2345fbe9f4477d5 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Sun, 19 Nov 2023 00:28:48 -0600 Subject: [PATCH 5/7] Remove commented code --- tilewe/__init__.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tilewe/__init__.py b/tilewe/__init__.py index 9739645..b76d8a3 100644 --- a/tilewe/__init__.py +++ b/tilewe/__init__.py @@ -632,7 +632,6 @@ def add_corner(self, tile: Tile) -> None: for rel in _PRP_REL_COORDS: pt = _REL_TILE_COORDS[rel] pt = (pt[0] + y, pt[1] + x) - # pt = (rel[0] + y, rel[1] + x) t = pt[0] * 20 + pt[1] ib = in_bounds(pt) @@ -644,13 +643,8 @@ def add_corner(self, tile: Tile) -> None: else: bad |= _PRP_WITH_REL_COORD[rel] - # if not ib or self.board._tiles[t]: - # bad |= _PRP_WITH_REL_COORD[rel] - # if ib and not (self.board._tiles[t] - tgt): - # bad |= _PRP_WITH_ADJ_REL_COORD[rel] - prps = self._prps & ~bad - if prps > 0: + if prps: self.corners[tile] = prps class Move: From ad4f51ca5037b4c578dfd871a8dcc496ae9847e9 Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Sun, 19 Nov 2023 01:06:36 -0600 Subject: [PATCH 6/7] Swap x, y and fix player bugs --- tilewe/__init__.py | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/tilewe/__init__.py b/tilewe/__init__.py index 3c459fb..f522597 100644 --- a/tilewe/__init__.py +++ b/tilewe/__init__.py @@ -42,7 +42,7 @@ ] TILE_COORDS = [ - (y, x) for y in range(20) for x in range(20) + (x, y) for y in range(20) for x in range(20) ] TILE_NAMES = [ @@ -72,7 +72,7 @@ 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] * 20 + coords[1] + return coords[0] + coords[1] * 20 def tile_to_index(tile: Tile) -> int: return tile @@ -161,7 +161,7 @@ def __init__(self, name: str, pc: _Piece, rot: Rotation, shape: np.ndarray): for x in range(W): # check each tile in piece if shape[y, x] != 0: - self.rel_tiles.append((y, x)) + self.rel_tiles.append((x, y)) v_neighbors = 0 h_neighbors = 0 @@ -173,7 +173,7 @@ def __init__(self, name: str, pc: _Piece, rot: Rotation, shape: np.ndarray): n_neighbors = v_neighbors + h_neighbors if (n_neighbors <= 1) or (v_neighbors == 1 and h_neighbors == 1): - self.rel_contacts.append((y, x)) + self.rel_contacts.append((x, y)) self.contact_shape[y, x] = 1 for coord in self.rel_contacts: @@ -210,25 +210,25 @@ def __init__(self, name: str, rot: _PieceRotation, pt: tuple[int, int]): self.contact = coords_to_tile(pt) _PIECE_ROTATION_POINTS.append(self) - dy, dx = pt + 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 y, x in rot.rel_tiles: - self.rel_tiles.append((y - dy, x - dx)) + for x, y in rot.rel_tiles: + self.rel_tiles.append((x - dx, y - dy)) - for y, x in self.rel_tiles: + for x, y in self.rel_tiles: for cy, cx in [(-1, 0), (1, 0), (0, -1), (0, 1)]: - rel = (y + cy, x + cx) + rel = (x + cx, y + cy) if rel not in self.rel_tiles: self.rel_adjacent.add(rel) - for y, x in self.rel_tiles: - for cy, cx in [(-1, -1), (1, -1), (-1, 1), (1, 1)]: - rel = (y + cy, x + cx) + 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) @@ -429,10 +429,10 @@ def add(suffix: str, arr: np.ndarray): ]) def create_rel_tile(pt: tuple[int, int]) -> Tile: - return ((pt[0] + 32) << 6) + pt[1] + 32 + return pt[0] + 32 + ((pt[1] + 32) << 6) _REL_TILE_COORDS = [ - (y - 32, x - 32) for y in range(64) for x in range(64) + (x - 32, y - 32) for y in range(64) for x in range(64) ] # compute relative coordinates for the pieces @@ -550,10 +550,12 @@ def copy_current_state(self, board: 'Board') -> '_Player': 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 @@ -627,12 +629,12 @@ def add_corner(self, tile: Tile) -> None: bad: _PrpSet = ~_PRP_SET_ALL tgt = self._tgt - y, x = tile_to_coords(tile) + x, y = tile_to_coords(tile) for rel in _PRP_REL_COORDS: pt = _REL_TILE_COORDS[rel] - pt = (pt[0] + y, pt[1] + x) - t = pt[0] * 20 + pt[1] + pt = (pt[0] + x, pt[1] + y) + t = pt[0] + pt[1] * 20 ib = in_bounds(pt) if ib: @@ -992,10 +994,10 @@ def _push_prp(self, move: Move, prp: _PieceRotationPoint, tile: Tile) -> None: p.push_state() # absolute position of tiles - ty, tx = TILE_COORDS[tile] - tiles = [coords_to_tile((t[0] + ty, t[1] + tx)) for t in prp.rel_tiles] - corners = [(t[0] + ty, t[1] + tx) for t in prp.rel_corners] - adj = [(t[0] + ty, t[1] + tx) for t in prp.rel_adjacent] + 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)] From ac927a25767517d540f598c801d02212cc58123c Mon Sep 17 00:00:00 2001 From: Nicholas Hamilton Date: Mon, 20 Nov 2023 22:32:24 -0600 Subject: [PATCH 7/7] Make test_finished_game_state work regardless of movegen order --- tilewe/tests/test_gameplay.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tilewe/tests/test_gameplay.py b/tilewe/tests/test_gameplay.py index ee7205a..3a87e64 100644 --- a/tilewe/tests/test_gameplay.py +++ b/tilewe/tests/test_gameplay.py @@ -35,12 +35,11 @@ def test_no_moves_is_finished(self): def test_finished_game_state(self): random.seed(0) board = tilewe.Board(4) - engine = RandomEngine() # play a game until the state is marked finished tracked_ply = 0 while not board.finished: - board.push(engine.search(board)) + 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 @@ -63,20 +62,21 @@ def test_finished_game_state(self): self.assertEqual(board.n_player_corners(i), 0) expected_game = [ - 'T5e-c3t20', 'P5sf-a3a20', 'P5sf-b1t1', 'Z5e-a1a1', 'Z4n-c1q18', 'I4n-a4c17', 'N5nf-b1r4', 'L5e-a1d4', - 'L4wf-c1n18', 'L4s-b1d18', 'I2n-a1s7', 'F5w-c2c6', 'F5wf-c3k17', 'Y5nf-a4d13', 'L5ef-d2p5', 'I3e-a1d7', - 'Y5w-d2k20', 'Z5n-c1c9', 'V5n-c1p8', 'U5n-a2e3', 'W5w-a3k14', 'T4w-a1e14', 'F5ef-c2l4', 'I2e-a1b8', - 'O1n-a1n20', 'U5n-a2e9', 'U5n-a2i2', 'Y5n-a3h6', 'T4n-a2n14', 'V5w-a3f13', 'O1n-a1o11', 'N5e-a2j8', - 'P5w-c1h18', 'I2e-a1i14', 'Y5sf-a2q11', 'L3e-b2i9', 'V5n-c3k12', 'O1n-a1h15', 'L3s-b1q14', 'V5n-c1g10', - 'L3e-a2r17', 'L5n-b1b13', 'L4nf-b3q3', 'O1n-a1k6', 'I3n-a3s15', 'L3e-a1g16', 'T4w-b2o2', 'W5e-b1m9', - 'I2e-b1e17', 'F5s-a1k15', 'Z4nf-b1o16', 'T4w-a1n11', 'I4n-a4t12', 'Z4nf-a1i18', 'I3n-a3t6', 'Z4nf-a1n6', - 'T5e-c1h3', 'T5w-a1q8', 'I4e-d1g6', 'L4e-a1m14', 'Z5e-c3c5'] + 'Z4e-a1a1', 'P5n-a3a20', 'W5n-c3t20', 'Z5ef-c1t1', 'T5e-c1c4', 'F5n-b3b17', 'L4w-c1q19', 'T4w-a1s4', + 'Y5wf-a2d7', 'X5n-a2d16', 'Y5wf-d2n18', 'Y5nf-b3q4', 'O1n-a1h6', 'Z5ef-c1f18', 'O4n-a2n16', 'P5wf-c1r7', + 'L5e-a2d3', 'V5n-c3c14', 'I3e-a1p14', 'L5sf-b4o8', 'I2e-a1i7', 'L3w-b2g15', 'T4s-b2m14', 'V5w-c1m9', + 'L4e-a1h8', 'Y5w-b1h16', 'X5n-c2k14', 'I5e-e1m4', 'P5sf-b1g10', 'I3n-a3h13', 'Z4e-b2j19', 'F5sf-b3h3', + 'N5wf-c2k6', 'T5s-a1i10', 'U5w-a3s15', 'I3n-a3n3', 'L3w-a1k8', 'W5e-a3d11', 'O1n-a1h15', 'O1n-a1j2', + 'U5w-b1c8', 'L4ef-a2k16', 'N5w-a2o12', 'W5w-b2s9', 'W5w-c1e13', 'O1n-a1i15', 'I2n-a2i12', 'I2e-b1h5', + '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] # assert that the expected game was played unexpected_game_msg = "Expected game not played, was generate_legal_moves() changed intentionally?" self.assertEqual(all_moves, expected_game, unexpected_game_msg) - self.assertEqual(board.winners, [2], unexpected_game_msg) + self.assertEqual(board.winners, [1], unexpected_game_msg) def test_open_corners_first_moves(self): engine = RandomEngine()