Return to repo list

heart-of-gold

Tactical RPG written in python, using pygame.
Return to HMagellan.com

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