# Eryn Wells import random from copy import copy from dataclasses import dataclass from typing import List, Optional, TYPE_CHECKING import tcod from ... import log from ...geometry import Point, Rect, Size if TYPE_CHECKING: from .. import Map class RoomGenerator: '''Abstract room generator class.''' @dataclass class Configuration: number_of_rooms: int minimum_room_size: Size maximum_room_size: Size DefaultConfiguration = Configuration( number_of_rooms=30, minimum_room_size=Size(7, 7), maximum_room_size=Size(20, 20)) def __init__(self, *, size: Size, config: Optional[Configuration] = None): self.size = size self.configuration = config if config else copy(RoomGenerator.DefaultConfiguration) self.rooms: List[Room] = [] self.up_stairs: List[Point] = [] self.down_stairs: List[Point] = [] def generate(self): '''Generate rooms and stairs''' did_generate_rooms = self._generate() if not did_generate_rooms: return self._generate_stairs() def _generate(self) -> bool: ''' Generate a list of rooms. Subclasses should implement this and fill in their specific map generation algorithm. Returns ------- np.ndarray A two-dimensional array of tiles. Dimensions should match the given size. ''' raise NotImplementedError() # pylint: disable=redefined-builtin def apply(self, map: 'Map'): '''Apply the generated rooms to a tile array''' self._apply(map) self._apply_stairs(map.tiles) def _apply(self, map: 'Map'): ''' Apply the generated list of rooms to an array of tiles. Subclasses must implement this. Arguments --------- map: Map The game map to apply the generated room to ''' tiles = map.tiles for room in self.rooms: for pt in room.floor_points: tiles[pt.x, pt.y] = Floor for room in self.rooms: for pt in room.wall_points: if tiles[pt.x, pt.y] != Empty: continue tiles[pt.x, pt.y] = Wall def _generate_stairs(self): up_stair_room = random.choice(self.rooms) down_stair_room = None if len(self.rooms) >= 2: while down_stair_room is None or down_stair_room == up_stair_room: down_stair_room = random.choice(self.rooms) else: down_stair_room = up_stair_room self.up_stairs.append(random.choice(list(up_stair_room.walkable_tiles))) self.down_stairs.append(random.choice(list(down_stair_room.walkable_tiles))) def _apply_stairs(self, tiles): for pt in self.up_stairs: tiles[pt.x, pt.y] = StairsUp for pt in self.down_stairs: tiles[pt.x, pt.y] = StairsDown class OneBigRoomGenerator(RoomGenerator): '''Generates one big room in the center of the map.''' def _generate(self) -> bool: if self.rooms: return True origin = Point(self.size.width // 4, self.size.height // 4) size = Size(self.size.width // 2, self.size.height // 2) room = RectangularRoom(Rect(origin, size)) self.rooms.append(room) return True class RandomRectRoomGenerator(RoomGenerator): '''Generate rooms by repeatedly attempting to place rects of random size across the map.''' NUMBER_OF_ATTEMPTS_PER_ROOM = 30 def _generate(self) -> bool: number_of_attempts = 0 minimum_room_size = self.configuration.minimum_room_size maximum_room_size = self.configuration.maximum_room_size width_range = (minimum_room_size.width, maximum_room_size.width) height_range = (minimum_room_size.height, maximum_room_size.height) while len(self.rooms) < self.configuration.number_of_rooms: size = Size(random.randint(*width_range), random.randint(*height_range)) origin = Point(random.randint(0, self.size.width - size.width), random.randint(0, self.size.height - size.height)) candidate_room_rect = Rect(origin, size) overlaps_any_existing_room = any(candidate_room_rect.intersects(room.bounds) for room in self.rooms) if not overlaps_any_existing_room: self.rooms.append(RectangularRoom(candidate_room_rect)) number_of_attempts = 0 continue number_of_attempts += 1 if number_of_attempts > RandomRectRoomGenerator.NUMBER_OF_ATTEMPTS_PER_ROOM: break return True class BSPRoomGenerator(RoomGenerator): '''Generate a rooms-and-corridors style map with BSP.''' def __init__(self, *, size: Size, config: Optional[RoomGenerator.Configuration] = None): super().__init__(size=size, config=config) self.rng: tcod.random.Random = tcod.random.Random() def _generate(self) -> bool: if self.rooms: return True minimum_room_size = self.configuration.minimum_room_size maximum_room_size = self.configuration.maximum_room_size # Recursively divide the map into squares of various sizes to place rooms in. bsp = tcod.bsp.BSP(x=0, y=0, width=self.size.width, height=self.size.height) # Add 2 to the minimum width and height to account for walls bsp.split_recursive( depth=4, min_width=minimum_room_size.width, min_height=minimum_room_size.height, max_horizontal_ratio=1.1, max_vertical_ratio=1.1) # Generate the rooms rooms: List[Room] = [] room_attrname = f'{__class__.__name__}.room' for node in bsp.post_order(): node_bounds = self.__rect_from_bsp_node(node) if node.children: continue log.MAP.debug('%s (room) %s', node_bounds, node) # Generate a room size between minimum_room_size and maximum_room_size. The minimum value is # straight-forward, but the maximum value needs to be clamped between minimum_room_size and the size of # the node. width_range = ( minimum_room_size.width, min(maximum_room_size.width, max( minimum_room_size.width, node.width - 2)) ) height_range = ( minimum_room_size.height, min(maximum_room_size.height, max( minimum_room_size.height, node.height - 2)) ) log.MAP.debug('|-> min room size %s', minimum_room_size) log.MAP.debug('|-> max room size %s', maximum_room_size) log.MAP.debug('|-> node size %s x %s', node.width, node.height) log.MAP.debug('|-> width range %s', width_range) log.MAP.debug('|-> height range %s', width_range) size = Size(self.rng.randint(*width_range), self.rng.randint(*height_range)) origin = Point(node.x + self.rng.randint(1, max(1, node.width - size.width - 2)), node.y + self.rng.randint(1, max(1, node.height - size.height - 2))) bounds = Rect(origin, size) log.MAP.debug('`-> %s', bounds) room = RectangularRoom(bounds) setattr(node, room_attrname, room) rooms.append(room) if not hasattr(node.parent, room_attrname): setattr(node.parent, room_attrname, room) elif random.random() < 0.5: setattr(node.parent, room_attrname, room) # Pass up a random child room so that parent nodes can connect subtrees to each other. parent = node.parent if parent: node_room = getattr(node, room_attrname) if not hasattr(node.parent, room_attrname): setattr(node.parent, room_attrname, node_room) elif random.random() < 0.5: setattr(node.parent, room_attrname, node_room) self.rooms = rooms return True def __rect_from_bsp_node(self, node: tcod.bsp.BSP) -> Rect: '''Create a Rect from the given BSP node object''' return Rect(Point(node.x, node.y), Size(node.width, node.height))