diff --git a/erynrl/engine.py b/erynrl/engine.py index 0bdeca0..c81ec4f 100644 --- a/erynrl/engine.py +++ b/erynrl/engine.py @@ -57,14 +57,11 @@ class Engine: self.rng = tcod.random.Random() self.message_log = MessageLog() - map_size = config.map_size map_generator = RoomsAndCorridorsGenerator( RoomGenerator( - size=map_size, - config=RoomGenerator.Configuration( + RoomGenerator.Configuration( rect_method=BSPRectMethod( - size=map_size, - config=BSPRectMethod.Configuration(number_of_rooms=30)), + BSPRectMethod.Configuration(number_of_rooms=30)), room_method=OrRoomMethod( methods=[ (0.2, CellularAtomatonRoomMethod(CellularAtomataMapGenerator.Configuration())), diff --git a/erynrl/map/__init__.py b/erynrl/map/__init__.py index fb1ae32..a4216b7 100644 --- a/erynrl/map/__init__.py +++ b/erynrl/map/__init__.py @@ -6,7 +6,7 @@ parts of a map. ''' import random -from typing import Iterable +from typing import Iterable, List import numpy as np import tcod @@ -14,6 +14,7 @@ import tcod from ..configuration import Configuration from ..geometry import Point, Rect, Size from .generator import MapGenerator +from .room import Corridor, Room from .tile import Empty, Shroud @@ -29,9 +30,6 @@ class Map: shape = map_size.numpy_shape self.tiles = np.full(shape, fill_value=Empty, order='F') - self.up_stairs = generator.up_stairs - self.down_stairs = generator.down_stairs - self.highlighted = np.full(shape, fill_value=False, order='F') # Map tiles that are currently visible to the player @@ -44,6 +42,12 @@ class Map: generator.generate(self) + # Map Features + self.rooms: List[Room] = [] + self.corridors: List[Corridor] = [] + self.up_stairs = generator.up_stairs + self.down_stairs = generator.down_stairs + @property def bounds(self) -> Rect: '''The bounds of the map''' diff --git a/erynrl/map/generator/__init__.py b/erynrl/map/generator/__init__.py index d82d44e..1fc1d8d 100644 --- a/erynrl/map/generator/__init__.py +++ b/erynrl/map/generator/__init__.py @@ -1,8 +1,10 @@ # Eryn Wells -from typing import List, TYPE_CHECKING +''' +This module defines a bunch of mechanisms for generating maps. +''' -import numpy as np +from typing import List, TYPE_CHECKING from .corridor import CorridorGenerator from .room import RoomGenerator @@ -25,7 +27,6 @@ class MapGenerator: '''The location of any routes to a lower floor of the dungeon.''' raise NotImplementedError() - # pylint: disable=redefined-builtin def generate(self, map: 'Map'): '''Generate a map and place it in `tiles`''' raise NotImplementedError() @@ -48,10 +49,8 @@ class RoomsAndCorridorsGenerator(MapGenerator): def down_stairs(self) -> List[Point]: return self.room_generator.down_stairs - # pylint: disable=redefined-builtin def generate(self, map: 'Map'): - self.room_generator.generate() + self.room_generator.generate(map) self.room_generator.apply(map) - - self.corridor_generator.generate(self.room_generator.rooms) - self.corridor_generator.apply(map.tiles) + self.corridor_generator.generate(map) + self.corridor_generator.apply(map) diff --git a/erynrl/map/generator/corridor.py b/erynrl/map/generator/corridor.py index 13ce2ba..b57a3ad 100644 --- a/erynrl/map/generator/corridor.py +++ b/erynrl/map/generator/corridor.py @@ -6,30 +6,35 @@ Defines an abstract CorridorGenerator and several concrete subclasses. These cla import random from itertools import pairwise -from typing import List +from typing import List, TYPE_CHECKING import tcod -import numpy as np from ... import log from ...geometry import Point -from ..room import Room +from ..room import Corridor, Room from ..tile import Empty, Floor, Wall +if TYPE_CHECKING: + from .. import Map + class CorridorGenerator: ''' Corridor generators produce corridors between rooms. ''' - def generate(self, rooms: List[Room]) -> bool: + def generate(self, map: 'Map') -> bool: '''Generate corridors given a list of rooms.''' raise NotImplementedError() - def apply(self, tiles: np.ndarray): + def apply(self, map: 'Map'): '''Apply corridors to a tile grid.''' raise NotImplementedError() + def _sorted_rooms(self, rooms: List[Room]) -> List[Room]: + return sorted(rooms, key=lambda r: r.bounds.origin) + class ElbowCorridorGenerator(CorridorGenerator): ''' @@ -48,25 +53,23 @@ class ElbowCorridorGenerator(CorridorGenerator): ''' def __init__(self): - self.corridors: List[List[Point]] = [] + self.corridors: List[Corridor] = [] + + def generate(self, map: 'Map') -> bool: + rooms = map.rooms - def generate(self, rooms: List[Room]) -> bool: if len(rooms) < 2: return True - sorted_rooms = sorted(rooms, key=lambda r: r.bounds.origin) + sorted_rooms = self._sorted_rooms(rooms) - for (left_room, right_room) in pairwise(sorted_rooms): + for left_room, right_room in pairwise(sorted_rooms): corridor = self._generate_corridor_between(left_room, right_room) self.corridors.append(corridor) - for i in range(len(rooms) - 2): - corridor = self._generate_corridor_between(rooms[i], rooms[i + 2]) - self.corridors.append(corridor) - return True - def _generate_corridor_between(self, left_room, right_room): + def _generate_corridor_between(self, left_room, right_room) -> Corridor: left_room_bounds = left_room.bounds right_room_bounds = right_room.bounds @@ -77,12 +80,18 @@ class ElbowCorridorGenerator(CorridorGenerator): end_point = right_room_bounds.midpoint # Randomly choose whether to move horizontally then vertically or vice versa - if random.random() < 0.5: + horizontal_first = random.random() < 0.5 + if horizontal_first: corner = Point(end_point.x, start_point.y) else: corner = Point(start_point.x, end_point.y) - log.MAP.debug('Digging a tunnel between %s and %s with corner %s', start_point, end_point, corner) + log.MAP.debug( + 'Digging a tunnel between %s and %s with corner %s (%s)', + start_point, + end_point, + corner, + 'horizontal' if horizontal_first else 'vertical') log.MAP.debug('|-> start: %s', left_room_bounds) log.MAP.debug('`-> end: %s', right_room_bounds) @@ -94,9 +103,13 @@ class ElbowCorridorGenerator(CorridorGenerator): for x, y in tcod.los.bresenham(tuple(corner), tuple(end_point)).tolist(): corridor.append(Point(x, y)) - return corridor + return Corridor(points=corridor) + + def apply(self, map: 'Map'): + tiles = map.tiles + + map.corridors = self.corridors - def apply(self, tiles): for corridor in self.corridors: for pt in corridor: tiles[pt.x, pt.y] = Floor diff --git a/erynrl/map/generator/room.py b/erynrl/map/generator/room.py index 8872436..727bfe2 100644 --- a/erynrl/map/generator/room.py +++ b/erynrl/map/generator/room.py @@ -23,24 +23,32 @@ class RoomGenerator: @dataclass class Configuration: + ''' + Configuration of a RoomGenerator + + ### Attributes + + rect_method : RectMethod + A RectMethod object to produce rectangles + room_method : RoomMethod + A RoomMethod object to produce rooms from rectangles + ''' rect_method: 'RectMethod' room_method: 'RoomMethod' - def __init__(self, *, size: Size, config: Configuration): - self.size = size + def __init__(self, config: Configuration): self.configuration = config self.rooms: List[Room] = [] - self.up_stairs: List[Point] = [] self.down_stairs: List[Point] = [] - def generate(self): + def generate(self, map: 'Map'): '''Generate rooms and stairs''' rect_method = self.configuration.rect_method room_method = self.configuration.room_method - for rect in rect_method.generate(): + for rect in rect_method.generate(map): room = room_method.room_in_rect(rect) if not room: break @@ -51,11 +59,10 @@ class RoomGenerator: self._generate_stairs() - # 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) + self._apply_stairs(map) def _apply(self, map: 'Map'): ''' @@ -68,6 +75,8 @@ class RoomGenerator: ''' tiles = map.tiles + map.rooms = self.rooms + for room in self.rooms: for pt in room.floor_points: tiles[pt.numpy_index] = Floor @@ -93,7 +102,12 @@ class RoomGenerator: 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): + def _apply_stairs(self, map: 'Map'): + tiles = map.tiles + + map.up_stairs = self.up_stairs + map.down_stairs = self.down_stairs + for pt in self.up_stairs: tiles[pt.numpy_index] = StairsUp for pt in self.down_stairs: @@ -103,10 +117,7 @@ class RoomGenerator: class RectMethod: '''An abstract class defining a method for generating rooms.''' - def __init__(self, *, size: Size): - self.size = size - - def generate(self) -> Iterator[Rect]: + def generate(self, map: 'Map') -> Iterator[Rect]: '''Generate rects to place rooms in until there are no more.''' raise NotImplementedError() @@ -132,13 +143,14 @@ class OneBigRoomRectMethod(RectMethod): width_percentage: float = 0.5 height_percentage: float = 0.5 - def __init__(self, *, size: Size, config: Optional[Configuration] = None): - super().__init__(size=size) + def __init__(self, config: Optional[Configuration] = None): + super().__init__() self.configuration = config or self.__class__.Configuration() - def generate(self) -> Iterator[Rect]: - width = self.size.width - height = self.size.height + def generate(self, map: 'Map') -> Iterator[Rect]: + map_size = map.bounds.size + width = map_size.width + height = map_size.height size = Size(math.floor(width * self.configuration.width_percentage), math.floor(height * self.configuration.height_percentage)) @@ -156,23 +168,24 @@ class RandomRectMethod(RectMethod): minimum_room_size: Size = Size(7, 7) maximum_room_size: Size = Size(20, 20) - def __init__(self, *, size: Size, config: Optional[Configuration] = None): - super().__init__(size=size) + def __init__(self, config: Optional[Configuration] = None): self.configuration = config or self.__class__.Configuration() self._rects: List[Rect] = [] - def generate(self) -> Iterator[Rect]: + def generate(self, map: 'Map') -> Iterator[Rect]: 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) + map_size = map.size + while len(self._rects) < self.configuration.number_of_rooms: for _ in range(self.__class__.NUMBER_OF_ATTEMPTS_PER_RECT): 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)) + origin = Point(random.randint(0, map_size.width - size.width), + random.randint(0, map_size.height - size.height)) candidate_rect = Rect(origin, size) overlaps_any_existing_room = any(candidate_rect.intersects(r) for r in self._rects) @@ -186,6 +199,10 @@ class RandomRectMethod(RectMethod): class BSPRectMethod(RectMethod): + ''' + Generate rectangles with Binary Space Partitioning. + ''' + @dataclass class Configuration: ''' @@ -219,18 +236,19 @@ class BSPRectMethod(RectMethod): 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) + def __init__(self, config: Optional[Configuration] = None): self.configuration = config or self.__class__.Configuration() - def generate(self) -> Iterator[Rect]: + def generate(self, map: 'Map') -> Iterator[Rect]: nodes_with_rooms = set() minimum_room_size = self.configuration.minimum_room_size maximum_room_size = self.configuration.maximum_room_size + map_size = map.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) + bsp = tcod.bsp.BSP(x=0, y=0, width=map_size.width, height=map_size.height) # Add 2 to the minimum width and height to account for walls bsp.split_recursive( diff --git a/erynrl/map/room.py b/erynrl/map/room.py index 60a3510..95b34f3 100644 --- a/erynrl/map/room.py +++ b/erynrl/map/room.py @@ -4,7 +4,7 @@ Implements an abstract Room class, and subclasses that implement it. Rooms are basic components of maps. ''' -from typing import Iterable +from typing import Iterable, Iterator, List, Optional import numpy as np @@ -118,3 +118,21 @@ class FreeformRoom(Room): def __str__(self): return '\n'.join(''.join(chr(i['light']['ch']) for i in row) for row in self.tiles) + + +class Corridor: + ''' + A corridor is a list of points connecting two endpoints + ''' + + def __init__(self, points: Optional[List[Point]] = None): + self.points: List[Point] = points or [] + + @property + def length(self) -> int: + '''The length of this corridor''' + return len(self.points) + + def __iter__(self) -> Iterator[Point]: + for pt in self.points: + yield pt