Source code for scripts.engine.core.dungen

from __future__ import annotations

import random
from dataclasses import dataclass, field
from typing import Dict, Iterator, List, Literal, TYPE_CHECKING

import tcod

import scripts.engine.core.matter
from scripts.engine.core import utility, world
from scripts.engine.internal import library
from scripts.engine.internal.constant import Direction, Height, MAP_BORDER_SIZE, TileCategory, TileCategoryType
from scripts.engine.internal.definition import ActorData, MapData, RoomConceptData
from scripts.engine.world_objects.tile import Tile

if TYPE_CHECKING:
    from typing import List, Optional, Tuple

__all__ = ["generate", "generate_steps"]


[docs]@dataclass class DungeonGenerator: rng: random.Random # containers map_data: MapData rooms_data: Dict[str, RoomConceptData] = field(default_factory=dict) actors_data: Dict[str, ActorData] = field(default_factory=dict) placed_rooms: List[RoomConcept] = field(default_factory=list) map_of_categories: List[List[TileCategoryType]] = field(default_factory=list) positions_in_rooms: List[Tuple[int, int]] = field(default_factory=list) # parameters/config max_generate_room_attempts = 100 # lower number means likely less rooms max_place_room_attempts = 1000 # lower number means likely less rooms max_place_entrance_attempts = 50 # lower number means likely less entrances (and poss more ignorant tunnels) max_make_room_accessible_attempts = 100 # lower number means likely more ignorant tunnels max_place_entity_attempts = 50 # lower number means likely less entities border_size = MAP_BORDER_SIZE # tiles to place around the outside of the map # info accessed via property but only created once requested _passable_map: List[List[Literal[True]]] = field(default_factory=list) _bools_map: List[List[bool]] = field(default_factory=list) _tiles_map: List[List[Tile]] = field(default_factory=list) # flags is_dirty: bool = False
[docs] def is_in_bounds(self, x: int, y: int): """ Check if a position is in the bounds of the map """ if 0 < x < self.map_data.width - 1 and 0 < y < self.map_data.height - 1: return True else: return False
[docs] def is_in_room(self, x: int, y: int) -> bool: """ Check if a position is in a placed room. """ if (x, y) in self.positions_in_rooms: return True else: return False
[docs] def is_only_accessible_diagonally(self, x: int, y: int) -> bool: """ Checks if a tile is only accessible via a diagonal move. """ # can be reached by cardinal if self.count_adjacent_walls(x, y) > 0: return False # can be reached by diagonal if self.count_neighbouring_walls(x, y) > 0: return True # not accessible at all return False
[docs] def is_in_border(self, x: int, y: int): """ Check if a position is in the border of the map """ border_size = self.border_size width = self.map_data.width height = self.map_data.height # top and bottom for _x in range(0, width): for _y in range(0, border_size): if (_x, _y) == (x, y) or (_x, height - _y - 1) == (x, y): return True # left and right for _x in range(0, border_size): for _y in range(0, height): if (_x, _y) == (x, y) or (width - _x - 1, _y) == (x, y): return True return False
[docs] def count_neighbouring_walls(self, x: int, y: int) -> int: """ Get the number of walls in 8 directions. """ wall_counter = 0 width = self.map_data.width height = self.map_data.height for neighbor_x in range(x - 1, x + 2): for neighbor_y in range(y - 1, y + 2): # Edges are considered alive. Makes map more likely to appear naturally closed. if neighbor_x < 0 or neighbor_y < 0 or neighbor_y >= height or neighbor_x >= width: wall_counter += 1 elif self.map_of_categories[neighbor_x][neighbor_y] == TileCategory.WALL: # exclude (x,y) from adjacency check if (neighbor_x != x) or (neighbor_y != y): wall_counter += 1 return wall_counter
[docs] def count_adjacent_walls(self, x: int, y: int) -> int: """ Get the number of walls in 4 directions. """ wall_counter = 0 left = (x - 1, y) right = (x + 1, y) top = (x, y - 1) bot = (x, y + 1) for _x, _y in (left, right, top, bot): # count out of bounds as a wall if self.is_in_bounds(_x, _y): if self.map_of_categories[_x][_y] == TileCategory.WALL: wall_counter += 1 else: wall_counter += 1 return wall_counter
@property def tiles_map(self) -> List[List[Tile]]: """ Returns an array of Tiles by converting values from map_of_categories to tiles. """ generated_level: List[List[Tile]] = [] width = len(self.map_of_categories) # use map of categories as this accounts for border height = len(self.map_of_categories[0]) if self._tiles_map and not self.is_dirty: return self._tiles_map # build the full size map and fill with tunnel sprites for x in range(width): generated_level.append([]) for y in range(height): tile = _create_tile_from_category(x, y, self.map_of_categories[x][y], self.map_data.sprite_paths) generated_level[x].append(tile) # overwrite tunnel sprites with room sprites for room in self.placed_rooms: sprite_paths = library.ROOMS[room.key].sprite_paths start_x = room.start_x start_y = room.start_y for x in range(room.width): for y in range(room.height): target_x = min(start_x + x, width - 1) target_y = min(start_y + y, height - 1) tile = _create_tile_from_category(target_x, target_y, room.tile_categories[x][y], sprite_paths) generated_level[target_x][target_y] = tile # update self self._tiles_map = generated_level self.is_dirty = False return self._tiles_map @property def bools_map(self) -> List[List[bool]]: """ Returns an array of bools by converting values from map_of_categories to bool. Floor == True, Wall == False. """ bools_map: List[List[bool]] = [] width = len(self.map_of_categories) # use map of categories as this accounts for border height = len(self.map_of_categories[0]) if self._bools_map and not self.is_dirty: return self._bools_map for x in range(width): bools_map.append([]) for y in range(height): if self.map_of_categories[x][y] == TileCategory.FLOOR: bools_map[x].append(True) else: bools_map[x].append(False) # update self self._bools_map = bools_map self.is_dirty = False return self._bools_map @property def passable_map(self) -> List[List[Literal[True]]]: """ 2d array of True, matching map size """ passable_map: List[List[Literal[True]]] = [] width = len(self.map_of_categories) # use map of categories as this accounts for border height = len(self.map_of_categories[0]) # if we have the passable map already created if self._passable_map and not self.is_dirty: return self._passable_map for x in range(width): passable_map.append([]) for y in range(height): passable_map[x].append(True) # update self self._passable_map = passable_map self.is_dirty = False return self._passable_map @property def generation_string(self) -> str: # add map info gen_info = "==== DUNGEN ==== \n" gen_info += f"==== Map Details ==== \n" gen_info += f"{self.map_data.name} | w:{self.map_data.width}, h:{self.map_data.height} | " gen_info += f"border:{self.border_size} | rooms: {len(self.placed_rooms)} \n" # add info from each room gen_info += f"==== Room Details ==== \n" for room in self.placed_rooms: gen_info += room.generation_info + "\n" # add tile info gen_info += f"==== Tile Details ==== \n" for row in self.map_of_categories: gen_info += f"{row} \n" return gen_info
[docs] def get_room(self, x: int, y: int) -> Optional[RoomConcept]: """ Returns the room at xy. """ _room = RoomConcept([], "", "", x, y) for room in self.placed_rooms: if room.intersects(_room): return room return None
[docs] def get_room_data(self, key: str) -> RoomConceptData: """ Get the data for a room based on key. """ if key in self.rooms_data: room_data = self.rooms_data[key] else: room_data = library.ROOMS[key] self.rooms_data[key] = room_data return room_data
[docs] def get_actor_data(self, key: str) -> ActorData: """ Get the data for an actor based on key. """ if key in self.actors_data: actor_data = self.actors_data[key] else: actor_data = library.ACTORS[key] self.actors_data[key] = actor_data return actor_data
[docs] def create_entities(self): """ Create all entities listed in rooms """ for room in self.placed_rooms: for actor_key, pos in room.actors.items(): actor_data = self.get_actor_data(actor_key) # create actor if actor_key != "player": actor = scripts.engine.core.matter.create_actor(actor_data, (pos[0], pos[1])) else: actor = scripts.engine.core.matter.create_actor(actor_data, (pos[0], pos[1]), True)
[docs] def set_tile_category(self, x: int, y: int, category: TileCategoryType): """ Set the tile category at xy in map_of_categories. Marks map as dirty """ self.map_of_categories[x][y] = category self.is_dirty = True
[docs]@dataclass class RoomConcept: """ Details of a room. Used for world generation. """ # what to place in a tile. Needed to hold values when returning room tile_categories: List[List[TileCategoryType]] design: str # algorithm used to generate key: str # the type of room placed start_x: int = -1 start_y: int = -1 actors: Dict[str, Tuple[int, int]] = field(default_factory=dict) # key, position @property def id(self) -> str: """ Return the id. Uses xy. """ _id = f"{self.start_x},{self.start_y}" return _id @property def available_area(self) -> int: """ Number of unblocked tiles. """ unblocked_count = 0 for row in self.tile_categories: for tile_cat in row: if tile_cat == TileCategory.FLOOR: unblocked_count += 1 return unblocked_count @property def total_area(self) -> int: """ Number of tiles in room. """ count = 0 for row in self.tile_categories: for tile_cat in row: count += 1 return count @property def width(self) -> int: """ Widest width. """ try: width = len(self.tile_categories) except IndexError: raise Exception("Something referenced room width before the room had any tile categories.") return width @property def height(self) -> int: """ Tallest height """ try: height = len(self.tile_categories[0]) except IndexError: raise Exception("Something referenced room height before the room had any tile categories.") return height @property def generation_info(self) -> str: """ Return the generation information about the room """ gen_info = ( f"{self.key} | {self.design} | {self.id} | w:{self.width}, h:{self.height} | " f"available:{self.available_area}/ total:{self.total_area} | actors:{self.actors}" ) return gen_info @property def end_x(self) -> int: return self.start_x + self.width @property def end_y(self) -> int: return self.start_y + self.height @property def centre_x(self) -> int: return (self.width // 2) + self.start_x @property def centre_y(self) -> int: return (self.height // 2) + self.start_y
[docs] def intersects(self, room: RoomConcept) -> bool: """ Check if this room intersects with another. """ if (self.start_x <= room.end_x and self.end_x >= room.start_x) and ( self.start_y <= room.end_y and self.end_y >= room.start_y ): return True return False
############################ GENERATE MAP ############################
[docs]def generate( map_name: str, rng: random.Random, player_data: Optional[ActorData] = None ) -> Tuple[List[List[Tile]], str]: """ Generate the map using the specified details. """ # create generator dungen = DungeonGenerator(rng, library.MAPS[map_name]) # generate the level for _ in _generate_map_in_steps(dungen): pass # generate entities for _ in _generate_entities_in_steps(dungen, player_data): pass # create the generated entities dungen.create_entities() return dungen.tiles_map, dungen.generation_string
[docs]def generate_steps(map_name: str) -> Iterator: """ Generates a map, returning each step of the generation. Used for dev view. """ # create generator dungen = DungeonGenerator(random.Random(), library.MAPS[map_name]) for step in _generate_map_in_steps(dungen): yield step for step in _generate_entities_in_steps(dungen): yield step
def _generate_map_in_steps(dungen: DungeonGenerator) -> Iterator: """ Generate the next step of the map generation. """ rooms_placed = 0 placement_attempts = 0 rooms_generated = 0 # set everything to walls dungen.map_of_categories = [] map_width = dungen.map_data.width map_height = dungen.map_data.height for x in range(map_width + (dungen.border_size * 2)): dungen.map_of_categories.append([]) # create new list for every col for y in range(map_height + (dungen.border_size * 2)): dungen.map_of_categories[x].append(TileCategory.WALL) yield dungen.map_of_categories # get room options for this map rooms = dungen.map_data.rooms room_names = [] room_weights = [] # break out names and weights for name, weight in rooms.items(): room_names.append(name) room_weights.append(weight) # generate and place rooms max_rooms = dungen.rng.randint(dungen.map_data.min_rooms, dungen.map_data.max_rooms) max_generate_room_attempts = dungen.max_generate_room_attempts while rooms_placed <= max_rooms and rooms_generated <= max_generate_room_attempts: found_place = False room = _generate_room(dungen, room_names, room_weights) rooms_generated += 1 # find place for the room while placement_attempts < dungen.max_place_room_attempts and not found_place: placement_attempts += 1 found_place = _place_room(dungen, room) # if no place found for the room try again if not found_place: continue # doesnt intersect so paint room on map and add room to list for room_x in range(room.width): for room_y in range(room.height): map_x = room.start_x + room_x map_y = room.start_y + room_y in_bounds = dungen.is_in_bounds(map_x, map_y) in_border = dungen.is_in_border(map_x, map_y) if in_bounds and not in_border: dungen.map_of_categories[map_x][map_y] = room.tile_categories[room_x][room_y] dungen.positions_in_rooms.append((map_x, map_y)) # place room dungen.placed_rooms.append(room) rooms_placed += 1 # yield map after each room is painted yield dungen.map_of_categories # handle anything dodgy if rooms_placed == 0: raise Exception("No rooms placed on the map.") # room placement complete, fill tunnels, without connecting to rooms for x in range(map_width): for y in range(map_height): # check is not part of a room if not dungen.is_in_room(x, y): # if its a wall, start a tunnel if dungen.map_of_categories[x][y] == TileCategory.WALL: if _add_tunnel(dungen, x, y): yield dungen.map_of_categories # join rooms to tunnels for room in dungen.placed_rooms: _add_entrances(dungen, room) yield dungen.map_of_categories _make_rooms_accessible(dungen) yield dungen.map_of_categories # N.B. do this last to keep more of the random tunnels _remove_deadends(dungen) yield dungen.map_of_categories def _generate_entities_in_steps(dungen: DungeonGenerator, player_data: Optional[ActorData] = None) -> Iterator: """ Add entities to all rooms using the room data. Also places player. """ # randomise order of rooms rooms = dungen.placed_rooms dungen.rng.shuffle(rooms) # we might not have player data if we are viewing the generated maps if player_data: # put player in first room player_room = rooms[0] placed = False placement_attempts = 0 while placement_attempts <= 1000 and not placed: xy = _find_place_for_actor(dungen, player_room, player_data) placement_attempts += 1 if xy: x, y = xy # add player data to list so we can use it when creating the entities dungen.actors_data["player"] = player_data # log actor in room player_room.actors["player"] = (x, y) dungen.set_tile_category(x, y, TileCategory.PLAYER) placed = True # just in case player hasnt been placed if not placed: raise Exception("Dungen: Unable to place player.") yield dungen.map_of_categories # work through all rooms and populate skipped_player_room = False for room in rooms: # make sure to skip player room as that is handled differently if not skipped_player_room: skipped_player_room = True continue room_key = room.key room_data = dungen.get_room_data(room_key) # generate the actor actors_placed = 0 placement_attempts = 0 max_attempts = dungen.max_place_entity_attempts max_actors = dungen.rng.randint(room_data.min_actors, room_data.max_actors) # if this room is empty of actors go to next room if max_actors == 0: continue # try and place the actor while actors_placed <= max_actors and placement_attempts <= max_attempts: actor_data = _generate_actor(dungen, room_data) # try to place the actor xy = _find_place_for_actor(dungen, room, actor_data) placement_attempts += 1 if xy: x, y = xy # log actor in room (for generation string) room.actors[actor_data.key] = (x, y) # mark actors on map, handle multi tile. for pos in actor_data.position_offsets: dungen.set_tile_category(x + pos[0], y + pos[1], TileCategory.ACTOR) actors_placed += 1 yield dungen.map_of_categories ############################ ROOMS ############################ def _generate_room(dungen: DungeonGenerator, room_names: List[str], room_weights: List[float]) -> RoomConcept: """ Select a room type to generate and return that room. If a generation method isnt provided then one is picked at random, using weightings in the data. """ # N.B. add more generation methods here design_methods = { "square": _generate_room_square, } # pick a room based on weights room_name = dungen.rng.choices(room_names, room_weights, k=1)[0] room_data = library.ROOMS[room_name] room = design_methods[room_data.design](dungen, room_data) return room def _generate_room_square(dungen: DungeonGenerator, room_data: RoomConceptData) -> RoomConcept: """ Generate a square-shaped room. """ map_width = dungen.map_data.width map_height = dungen.map_data.height # ensure not bigger than the map room_width = min(dungen.rng.randint(room_data.min_width, room_data.max_width), map_width) room_height = min(dungen.rng.randint(room_data.min_height, room_data.max_height), map_height) # populate area with floor categories tile_categories: List[List[TileCategoryType]] = [] for x in range(room_width): tile_categories.append([]) for y in range(room_height): tile_categories[x].append(TileCategory.FLOOR) # convert to room room = RoomConcept(tile_categories=tile_categories, design="square", key=room_data.key) return room def _place_room(dungen: DungeonGenerator, room: RoomConcept) -> bool: """ Place room in random location. Updates room start_x and start_y. Returns True if valid placement found. """ intersects = False map_width = dungen.map_data.width map_height = dungen.map_data.height border_size = dungen.border_size # pick random location to place room, not including borders room.start_x = dungen.rng.randint(border_size, max(border_size, map_width - room.width - 1)) room.start_y = dungen.rng.randint(border_size, max(border_size, map_height - room.height - 1)) # if placed there does room overlap any existing rooms? for _room in dungen.placed_rooms: if room.intersects(_room): intersects = True break if not intersects: return True else: return False ####################### MAP AMENDMENTS ############################## def _add_tunnel(dungen: DungeonGenerator, x: int, y: int) -> bool: """ Follow a path from origin (xy) setting relevant position in map_of_categories to TileCategory.FLOOR. Uses flood fill. Returns True if tunnel added. """ added_tunnel = False to_fill_positions = set() last_direction = (0, 0) # check position we've been given is OK before entering loop in_bounds = dungen.is_in_bounds(x, y) in_border = dungen.is_in_border(x, y) in_room = dungen.is_in_room(x, y) num_walls = dungen.count_neighbouring_walls(x, y) # only start where no other floors are if in_bounds and not in_room and not in_border and num_walls >= 8: # first position is good, add to list to_fill_positions.add((x, y)) while to_fill_positions: possible_directions = [] # get next pos in set _x, _y = to_fill_positions.pop() # convert to floor dungen.set_tile_category(_x, _y, TileCategory.FLOOR) dungen.positions_in_rooms.append((_x, _y)) added_tunnel = True # check for appropriate, adjacent wall tiles for x_dir, y_dir in (Direction.UP, Direction.DOWN, Direction.LEFT, Direction.RIGHT): x_check = _x + x_dir y_check = _y + y_dir in_bounds = dungen.is_in_bounds(x_check, y_check) in_room = dungen.is_in_room(x_check, y_check) in_border = dungen.is_in_border(x_check, y_check) num_walls = dungen.count_adjacent_walls(x_check, y_check) # can only move cardinal so check that # direction must be in bounds, not in a room and be surrounded by walls on all but X sides # must also not be adjacent to a room - i.e. can connect to a tunnel but not a room. # N.B. the lower the num walls the more overlapping and joined up the tunnels are if in_bounds and not in_room and not in_border and num_walls >= 3: if dungen.map_of_categories[x_check][y_check] == TileCategory.WALL and not dungen.is_in_room( _x + (x_dir * 2), _y + (y_dir * 2) ): possible_directions.append((x_dir, y_dir)) # choose next direction to go in if possible_directions: # pick a possible position, preferring previous direction, unless tunnel winds if ( last_direction in possible_directions and dungen.rng.randint(1, 100) > dungen.map_data.chance_of_tunnel_winding ): new_direction = last_direction else: new_direction = dungen.rng.choice(possible_directions) # add next position to be checked to_fill_positions.add((_x + new_direction[0], _y + new_direction[1])) # update last direction last_direction = new_direction return added_tunnel def _add_ignorant_tunnel(dungen: DungeonGenerator, start_x: int, start_y: int, end_x: int, end_y: int): """ Create a tunnel between two points, ignoring all terrain on the way """ x = min(start_x, end_x) y = min(start_y, end_y) max_x = max(start_x, end_x) - 1 max_y = max(start_y, end_y) - 1 while x < max_x: if dungen.is_in_bounds(x, y): dungen.set_tile_category(x, y, TileCategory.FLOOR) x += 1 while y < max_y: if dungen.is_in_bounds(x, y): dungen.set_tile_category(x, y, TileCategory.FLOOR) y += 1 def _add_entrances(dungen: DungeonGenerator, room: RoomConcept): """ Loop the outer edge of the room for two adjoining floors and break through to link the locations. """ entrances = 0 attempts = 0 placed_entrances = set() # print(f"Add entrance to room: x:{room.start_x} | end_x:{room.end_x} | y:{room.start_y} | end_y:{room.end_y}") # roll for an extra entrance base_num_entrances = dungen.map_data.max_room_entrances if dungen.rng.randint(1, 100) >= dungen.map_data.extra_entrance_chance: _max_entrances = base_num_entrances + 1 else: _max_entrances = base_num_entrances # roll for max entrances, ensure minimum 1 max_entrances = dungen.rng.randint(1, max(1, _max_entrances)) # find somewhere to place the entrance while attempts <= dungen.max_place_entrance_attempts and entrances <= max_entrances: attempts += 1 poss_positions = [] # pick random positions top_pos = (room.start_x + dungen.rng.randint(1, room.width - 1), room.start_y - 1) top_pos2 = top_pos[0], top_pos[1] - 1 bot_pos = (room.start_x + dungen.rng.randint(1, room.width - 1), room.start_y + room.height + 1) bot_pos2 = bot_pos[0], bot_pos[1] + 1 left_pos = (room.start_x - 1, room.start_y + dungen.rng.randint(1, room.height - 1)) left_pos2 = left_pos[0] - 1, left_pos[1] right_pos = (room.start_x + room.width + 1, room.start_y + dungen.rng.randint(1, room.height - 1)) right_pos2 = right_pos[0] + 1, right_pos[1] # print(f"-> Random pos: top:{top_pos}:{top_pos2} | bot:{bot_pos}:{bot_pos2} | left:{left_pos}" # f":{left_pos2} | right:{right_pos}:{right_pos2}") # note which ones are applicable for _pos, _pos2 in ((top_pos, top_pos2), (bot_pos, bot_pos2), (left_pos, left_pos2), (right_pos, right_pos2)): pos2_is_floor = False x, y = _pos2 # if second pos in bounds then first and target must be in_bounds = dungen.is_in_bounds(x, y) in_border = dungen.is_in_border(x, y) if in_bounds and not in_border: if dungen.map_of_categories[x][y] == TileCategory.FLOOR: pos2_is_floor = True # if target is wall and one after is floor and not already a placed entrance if pos2_is_floor and _pos not in placed_entrances: poss_positions.append(_pos) # pick one of the possible options and update the map if poss_positions: pos = dungen.rng.choice(poss_positions) dungen.set_tile_category(pos[0], pos[1], TileCategory.FLOOR) placed_entrances.add(pos) entrances += 1 def _remove_deadends(dungen: DungeonGenerator): """ Find all instances where a tunnel has a deadend and uncarve it, converting it back to a wall. """ deadends = set() # find initial deadends for x in range(0, dungen.map_data.width): for y in range(0, dungen.map_data.height): if dungen.map_of_categories[x][y] == TileCategory.FLOOR and dungen.count_adjacent_walls(x, y) >= 3: deadends.add((x, y)) while deadends: _x, _y = deadends.pop() # mark as wall dungen.set_tile_category(_x, _y, TileCategory.WALL) # check around where we just amended for new deadends for x_dir, y_dir in ( Direction.UP, Direction.DOWN, Direction.LEFT, Direction.RIGHT, Direction.UP_LEFT, Direction.UP_RIGHT, Direction.DOWN_LEFT, Direction.DOWN_RIGHT, ): x_check = _x + x_dir y_check = _y + y_dir in_bounds = dungen.is_in_bounds(x_check, y_check) if in_bounds: tile_cat = dungen.map_of_categories[x_check][y_check] num_walls = dungen.count_adjacent_walls(x_check, y_check) if num_walls >= 3 and tile_cat == TileCategory.FLOOR: deadends.add((x_check, y_check)) def _make_rooms_accessible(dungen: DungeonGenerator): """ Pick a room as the anchor and make sure all other floor tiles can connect to it via pathfinding. """ connections: Dict[str, List[str]] = {} # room, connected rooms inaccessible_rooms = dungen.placed_rooms.copy() # start by picking one room that all other rooms should connect to anchor_room = dungen.rng.choice(dungen.placed_rooms) # pick spot in room as anchor and make sure it is open anchor_x = anchor_room.centre_x anchor_y = anchor_room.centre_y dungen.set_tile_category(anchor_x, anchor_y, TileCategory.FLOOR) # loop all rooms until they can all get to the anchor room while inaccessible_rooms: room = inaccessible_rooms.pop() # dont check anchor room if room == anchor_room: continue room_x = room.centre_x room_y = room.centre_y # ensure the centre of the room is open dungen.set_tile_category(room_x, room_y, TileCategory.FLOOR) # check if route is possible between anchor and target pathfinder = tcod.path.Dijkstra(dungen.bools_map, 0) pathfinder.set_goal(room_x, room_y) path = pathfinder.get_path(anchor_x, anchor_y) # if no possible route, make one if not path: # get ignore list i.e. connected rooms ignore_list = [] for _room_id, connected_rooms_ids in connections.items(): if _room_id == room.id: # if the key matches then add all of the values in the list ignore_list.extend(connected_rooms_ids) elif room.id in connected_rooms_ids: # if id in the value list then add the key ignore_list.append(_room_id) # get nearest room and tunnel to that or the anchor nearest_room = _get_nearest_room(dungen, room, ignore_list) if nearest_room: _add_ignorant_tunnel(dungen, room_x, room_y, nearest_room.centre_x, nearest_room.centre_y) # log the connection if room.id in connections: connections[room.id].append(nearest_room.id) else: connections[room.id] = [nearest_room.id] # add back to list to check can now reach anchor inaccessible_rooms.append(room) else: _add_ignorant_tunnel(dungen, room_x, room_y, anchor_room.centre_x, anchor_room.centre_y) else: # we have a path so log the connection to the anchor room if anchor_room.id in connections: connections[anchor_room.id].append(room.id) else: connections[anchor_room.id] = [room.id] def _get_nearest_room( dungen: DungeonGenerator, base_room: RoomConcept, id_ignore_list: List[str] ) -> Optional[RoomConcept]: """ Starting from xy find the directly nearest room. Treats all tiles as floors. """ nearest_room = None shortest_path_length = 0 base_x = base_room.centre_x base_y = base_room.centre_y # create pathfinder pathfinder = tcod.path.Dijkstra(dungen.passable_map, 0) pathfinder.set_goal(base_x, base_y) # check each room to see which is closest for room in dungen.placed_rooms: # check ignore list if room.id in id_ignore_list or room.id == base_room.id: continue # create new path path = pathfinder.get_path(room.centre_x, room.centre_y) path_length = len(path) # if we havent got any room yet set values if not nearest_room: nearest_room = room shortest_path_length = path_length continue # check length if path_length < shortest_path_length: shortest_path_length = path_length nearest_room = room return nearest_room ####################### ENTITIES ############################## def _generate_actor(dungen: DungeonGenerator, room_data: RoomConceptData) -> ActorData: """ Pick an actor using the possible options in the room and their weighting and return the actor's data. """ # pick an actor keys = [] weights = [] for key, weight in room_data.actors.items(): keys.append(key) weights.append(weight) actor_key = dungen.rng.choices(keys, weights, k=1)[0] # get the actor data actor_data = dungen.get_actor_data(actor_key) return actor_data def _find_place_for_actor( dungen: DungeonGenerator, room: RoomConcept, actor_data: ActorData ) -> Optional[Tuple[int, int]]: """ Keep picking random locations in a room to place the actor. Returns xy if successful. """ # pick random location to place actor x = room.start_x + dungen.rng.randint(1, max(1, room.width - 1)) y = room.start_y + dungen.rng.randint(1, max(1, room.height - 1)) # check if actor is blocked blocked = True for pos in actor_data.position_offsets: offset_x = x + pos[0] offset_y = y + pos[1] # only need to check tile category as that captures entity placement too if dungen.map_of_categories[offset_x][offset_y] == TileCategory.FLOOR: blocked = False else: blocked = True # if blocked try again if not blocked: return x, y else: return None ####################### HELPER FUNCTIONS ############################## def _create_tile_from_category(x: int, y: int, tile_category: TileCategoryType, sprite_paths: Dict[str, str]) -> Tile: """ Convert a tile category into the relevant tile. If it isnt a wall it is floor by default. """ if tile_category == TileCategory.WALL: sprite_path = sprite_paths[TileCategory.WALL] sprite = utility.get_image(sprite_path) height = Height.MAX blocks_movement = True else: # everything else is considered a floor: sprite_path = sprite_paths[TileCategory.FLOOR] sprite = utility.get_image(sprite_path) height = Height.MIN blocks_movement = False tile = Tile(x, y, sprite, sprite_path, blocks_movement, height) return tile