def test_get_spiral(): # For radius of 3 expected = [] r = 2 expected.append((r - 0) * hx.W + 0 * hx.NW) expected.append((r - 1) * hx.W + 1 * hx.NW) expected.append((r - 0) * hx.NW + 0 * hx.NE) expected.append((r - 1) * hx.NW + 1 * hx.NE) expected.append((r - 0) * hx.NE + 0 * hx.E) expected.append((r - 1) * hx.NE + 1 * hx.E) expected.append((r - 0) * hx.E + 0 * hx.SE) expected.append((r - 1) * hx.E + 1 * hx.SE) expected.append((r - 0) * hx.SE + 0 * hx.SW) expected.append((r - 1) * hx.SE + 1 * hx.SW) expected.append((r - 0) * hx.SW + 0 * hx.W) expected.append((r - 1) * hx.SW + 1 * hx.W) r += 1 expected.append((r - 0) * hx.W + 0 * hx.NW) expected.append((r - 1) * hx.W + 1 * hx.NW) expected.append((r - 2) * hx.W + 2 * hx.NW) expected.append((r - 0) * hx.NW + 0 * hx.NE) expected.append((r - 1) * hx.NW + 1 * hx.NE) expected.append((r - 2) * hx.NW + 2 * hx.NE) expected.append((r - 0) * hx.NE + 0 * hx.E) expected.append((r - 1) * hx.NE + 1 * hx.E) expected.append((r - 2) * hx.NE + 2 * hx.E) expected.append((r - 0) * hx.E + 0 * hx.SE) expected.append((r - 1) * hx.E + 1 * hx.SE) expected.append((r - 2) * hx.E + 2 * hx.SE) expected.append((r - 0) * hx.SE + 0 * hx.SW) expected.append((r - 1) * hx.SE + 1 * hx.SW) expected.append((r - 2) * hx.SE + 2 * hx.SW) expected.append((r - 0) * hx.SW + 0 * hx.W) expected.append((r - 1) * hx.SW + 1 * hx.W) expected.append((r - 2) * hx.SW + 2 * hx.W) assert(np.array_equal( hx.get_spiral([0, 0, 0], 2, 3), np.array(expected)))
def __init__(self, size=(600, 600), hex_radius=22, caption="ExampleHexMap"): self.caption = caption self.size = np.array(size) self.width, self.height = self.size self.center = self.size / 2 self.hex_radius = hex_radius self.hex_apothem = hex_radius * np.sqrt(3) / 2 self.hex_offset = np.array( [self.hex_radius * np.sqrt(3) / 2, self.hex_radius]) self.hex_map = hx.HexMap() self.max_coord = 6 self.rad = 3 self.selected_hex_image = make_hex_surface((128, 128, 128, 160), self.hex_radius, (255, 255, 255), hollow=True) self.selection_type = 3 self.clicked_hex = np.array([0, 0, 0]) # Get all possible coordinates within `self.max_coord` as radius. coords = hx.get_spiral(np.array((0, 0, 0)), 1, self.max_coord) # Convert coords to axial coordinates, create hexes and randomly filter out some hexes. hexes = [] num_hexes = np.random.binomial(len(coords), .9) axial_coords = hx.cube_to_axial(coords) axial_coords = axial_coords[np.random.choice(len(axial_coords), num_hexes, replace=False)] for i, axial in enumerate(axial_coords): colo = list(COLORS[COL_IDX[i]]) colo.append(255) hexes.append(ExampleHex(axial, colo, hex_radius)) hexes[-1].set_value(i) # the number at the center of the hex self.hex_map[np.array(axial_coords)] = hexes self.main_surf = None self.font = None self.clock = None self.init_pg()
def get_selection(selection_type, max_range, hex_radius): hex_map = hx.HexMap() hexes = [] axial_coordinates = [] if selection_type == Selection.Type.RECT: for r in range(-max_range, max_range + 1): r_offset = r >> 1 for q in range(-max_range - r_offset, max_range - r_offset): c = [q, r] axial_coordinates.append(c) hexes.append(ExampleHex(c, [141, 207, 104, 255], hex_radius)) elif selection_type == Selection.Type.HEX: spiral_coordinates = hx.get_spiral(np.array((0, 0, 0)), 1, max_range) # Gets spiral coordinates from center axial_coordinates = hx.cube_to_axial(spiral_coordinates) for i, axial in enumerate(axial_coordinates): hex_color = list(COLORS[3]) # set color of hex hex_color.append(255) #set alpha to 255 hexes.append(ExampleHex(axial, hex_color, hex_radius)) elif selection_type == Selection.Type.TRIANGLE: top = int(max_range / 2) for q in range(0, max_range + 1): for r in range(0, max_range - q + 1): c = [q, r] axial_coordinates.append(c) hexes.append(ExampleHex(c, [141, 207, 104, 255], hex_radius)) elif selection_type == Selection.Type.RHOMBUS: q1 = int(-max_range / 2) q2 = int(max_range / 2) r1 = -max_range r2 = max_range # Parallelogram for q in range(q1, q2 + 1): for r in range(r1, r2 + 1): axial_coordinates.append([q, r]) hexes.append(ExampleHex([q, r], [141, 207, 104, 255], hex_radius)) elif selection_type == Selection.Type.CUSTOM: axial_coordinates.append([0, 0]) hexes.append(ExampleHex([0, 0], [141, 207, 104, 255], hex_radius)) hex_map[np.array(axial_coordinates)] = hexes # create hex map return hex_map
def __init__(self, size=(600, 600), hex_radius=22, caption="ExampleHexMap"): self.caption = caption self.size = np.array(size) self.width, self.height = self.size self.center = self.size / 2 self.hex_radius = hex_radius self.hex_map = hx.HexMap() self.max_coord = 6 self.rad = ClampedInteger(3, 1, 5) self.selected_hex_image = make_hex_surface((128, 128, 128, 160), self.hex_radius, (255, 255, 255), hollow=True) self.selection_type = CyclicInteger(3, 0, 3) self.clicked_hex = np.array([0, 0, 0]) # Get all possible coordinates within `self.max_coord` as radius. spiral_coordinates = hx.get_spiral(np.array((0, 0, 0)), 1, self.max_coord) # Convert `spiral_coordinates` to axial coordinates, create hexes and randomly filter out some hexes. hexes = [] num_shown_hexes = np.random.binomial(len(spiral_coordinates), .9) axial_coordinates = hx.cube_to_axial(spiral_coordinates) axial_coordinates = axial_coordinates[np.random.choice( len(axial_coordinates), num_shown_hexes, replace=False)] for i, axial in enumerate(axial_coordinates): hex_color = list(COLORS[COL_IDX[i]]) hex_color.append(255) hexes.append(ExampleHex(axial, hex_color, hex_radius)) hexes[-1].set_value(i) # the number at the center of the hex self.hex_map[np.array(axial_coordinates)] = hexes # pygame specific variables self.main_surf = None self.font = None self.clock = None self.init_pg()
class Board: tile_coords = hx.get_spiral(np.array((0, 0, 0)), 1, BOARD_RADIUS) x_tiles = [i for i in tile_coords if i[0] == 0] y_tiles = [i for i in tile_coords if i[1] == 0] z_tiles = [i for i in tile_coords if i[2] == 0] suns = np.array([ np.array([0, 1, -1]), # N np.array([1, 0, -1]), # NE np.array([1, -1, 0]), # SE np.array([0, -1, 1]), # S np.array([-1, 0, 1]), # SW np.array([-1, 1, 0]), # NW ]) @time_function def __init__(self, players: List[Player]): self.players = players self.player_count = len(self.players) self.tuple_tile_coords = tuple( [tuple(coord) for coord in self.tile_coords]) self.reset() # self.round_number = 0 # self.sun_position = 5 # self.pg_active = False # trees = self._get_initial_trees() # self.data = self._get_initial_data(trees) # self.tree_of_trees = self._get_tree_index(trees) def _get_initial_data(self, trees): Data = namedtuple("data", "tiles trees players tokens") data = Data( tiles=[ Tile(tree=None, coords=coord, index=self.get_tile_index(tuple(coord))) for i, coord in enumerate(self.tile_coords) ], trees=np.array(trees), players=self.players, tokens=[ Token(richness=key, value=value) for key in TOKENS for value in TOKENS[key] ], ) return data def _get_initial_trees(self) -> List: trees = [] for i, p in enumerate(self.players): for tree_type in TREES.keys(): tree_count = len(trees) tree = TREES[tree_type] trees += [ Tree( id=tree_count + j, owner=i + 1, size=tree["size"], is_bought=True, tree_type=tree_type, score=tree["score"], ) for j in range(tree["starting"]) ] tree_count = len(trees) trees += [ Tree( id=tree_count + j, owner=i + 1, size=tree["size"], is_bought=False, cost=cost, tree_type=tree_type, score=tree["score"], ) for j, cost in enumerate(tree["cost"]) ] return trees def _get_tree_index(self, trees): # index these separately so don't need to calculate many times tree_of_trees = { player.number: { "bought": { tree.id: tree for tree in trees if (tree.owner == player.number) & (not tree.tile) & tree.is_bought }, "in_shop": { tree.id: tree for tree in trees if (tree.owner == player.number) & (not tree.tile) & (not tree.is_bought) }, "on_board": { tree.id: tree for tree in trees if (tree.owner == player.number) & (tree.tile is not None) }, } for player in self.players } return tree_of_trees def reset(self): self.round_number = 0 # number of rounds self.round_turn = 0 # mid round turns - number between 0 and len(players) -1 self.sun_position = 5 self.pg_active = False trees = self._get_initial_trees() self.data = self._get_initial_data(trees) self.tree_of_trees = self._get_tree_index(trees) ######### # UTILS # ######### @classmethod @lru_cache(maxsize=None) @time_function def _get_tile_index_from_coords(cls, coords: Tuple[int]) -> int: mask = find_array_in_2D_array(np.array(coords), cls.tile_coords) index = np.where(mask)[0][0] return index @lru_cache(maxsize=None) @time_function def get_surrounding_tiles(self, coords: Tuple[int], radius: int) -> Tuple[Tile]: surrounding_tile_coords = get_surrounding_coords( coords, radius, BOARD_RADIUS) surrounding_tiles = [ self.data.tiles[self._get_tile_index_from_coords(coord)] for coord in surrounding_tile_coords ] return tuple(surrounding_tiles) @classmethod @lru_cache(maxsize=None) @time_function def get_tile_index(cls, coord: Tuple) -> int: mask = find_array_in_2D_array(np.array(coord), cls.tile_coords) return int(np.where(mask)[0]) @time_function def get_empty_unlocked_tiles(self) -> List[Tile]: return [ tile for tile in self.data.tiles if (not tile.tree) & (not tile.is_locked) ] @lru_cache(maxsize=None) @time_function def _get_tiles_at_sun_edge(self, sun: Tuple) -> List[Tile]: edge_coords = get_coords_at_sun_edge(tuple(sun), self.tuple_tile_coords) tiles = [ self.data.tiles[self._get_tile_index_from_coords(tuple(coords))] for coords in edge_coords ] return tiles @lru_cache(maxsize=None) @time_function def _get_tiles_along_same_axis(self, start_tile_coords: Tuple[int], axis: Tuple[int]) -> Tuple[Tile]: tiles = [ self.data.tiles[self._get_tile_index_from_coords(tuple(coords))] for coords in get_coords_along_same_axis(start_tile_coords, axis, BOARD_RADIUS) ] return tuple(tiles) ###################### # ROUND CALCULATIONS # ###################### @time_function def end_round(self): if self.round_number in [0, 1]: self._set_shadows() self.round_number += 1 return self.rotate_sun() self._set_shadows() for tile in self.data.tiles: tile.is_locked = False if (not tile.is_shadow) & (tile.tree is not None): player = [ player for player in self.data.players if player.number == tile.tree.owner ][0] player.l_points += tile.tree.score player.l_points_earned_history[self.round_number].append( tile.tree.score) @time_function def rotate_sun(self): self.sun_position = (self.sun_position + 1) % 6 self.round_number += 1 return @time_function def _set_shadows_along_axis(self, tile: Tile, axis: np.ndarray): axis_tiles = self._get_tiles_along_same_axis(tuple(tile.coords), tuple(axis)) current_shadow_size = 0 for axis_tile in axis_tiles: axis_tile.is_shadow = True if current_shadow_size > 0 else False current_shadow_size = (current_shadow_size - 1) if current_shadow_size > 0 else 0 if not axis_tile.tree: continue current_shadow_size = max( [axis_tile.tree.shadow, current_shadow_size]) @time_function def _set_shadows(self): sun = self.suns[self.sun_position] edge_tiles = self._get_tiles_at_sun_edge(tuple(sun)) for tile in edge_tiles: self._set_shadows_along_axis(tile, sun) ################## # MOVE EXECUTION # ################## @time_function def get_next_token(self, richness: int) -> Token: # in two player game, richness 4 is removed fom the game if (self.player_count == 2) & (richness == 4): richness = 3 while richness > 0: tokens = [ token for token in self.data.tokens if (token.richness == richness) & (token.owner is None) ] if tokens: return tokens[0] richness -= 1 raise ValueError("Ran out of tokens") @time_function def grow_tree(self, from_tree: Tree, to_tree: Tree, cost: int): # should wrap these into unit tests if not from_tree.tile: raise ValueError("This tree isn't on the board") if from_tree.size == 3: raise ValueError("This tree can't grow anymore!") if (to_tree.size - from_tree.size) != 1: raise ValueError( "The tree is trying to grow to a size that isn't one bigger!") if not to_tree.is_bought: raise ValueError("The tree hasn't been bought yet") if to_tree.tile: raise ValueError("The tree growing to is already on the board") tile = from_tree.tile to_tree.tile = tile from_tree.tile = None tile.tree = to_tree tile.is_locked = True player = [ player for player in self.data.players if player.number == tile.tree.owner ][0] player.l_points -= cost # put back in shop num_trees_of_type_in_store = len([ board_tree for board_tree in self.data.trees if (from_tree.size == board_tree.size) & (not board_tree.is_bought) & (board_tree.owner == from_tree.owner) ]) # if shop is full remove tree from game if num_trees_of_type_in_store >= len( TREES[from_tree.tree_type]["cost"]): from_tree.is_deleted = True else: tree_cost = TREES[ from_tree.tree_type]["cost"][num_trees_of_type_in_store] from_tree.cost = tree_cost from_tree.is_bought = False self.tree_of_trees[player.number]["in_shop"].update( {from_tree.id: from_tree}) self.tree_of_trees[player.number]["on_board"].update( {to_tree.id: to_tree}) self.tree_of_trees[player.number]["on_board"].pop(from_tree.id) self.tree_of_trees[player.number]["bought"].pop(to_tree.id) @time_function def plant_tree(self, tile: Tile, tree: Tree, from_tile: Tile, cost: int): if tile.tree: raise ValueError("There is already a tree here") if tree.tile: raise ValueError("This tree is already planted") if not tree.is_bought: raise ValueError("The tree hasn't been bought yet") tile.tree = tree tree.tile = tile self.data.tiles[tile.index].tree = tree tree.tile = tile tile.is_locked = True if from_tile: print(1) from_tile.is_locked = True player = [ player for player in self.data.players if player.number == tile.tree.owner ][0] player.l_points -= cost self.tree_of_trees[player.number]["on_board"].update({tree.id: tree}) self.tree_of_trees[player.number]["bought"].pop(tree.id) @time_function def collect_tree(self, tree: Tree, cost: int): if not tree.tile: raise ValueError("This tree isn't on the board") if tree.size != 3: raise ValueError("The tree isn't fully grown") tile = tree.tile tile.tree = None tree.tile = None tile.is_locked = True # put tree back in store num_trees_of_type_in_store = len([ board_tree for board_tree in self.data.trees if (tree.size == board_tree.size) & (not board_tree.is_bought) & (board_tree.owner == tree.owner) ]) tree_cost = TREES[tree.tree_type]["cost"][num_trees_of_type_in_store] tree.cost = tree_cost tree.is_bought = False player = [ player for player in self.data.players if player.number == tree.owner ][0] player.l_points -= cost token = self.get_next_token(tile.richness) token.owner = tree.owner player.score += token.value player.score_earned_history[self.round_number].append(token.value) self.tree_of_trees[player.number]["in_shop"].update({tree.id: tree}) self.tree_of_trees[player.number]["on_board"].pop(tree.id) @time_function def buy_tree(self, tree: Tree, cost: int): if tree.is_bought: raise ValueError("The tree has already been bought") tree.is_bought = True player = [ player for player in self.data.players if player.number == tree.owner ][0] player.l_points -= cost tree.cost = 0 self.tree_of_trees[player.number]["bought"].update({tree.id: tree}) self.tree_of_trees[player.number]["in_shop"].pop(tree.id) @time_function def end_go(self, player_number): player = [ player for player in self.data.players if player.number == player_number ][0] player.go_active = False st = self.round_turn if self.round_turn == len(self.players) - 1: self.end_round() self.round_turn = 0 else: self.round_turn += 1 ################## # VISUALISAIION # ################## def show(self): player_colors = {0: "green", 1: "red", 2: "blue"} colors = [ player_colors[tile.tree.owner] if tile.tree else "green" for tile in self.data.tiles ] shadows = [ "black" if tile.is_shadow else "orange" for tile in self.data.tiles ] labels = [ tile.tree.size if tile.tree else "" for tile in self.data.tiles ] hcoord = [c[0] for c in self.tile_coords] vcoord = [ 2.0 * np.sin(np.radians(60)) * (c[1] - c[2]) / 3.0 for c in self.tile_coords ] fig, ax = plt.subplots(1) fig.set_size_inches(12.5, 8.5) plt.axis([-5, 10, -5, 5]) ax.set_aspect("equal") # Add some coloured hexagons for x, y, c, l in zip(hcoord, vcoord, colors, labels): hexagon = RegularPolygon( (x, y), numVertices=6, radius=2.0 / 3.0, orientation=np.radians(30), facecolor=c, alpha=0.2, edgecolor="k", ) ax.add_patch(hexagon) # Add a text label ax.text(x, y + 0.3, l, ha="center", va="center", size=10) sun_coords = self.suns[self.sun_position] * 4 sun_y_coord = 2 * np.sin( np.radians(60)) * (sun_coords[1] - sun_coords[2]) / 3 ax.text(sun_coords[0], sun_y_coord, "SUN", ha="center", va="center", size=20, color="orange") for player in self.data.players: ax.text( 7, 2 - player.number / 2, f"Player {player.name}: Light Points: {player.l_points}, score: {player.score}", ha="center", va="center", size=10, color="black", ) ax.text( 7, 4, f"Round number {self.round_number}.{self.round_turn}", ha="center", va="center", size=10, color="black", ) # Add scatter points in hexagon centres ax.scatter(hcoord, vcoord, c=shadows, alpha=0.5) plt.axis("off") plt.show()
import numpy as np import hexy as hx hm = hx.HexMap() radius = 10 coords = hx.get_spiral(np.array((0, 0, 0)), 1, 10) def test_hexes_amount_to_radius_conversion(): hexes = 1 + 3 * radius * (radius + 1) found_radius = hx.radius_from_hexes(hexes) assert found_radius == radius def test_axial_to_cube_conversion(): axial_coords = hx.cube_to_axial(coords) cube_coords = hx.axial_to_cube(axial_coords) assert np.array_equal(coords, cube_coords) def test_axial_to_pixel_conversion(): axial_coords = hx.cube_to_axial(coords) pixel_coords = hx.axial_to_pixel(axial_coords, radius) pixel_to_axial_coords = hx.pixel_to_axial(pixel_coords, radius) assert np.array_equal(axial_coords, pixel_to_axial_coords) def test_cube_to_pixel_conversion():