board.py (12595B)
1 import pygame, pytmx, os, queue 2 from . import manager, entity 3 from .constants import * 4 5 ############ 6 # board.py # 7 ############ 8 9 # This file contains: 10 # 1. The BoardManager class, which manages boards and swaps between them 11 # 2. The Board class, which represents a grid of Tile objects and is the play area 12 13 ################################### 14 # Section 1 - BoardManager Object # 15 ################################### 16 17 class BoardManager(manager.Manager): 18 """ 19 BoardManager handles loading and managing Board objects. It 20 is directly subordinate to Game and is mostly useful for 21 switching between Boards. 22 """ 23 24 def __init__(self, game, bus, camera, name): 25 26 super().__init__(game, bus, camera, name) 27 28 # Board values 29 self.current_board = None 30 self.board_overlay = pygame.sprite.LayeredDirty() 31 32 # Move values 33 self.move_targets = {} # Keys = (x, y) of tiles, vals are a list of (x, y) tuples representing the path to be taken 34 self.previous_moves = {} # Keys = (x, y) of tiles, vals are the (x, y) of the previous node 35 36 # Attack values 37 self.attack_targets = [] # (x, y) of tiles 38 39 def load_board(self, boardname): 40 """ 41 Load a given board. 42 """ 43 self.current_board = Board(self, boardname) 44 self.load_overlay() 45 self.update(None) 46 47 def load_overlay(self): 48 """ 49 Derive an overlay from available loaded board. Calling 50 this has the effect of refreshing the overlay. 51 """ 52 self.board_overlay = pygame.sprite.LayeredDirty() 53 for layer in self.current_board.tmx_data.visible_layers: 54 if isinstance(layer, pytmx.TiledTileLayer): 55 for x, y, gid in layer: 56 if self.current_board.tmx_data.get_tile_properties_by_gid(gid)["Passable"] == 1: 57 e = entity.Entity(self.bus.fetch("sheet_manager", "sheets")["board_overlays_1"]) 58 e.set_position((x * self.current_board.tmx_data.tilewidth, y * self.current_board.tmx_data.tileheight)) 59 e.custom_flags = "OverlayGrid" 60 self.board_overlay.add(e) 61 62 def get_tile_pos_at_position(self, pos): 63 """ 64 Return (x, y) tile_pos if there is a tile at 'pos', and 65 return None otherwise. 66 """ 67 w = self.current_board.tmx_data.tilewidth 68 h = self.current_board.tmx_data.tileheight 69 for layer in self.current_board.tmx_data.visible_layers: 70 if isinstance(layer, pytmx.TiledTileLayer): 71 for x, y, gid in layer: 72 if pos[0] >= x * w and pos[0] < (x + 1) * w and pos[1] >= y * w and pos[1] < (y + 1) * w: 73 return (x, y) 74 return None 75 76 def get_tile_at_position(self, pos): 77 """ 78 Return (x, y, gid) if there is tile at 'pos', and return 79 None otherwise. 80 """ 81 w = self.current_board.tmx_data.tilewidth 82 h = self.current_board.tmx_data.tileheight 83 for layer in self.current_board.tmx_data.visible_layers: 84 if isinstance(layer, pytmx.TiledTileLayer): 85 for x, y, gid in layer: 86 if pos[0] >= x * w and pos[0] < (x + 1) * w and pos[1] >= y * w and pos[1] < (y + 1) * w: 87 return (x, y, gid) 88 return None 89 90 def get_tile_at_tile_pos(self, tile_pos): 91 """ 92 Return (x, y, gid) if there is a tile at tile_pos 'tile_pos', 93 and return None otherwise. 94 """ 95 for layer in self.current_board.tmx_data.visible_layers: 96 if isinstance(layer, pytmx.TiledTileLayer): 97 for x, y, gid in layer: 98 if tile_pos == (x, y): 99 return (x, y, gid) 100 return None 101 102 def get_overlay_move_entity_at_pos(self, pos): 103 """ 104 Return (x, y) if there is a legal overlay move entity at 'pos', 105 and return None otherwise. 106 """ 107 for e in self.board_overlay: 108 if "OverlayMove" in e.custom_flags and e.rect.collidepoint(pos): 109 return e.custom_flags[1] 110 return None 111 112 def get_overlay_attack_entity_at_pos(self, pos): 113 """ 114 Return (x, y) if there is a legal overlay attack entity at 115 'pos', and return None otherwise. 116 """ 117 for e in self.board_overlay: 118 if "OverlayAttack" in e.custom_flags and e.rect.collidepoint(pos): 119 return e.custom_flags[1] 120 return None 121 122 def display_as_move_range(self, piece, tile_pos_list): 123 """ 124 Display a move range from a given list of tile_pos 125 tuples. 126 """ 127 # First, refresh overlay to avoid drawing over existing move markers 128 self.load_overlay() 129 130 self.create_move_range(piece) 131 for t in self.move_targets: 132 if not self.bus.check_is_ally_occupied_by_tile_pos(t, piece.team): 133 e = entity.Entity(self.bus.fetch("sheet_manager", "sheets")["board_overlays_1"], (1, 0)) 134 e.set_position((t[0] * TILE_WIDTH, t[1] * TILE_HEIGHT)) 135 e.custom_flags = ("OverlayMove", t) 136 self.board_overlay.add(e) 137 138 def display_as_attack_range(self, piece): 139 """ 140 Display an attack range from a given list of tile_pos 141 tuples. 142 """ 143 # Refresh overlay to begin with 144 self.load_overlay() 145 146 self.create_attack_range(piece) 147 for t in self.attack_targets: 148 e = entity.Entity(self.bus.fetch("sheet_manager", "sheets")["board_overlays_1"], (0, 1)) 149 e.set_position((t[0] * TILE_WIDTH, t[1] * TILE_HEIGHT)) 150 e.custom_flags = ("OverlayAttack", t) 151 self.board_overlay.add(e) 152 153 def create_move_range(self, piece): 154 """ 155 Create a legal move range for the given piece. 156 This is mostly a subtractive process. 157 """ 158 # Setup 159 self.empty_move_range() 160 movemax = piece.active_stats["MOVE"] 161 mx = 0 162 my = 0 163 distances = {} 164 adj_list = {} 165 pq = queue.PriorityQueue() 166 index = -1 167 min_val = -1 168 new_dist = 0 169 170 # First, enumerate all tile positions in the legal move range 171 # except those that are impassable 172 for layer in self.current_board.tmx_data.visible_layers: 173 if isinstance(layer, pytmx.TiledTileLayer): 174 for x, y, gid in layer: 175 mx = piece.tile_pos[0] - x 176 my = piece.tile_pos[1] - y 177 if (abs(mx) + abs(my)) <= movemax and self.current_board.tmx_data.get_tile_properties_by_gid(gid)["Passable"] == 1 and not self.bus.check_is_enemy_occupied_by_tile_pos((x, y), piece.team): 178 distances[(x, y)] = movemax + 1 # So we are always greater than the max move 179 180 # Next, calculate the move from the starting pos to each potential 181 # This implements Dijkstra's algorithm (kinda) 182 distances[piece.tile_pos] = 0 183 adj_list = self.get_adjacency_list(list(distances.keys())) 184 pq.put((piece.tile_pos, 0)) 185 186 # While there are still potentials to check 187 while not pq.empty(): 188 index, min_val = pq.get() 189 190 # If we could possibly optimise it, try to 191 if distances[index] >= min_val: 192 for n in adj_list[index]: 193 if adj_list[index][n] in distances.keys(): 194 new_dist = distances[index] + 1 195 if new_dist < distances[adj_list[index][n]]: 196 self.previous_moves[adj_list[index][n]] = index 197 distances[adj_list[index][n]] = new_dist 198 pq.put((adj_list[index][n], new_dist)) 199 200 # Next, remove all all potentials with path length > movemax 201 for ds in distances: 202 if distances[ds] <= movemax: 203 self.move_targets[ds] = distances[ds] 204 205 def create_attack_range(self, piece): 206 """ 207 Generate a list of legal attack targets within range of 208 the given piece. 209 """ 210 self.attack_targets = [] 211 for layer in self.current_board.tmx_data.visible_layers: 212 if isinstance(layer, pytmx.TiledTileLayer): 213 for x, y, gid in layer: 214 mx = piece.tile_pos[0] - x 215 my = piece.tile_pos[1] - y 216 if not (mx == 0 and my == 0) and (abs(mx) + abs(my)) <= piece.attack_range: 217 self.attack_targets.append((x, y)) 218 219 def get_path_by_previous_moves(self, start_tile, target_tile): 220 """ 221 Generate a path (list of (x, y) tile_pos tuples) using 222 the self.previous_moves dict, if it is filled, to the 223 given target tile from the start tile. If it can't be 224 done, return None. 225 """ 226 if self.previous_moves != {} and target_tile in self.move_targets.keys(): 227 n = target_tile 228 path = [] 229 while n != start_tile: 230 path.append(self.previous_moves[n]) 231 n = self.previous_moves[n] 232 path.reverse() 233 path.append(target_tile) 234 return path 235 else: 236 return None 237 238 def get_adjacent_tiles_by_tile_pos(self, tile_pos): 239 """ 240 Return cardinal adjacent tiles of the given tile_pos. 241 Return value is a dict, the values of which are either 242 (x, y) tuples or None if the adjacent is outside of the 243 bounds of the board. 244 """ 245 adj = {(-1, 0) : None, (1, 0) : None, (0, -1) : None, (0, 1): None} 246 mx = 0 247 my = 0 248 for x in range(0, self.current_board.tmx_data.width): 249 for y in range(0, self.current_board.tmx_data.height): 250 mx = tile_pos[0] - x 251 my = tile_pos[1] - y 252 if (mx, my) in adj.keys(): 253 adj[(mx, my)] = (x, y) 254 return adj 255 256 def get_adjacency_list(self, tiles): 257 """ 258 Return an adjaceny list of all the tiles included 259 in 'tiles'. Tiles should be a list or tuple. The 260 return value is a dict. 261 """ 262 adj_list = {} 263 for t in tiles: 264 adj_list[t] = self.get_adjacent_tiles_by_tile_pos(t) 265 return adj_list 266 267 def empty_move_range(self): 268 """ 269 Reset to a default, non-moving state. 270 """ 271 self.move_targets = {} 272 self.previous_moves = {} 273 274 def expose(self): 275 """ 276 Expose info about the board to the ManagerBus. 277 """ 278 data = { 279 "current_board" : self.current_board, 280 "board_dimensions" : self.current_board.board_dimensions, 281 "board_pixel_dimensions" : self.current_board.pixel_dimensions, 282 "board_tile_layer" : { (x, y) : gid for layer in self.current_board.tmx_data.visible_layers for x, y, gid in layer if isinstance(layer, pytmx.TiledTileLayer) }, 283 "board_overlay" : self.board_overlay 284 } 285 self.bus.record(self.name, data) 286 287 def update_managed(self, surface = None): 288 """ 289 Update the current board. 290 """ 291 if surface != None: 292 self.current_board.draw_board(surface) 293 self.board_overlay.update(surface) 294 295 ############################ 296 # Section 2 - Board Object # 297 ############################ 298 299 class Board(object): 300 """ 301 Board is an object that consists of a Pytmx Tiled map and 302 some functions for drawing it. Board is managed by a 303 BoardManager object. 304 """ 305 306 def __init__(self, manager, boardname): 307 308 # Saved values 309 self.manager = manager 310 self.boardname = boardname 311 self.filename = boardname + ".tmx" 312 313 # Pytmx values 314 self.tmx_data = pytmx.load_pygame(os.path.join(BOARD_PATH, self.boardname, self.filename)) 315 self.board_dimensions = (self.tmx_data.width, self.tmx_data.height) 316 self.pixel_dimensions = (self.tmx_data.width * TILE_WIDTH, self.tmx_data.height * TILE_HEIGHT) 317 318 def draw_board(self, surface = None): 319 """ 320 Draw the tiles of the board onto the provided PyGame 321 surface object. 322 """ 323 for layer in self.tmx_data.visible_layers: 324 if isinstance(layer, pytmx.TiledTileLayer): 325 for x, y, gid in layer: 326 t = self.tmx_data.get_tile_image_by_gid(gid) 327 if t: 328 surface.blit(t, (x * TILE_HEIGHT, y * TILE_WIDTH)) 329