diff --git a/erynrl/map/__init__.py b/erynrl/map/__init__.py index 9c7cb26..075a915 100644 --- a/erynrl/map/__init__.py +++ b/erynrl/map/__init__.py @@ -17,7 +17,10 @@ class Map: self.generator = generator self.tiles = np.full(tuple(size), fill_value=Empty, order='F') - self.generator.generate(self.tiles) + generator.generate(self.tiles) + + self.up_stairs = generator.up_stairs + self.down_stairs = generator.down_stairs # Map tiles that are currently visible to the player self.visible = np.full(tuple(self.size), fill_value=True, order='F') diff --git a/erynrl/map/generator/corridor.py b/erynrl/map/generator/corridor.py index 752c5e8..c61b273 100644 --- a/erynrl/map/generator/corridor.py +++ b/erynrl/map/generator/corridor.py @@ -46,37 +46,49 @@ class ElbowCorridorGenerator(CorridorGenerator): self.corridors: List[List[Point]] = [] def generate(self, rooms: List[Room]) -> bool: + if len(rooms) < 2: + return True + for (left_room, right_room) in pairwise(rooms): - left_room_bounds = left_room.bounds - right_room_bounds = right_room.bounds - - log.MAP.debug(' left: %s, %s', left_room, left_room_bounds) - log.MAP.debug('right: %s, %s', right_room, right_room_bounds) - - start_point = left_room_bounds.midpoint - end_point = right_room_bounds.midpoint - - # Randomly choose whether to move horizontally then vertically or vice versa - if random.random() < 0.5: - 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('|-> start: %s', left_room_bounds) - log.MAP.debug('`-> end: %s', right_room_bounds) - - corridor: List[Point] = [] - - for x, y in tcod.los.bresenham(tuple(start_point), tuple(corner)).tolist(): - corridor.append(Point(x, y)) - for x, y in tcod.los.bresenham(tuple(corner), tuple(end_point)).tolist(): - corridor.append(Point(x, y)) + 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): + left_room_bounds = left_room.bounds + right_room_bounds = right_room.bounds + + log.MAP.debug(' left: %s, %s', left_room, left_room_bounds) + log.MAP.debug('right: %s, %s', right_room, right_room_bounds) + + start_point = left_room_bounds.midpoint + end_point = right_room_bounds.midpoint + + # Randomly choose whether to move horizontally then vertically or vice versa + if random.random() < 0.5: + 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('|-> start: %s', left_room_bounds) + log.MAP.debug('`-> end: %s', right_room_bounds) + + corridor: List[Point] = [] + + for x, y in tcod.los.bresenham(tuple(start_point), tuple(corner)).tolist(): + corridor.append(Point(x, y)) + + for x, y in tcod.los.bresenham(tuple(corner), tuple(end_point)).tolist(): + corridor.append(Point(x, y)) + + return corridor + def apply(self, tiles): for corridor in self.corridors: for pt in corridor: diff --git a/erynrl/map/generator/room.py b/erynrl/map/generator/room.py index 2655c56..bf069d6 100644 --- a/erynrl/map/generator/room.py +++ b/erynrl/map/generator/room.py @@ -8,7 +8,7 @@ import tcod from ... import log from ...geometry import Direction, Point, Rect, Size from ..room import Room, RectangularRoom -from ..tile import Empty, Floor, Wall +from ..tile import Empty, Floor, StairsUp, StairsDown, Wall class RoomGenerator: @@ -17,8 +17,17 @@ class RoomGenerator: def __init__(self, *, size: Size): self.size = size self.rooms: List[Room] = [] + self.up_stairs: List[Point] = [] + self.down_stairs: List[Point] = [] - def generate(self) -> bool: + def generate(self): + '''Generate rooms and stairs''' + did_generate_rooms = self._generate() + + if did_generate_rooms: + self._generate_stairs() + + def _generate(self) -> bool: ''' Generate a list of rooms. @@ -33,6 +42,11 @@ class RoomGenerator: raise NotImplementedError() def apply(self, tiles: np.ndarray): + '''Apply the generated rooms to a tile array''' + self._apply(tiles) + self._apply_stairs(tiles) + + def _apply(self, tiles: np.ndarray): ''' Apply the generated list of rooms to an array of tiles. Subclasses must implement this. @@ -41,7 +55,33 @@ class RoomGenerator: tiles: np.ndarray The array of tiles to update. ''' - raise NotImplementedError() + 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 BSPRoomGenerator(RoomGenerator): @@ -59,16 +99,12 @@ class BSPRoomGenerator(RoomGenerator): maximum_room_size=Size(20, 20), ) - def __init__(self, *, size: Size, config: Optional[Configuration] = None): - super().__init__(size=size) - self.configuration = config if config else BSPRoomGenerator.DefaultConfiguration +class BSPRoomGenerator(RoomGenerator): + '''Generate a rooms-and-corridors style map with BSP.''' self.rng: tcod.random.Random = tcod.random.Random() - self.rooms: List[RectangularRoom] = [] - self.tiles: Optional[np.ndarray] = None - - def generate(self) -> bool: + def _generate(self) -> bool: if self.rooms: return True @@ -89,7 +125,7 @@ class BSPRoomGenerator(RoomGenerator): ) # Generate the rooms - rooms: List['RectangularRoom'] = [] + rooms: List[Room] = [] room_attrname = f'{__class__.__name__}.room' @@ -145,30 +181,6 @@ class BSPRoomGenerator(RoomGenerator): return True - def apply(self, tiles: np.ndarray): - for room in self.rooms: - for wall_position in room.walls: - if tiles[wall_position.x, wall_position.y] != Floor: - tiles[wall_position.x, wall_position.y] = Wall - - bounds = room.bounds - # The range of a numpy array slice is [a, b). - floor_rect = bounds.inset_rect(top=1, right=1, bottom=1, left=1) - tiles[floor_rect.min_x:floor_rect.max_x + 1, - floor_rect.min_y:floor_rect.max_y + 1] = Floor - - for y in range(self.size.height): - for x in range(self.size.width): - pos = Point(x, y) - if tiles[x, y] != Floor: - continue - - neighbors = (pos + direction for direction in Direction.all()) - for neighbor in neighbors: - if tiles[neighbor.x, neighbor.y] != Empty: - continue - tiles[neighbor.x, neighbor.y] = Wall - 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 be78a04..9a2e68d 100644 --- a/erynrl/map/room.py +++ b/erynrl/map/room.py @@ -1,13 +1,13 @@ from typing import Iterator -from ..geometry import Point +from ..geometry import Point, Rect class Room: '''An abstract room. It can be any size or shape.''' - def __init__(self, bounds): - self.bounds = bounds + def __init__(self, bounds: Rect): + self.bounds: Rect = bounds @property def center(self) -> Point: @@ -15,13 +15,18 @@ class Room: return self.bounds.midpoint @property - def walls(self) -> Iterator[Point]: - '''An iterator over all the wall tiles of this room.''' + def wall_points(self) -> Iterator[Point]: + '''An iterator over all the points that make up the walls of this room.''' + raise NotImplementedError() + + @property + def floor_points(self) -> Iterator[Point]: + '''An iterator over all the points that make of the floor of this room''' raise NotImplementedError() @property def walkable_tiles(self) -> Iterator[Point]: - '''An iterator over all the walkable tiles in this room.''' + '''An iterator over all the points that are walkable in this room.''' raise NotImplementedError() @@ -43,16 +48,34 @@ class RectangularRoom(Room): yield Point(x, y) @property - def walls(self) -> Iterator[Point]: + def wall_points(self) -> Iterator[Point]: bounds = self.bounds + min_y = bounds.min_y max_y = bounds.max_y min_x = bounds.min_x max_x = bounds.max_x - for y in range(min_y, max_y + 1): - for x in range(min_x, max_x + 1): - if y == min_y or y == max_y or x == min_x or x == max_x: - yield Point(x, y) + + for x in range(min_x, max_x + 1): + yield Point(x, min_y) + yield Point(x, max_y) + + for y in range(min_y + 1, max_y): + yield Point(min_x, y) + yield Point(max_x, y) + + @property + def floor_points(self) -> Iterator[Point]: + inset_bounds = self.bounds.inset_rect(1, 1, 1, 1) + + min_y = inset_bounds.min_y + max_y = inset_bounds.max_y + min_x = inset_bounds.min_x + max_x = inset_bounds.max_x + + for x in range(min_x, max_x + 1): + for y in range(min_y, max_y + 1): + yield Point(x, y) def __repr__(self) -> str: return f'{self.__class__.__name__}({self.bounds})'