diff --git a/erynrl/engine.py b/erynrl/engine.py index 5cc2a92..4c50099 100644 --- a/erynrl/engine.py +++ b/erynrl/engine.py @@ -18,7 +18,8 @@ from .geometry import Point, Size from .interface import Interface from .map import Map from .map.generator import RoomsAndCorridorsGenerator -from .map.generator.room import RoomGenerator, RandomRectMethod, RectangularRoomMethod +from .map.generator.cellular_atomata import CellularAtomataMapGenerator +from .map.generator.room import BSPRectMethod, CellularAtomatonRoomMethod, OrRoomMethod, RoomGenerator, RandomRectMethod, RectangularRoomMethod from .map.generator.corridor import ElbowCorridorGenerator from .messages import MessageLog from .object import Actor, Entity, Hero, Monster @@ -61,10 +62,16 @@ class Engine: RoomGenerator( size=map_size, config=RoomGenerator.Configuration( - rect_method=RandomRectMethod( + rect_method=BSPRectMethod( size=map_size, - config=RandomRectMethod.Configuration(number_of_rooms=4)), - room_method=RectangularRoomMethod()) + config=BSPRectMethod.Configuration(number_of_rooms=30)), + room_method=OrRoomMethod( + methods=[ + (0.2, CellularAtomatonRoomMethod(CellularAtomataMapGenerator.Configuration())), + (0.8, RectangularRoomMethod()) + ] + ) + ) ), ElbowCorridorGenerator()) self.map = Map(config, map_generator) diff --git a/erynrl/log.py b/erynrl/log.py index af8ccdb..8f7e4d1 100644 --- a/erynrl/log.py +++ b/erynrl/log.py @@ -28,6 +28,7 @@ EVENTS = logging.getLogger(_log_name('events')) UI = logging.getLogger(_log_name('ui')) MAP = logging.getLogger(_log_name('map')) +MAP_BSP = logging.getLogger(_log_name('map', 'bsp')) MAP_CELL_ATOM = logging.getLogger(_log_name('map', 'cellular')) diff --git a/erynrl/map/generator/room.py b/erynrl/map/generator/room.py index d0ee4e9..1b7cbee 100644 --- a/erynrl/map/generator/room.py +++ b/erynrl/map/generator/room.py @@ -185,6 +185,109 @@ class RandomRectMethod(RectMethod): yield candidate_rect +class BSPRectMethod(RectMethod): + @dataclass + class Configuration: + ''' + Configuration for the binary space partitioning (BSP) Rect method. + + ### Attributes + + number_of_rooms : int + The maximum number of rooms to produce + maximum_room_size : Size + The maximum size of any room + minimum_room_size : Size + The minimum size of any room + room_size_ratio : Tuple[float, float] + A pair of floats indicating the maximum proportion the sides of a + BSP node can have to each other. + + The first value is the horizontal ratio. BSP nodes will never have a + horizontal size (width) bigger than `room_size_ratio[0]` times the + vertical size. + + The second value is the vertical ratio. BSP nodes will never have a + vertical size (height) larger than `room_size_ratio[1]` times the + horizontal size. + + The closer these values are to 1.0, the more square the BSP nodes + will be. + ''' + number_of_rooms: int = 30 + minimum_room_size: Size = Size(7, 7) + maximum_room_size: Size = Size(20, 20) + room_size_ratio: Tuple[float, float] = (1.1, 1.1) + + def __init__(self, *, size: Size, config: Optional[Configuration] = None): + super().__init__(size=size) + self.configuration = config or self.__class__.Configuration() + + def generate(self) -> Iterator[Rect]: + nodes_with_rooms = set() + + 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=6, + min_width=minimum_room_size.width, + min_height=minimum_room_size.height, + max_horizontal_ratio=self.configuration.room_size_ratio[0], + max_vertical_ratio=self.configuration.room_size_ratio[1]) + + log.MAP_BSP.info('Generating room rects via BSP') + + # Visit all nodes in a level before visiting any of their children + for bsp_node in bsp.level_order(): + node_width = bsp_node.w + node_height = bsp_node.h + + if node_width > maximum_room_size.width or node_height > maximum_room_size.height: + log.MAP_BSP.debug('Node with size (%s, %s) exceeds maximum size %s', + node_width, node_height, maximum_room_size) + continue + + if len(nodes_with_rooms) >= self.configuration.number_of_rooms: + # Made as many rooms as we're allowed. We're done. + log.MAP_BSP.debug("Generated enough rooms (more than %d); we're done", + self.configuration.number_of_rooms) + return + + if any(node in nodes_with_rooms for node in self.__all_parents_of_node(bsp_node)): + # Already made a room for one of this node's parents + log.MAP_BSP.debug('Already made a room for parent of %s', bsp_node) + continue + + try: + probability_of_room = max( + 1.0 / (node_width - minimum_room_size.width), + 1.0 / (node_height - minimum_room_size.height)) + except ZeroDivisionError: + probability_of_room = 1.0 + + log.MAP_BSP.info('Probability of generating room for %s: %f', bsp_node, probability_of_room) + + if random.random() <= probability_of_room: + log.MAP_BSP.info('Yielding room for node %s', bsp_node) + nodes_with_rooms.add(bsp_node) + yield self.__rect_from_bsp_node(bsp_node) + + log.MAP_BSP.info('Finished BSP room rect generation, yielded %d rooms', len(nodes_with_rooms)) + + def __rect_from_bsp_node(self, bsp_node: tcod.bsp.BSP) -> Rect: + return Rect.from_raw_values(bsp_node.x, bsp_node.y, bsp_node.w, bsp_node.h) + + def __all_parents_of_node(self, node: tcod.bsp.BSP | None) -> Iterable[tcod.bsp.BSP]: + while node: + yield node + node = node.parent + + class RoomMethod: '''An abstract class defining a method for generating rooms.''' @@ -292,94 +395,3 @@ class RandomRectRoomGenerator(RoomGenerator): 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)) diff --git a/erynrl/map/room.py b/erynrl/map/room.py index 5228620..60a3510 100644 --- a/erynrl/map/room.py +++ b/erynrl/map/room.py @@ -115,3 +115,6 @@ class FreeformRoom(Room): for y, x in np.ndindex(self.tiles.shape): if self.tiles[y, x]['walkable']: yield Point(x, y) + room_origin_vector + + def __str__(self): + return '\n'.join(''.join(chr(i['light']['ch']) for i in row) for row in self.tiles)