Refactor how maps, rooms, and corridors are generated

- Rect and Room method objects no longer need to know the map size up front
- The Map object has lists of interesting map features (I don't like this)
- Room and corridor generators take the map itself as an argument to their
  generate and apply methods
- Create a Corridor object to hold a list of points
- Add a bunch of documentation here and there
This commit is contained in:
Eryn Wells 2023-03-11 00:06:47 -08:00
parent e1523cd9c0
commit b0d91c9c5d
6 changed files with 111 additions and 62 deletions

View file

@ -57,14 +57,11 @@ class Engine:
self.rng = tcod.random.Random() self.rng = tcod.random.Random()
self.message_log = MessageLog() self.message_log = MessageLog()
map_size = config.map_size
map_generator = RoomsAndCorridorsGenerator( map_generator = RoomsAndCorridorsGenerator(
RoomGenerator( RoomGenerator(
size=map_size, RoomGenerator.Configuration(
config=RoomGenerator.Configuration(
rect_method=BSPRectMethod( rect_method=BSPRectMethod(
size=map_size, BSPRectMethod.Configuration(number_of_rooms=30)),
config=BSPRectMethod.Configuration(number_of_rooms=30)),
room_method=OrRoomMethod( room_method=OrRoomMethod(
methods=[ methods=[
(0.2, CellularAtomatonRoomMethod(CellularAtomataMapGenerator.Configuration())), (0.2, CellularAtomatonRoomMethod(CellularAtomataMapGenerator.Configuration())),

View file

@ -6,7 +6,7 @@ parts of a map.
''' '''
import random import random
from typing import Iterable from typing import Iterable, List
import numpy as np import numpy as np
import tcod import tcod
@ -14,6 +14,7 @@ import tcod
from ..configuration import Configuration from ..configuration import Configuration
from ..geometry import Point, Rect, Size from ..geometry import Point, Rect, Size
from .generator import MapGenerator from .generator import MapGenerator
from .room import Corridor, Room
from .tile import Empty, Shroud from .tile import Empty, Shroud
@ -29,9 +30,6 @@ class Map:
shape = map_size.numpy_shape shape = map_size.numpy_shape
self.tiles = np.full(shape, fill_value=Empty, order='F') 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') self.highlighted = np.full(shape, fill_value=False, order='F')
# Map tiles that are currently visible to the player # Map tiles that are currently visible to the player
@ -44,6 +42,12 @@ class Map:
generator.generate(self) 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 @property
def bounds(self) -> Rect: def bounds(self) -> Rect:
'''The bounds of the map''' '''The bounds of the map'''

View file

@ -1,8 +1,10 @@
# Eryn Wells <eryn@erynwells.me> # Eryn Wells <eryn@erynwells.me>
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 .corridor import CorridorGenerator
from .room import RoomGenerator from .room import RoomGenerator
@ -25,7 +27,6 @@ class MapGenerator:
'''The location of any routes to a lower floor of the dungeon.''' '''The location of any routes to a lower floor of the dungeon.'''
raise NotImplementedError() raise NotImplementedError()
# pylint: disable=redefined-builtin
def generate(self, map: 'Map'): def generate(self, map: 'Map'):
'''Generate a map and place it in `tiles`''' '''Generate a map and place it in `tiles`'''
raise NotImplementedError() raise NotImplementedError()
@ -48,10 +49,8 @@ class RoomsAndCorridorsGenerator(MapGenerator):
def down_stairs(self) -> List[Point]: def down_stairs(self) -> List[Point]:
return self.room_generator.down_stairs return self.room_generator.down_stairs
# pylint: disable=redefined-builtin
def generate(self, map: 'Map'): def generate(self, map: 'Map'):
self.room_generator.generate() self.room_generator.generate(map)
self.room_generator.apply(map) self.room_generator.apply(map)
self.corridor_generator.generate(map)
self.corridor_generator.generate(self.room_generator.rooms) self.corridor_generator.apply(map)
self.corridor_generator.apply(map.tiles)

View file

@ -6,30 +6,35 @@ Defines an abstract CorridorGenerator and several concrete subclasses. These cla
import random import random
from itertools import pairwise from itertools import pairwise
from typing import List from typing import List, TYPE_CHECKING
import tcod import tcod
import numpy as np
from ... import log from ... import log
from ...geometry import Point from ...geometry import Point
from ..room import Room from ..room import Corridor, Room
from ..tile import Empty, Floor, Wall from ..tile import Empty, Floor, Wall
if TYPE_CHECKING:
from .. import Map
class CorridorGenerator: class CorridorGenerator:
''' '''
Corridor generators produce corridors between rooms. 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.''' '''Generate corridors given a list of rooms.'''
raise NotImplementedError() raise NotImplementedError()
def apply(self, tiles: np.ndarray): def apply(self, map: 'Map'):
'''Apply corridors to a tile grid.''' '''Apply corridors to a tile grid.'''
raise NotImplementedError() raise NotImplementedError()
def _sorted_rooms(self, rooms: List[Room]) -> List[Room]:
return sorted(rooms, key=lambda r: r.bounds.origin)
class ElbowCorridorGenerator(CorridorGenerator): class ElbowCorridorGenerator(CorridorGenerator):
''' '''
@ -48,25 +53,23 @@ class ElbowCorridorGenerator(CorridorGenerator):
''' '''
def __init__(self): 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: if len(rooms) < 2:
return True 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) corridor = self._generate_corridor_between(left_room, right_room)
self.corridors.append(corridor) 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 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 left_room_bounds = left_room.bounds
right_room_bounds = right_room.bounds right_room_bounds = right_room.bounds
@ -77,12 +80,18 @@ class ElbowCorridorGenerator(CorridorGenerator):
end_point = right_room_bounds.midpoint end_point = right_room_bounds.midpoint
# Randomly choose whether to move horizontally then vertically or vice versa # 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) corner = Point(end_point.x, start_point.y)
else: else:
corner = Point(start_point.x, end_point.y) 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('|-> start: %s', left_room_bounds)
log.MAP.debug('`-> end: %s', right_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(): for x, y in tcod.los.bresenham(tuple(corner), tuple(end_point)).tolist():
corridor.append(Point(x, y)) 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 corridor in self.corridors:
for pt in corridor: for pt in corridor:
tiles[pt.x, pt.y] = Floor tiles[pt.x, pt.y] = Floor

View file

@ -23,24 +23,32 @@ class RoomGenerator:
@dataclass @dataclass
class Configuration: 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' rect_method: 'RectMethod'
room_method: 'RoomMethod' room_method: 'RoomMethod'
def __init__(self, *, size: Size, config: Configuration): def __init__(self, config: Configuration):
self.size = size
self.configuration = config self.configuration = config
self.rooms: List[Room] = [] self.rooms: List[Room] = []
self.up_stairs: List[Point] = [] self.up_stairs: List[Point] = []
self.down_stairs: List[Point] = [] self.down_stairs: List[Point] = []
def generate(self): def generate(self, map: 'Map'):
'''Generate rooms and stairs''' '''Generate rooms and stairs'''
rect_method = self.configuration.rect_method rect_method = self.configuration.rect_method
room_method = self.configuration.room_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) room = room_method.room_in_rect(rect)
if not room: if not room:
break break
@ -51,11 +59,10 @@ class RoomGenerator:
self._generate_stairs() self._generate_stairs()
# pylint: disable=redefined-builtin
def apply(self, map: 'Map'): def apply(self, map: 'Map'):
'''Apply the generated rooms to a tile array''' '''Apply the generated rooms to a tile array'''
self._apply(map) self._apply(map)
self._apply_stairs(map.tiles) self._apply_stairs(map)
def _apply(self, map: 'Map'): def _apply(self, map: 'Map'):
''' '''
@ -68,6 +75,8 @@ class RoomGenerator:
''' '''
tiles = map.tiles tiles = map.tiles
map.rooms = self.rooms
for room in self.rooms: for room in self.rooms:
for pt in room.floor_points: for pt in room.floor_points:
tiles[pt.numpy_index] = Floor tiles[pt.numpy_index] = Floor
@ -93,7 +102,12 @@ class RoomGenerator:
self.up_stairs.append(random.choice(list(up_stair_room.walkable_tiles))) self.up_stairs.append(random.choice(list(up_stair_room.walkable_tiles)))
self.down_stairs.append(random.choice(list(down_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: for pt in self.up_stairs:
tiles[pt.numpy_index] = StairsUp tiles[pt.numpy_index] = StairsUp
for pt in self.down_stairs: for pt in self.down_stairs:
@ -103,10 +117,7 @@ class RoomGenerator:
class RectMethod: class RectMethod:
'''An abstract class defining a method for generating rooms.''' '''An abstract class defining a method for generating rooms.'''
def __init__(self, *, size: Size): def generate(self, map: 'Map') -> Iterator[Rect]:
self.size = size
def generate(self) -> Iterator[Rect]:
'''Generate rects to place rooms in until there are no more.''' '''Generate rects to place rooms in until there are no more.'''
raise NotImplementedError() raise NotImplementedError()
@ -132,13 +143,14 @@ class OneBigRoomRectMethod(RectMethod):
width_percentage: float = 0.5 width_percentage: float = 0.5
height_percentage: float = 0.5 height_percentage: float = 0.5
def __init__(self, *, size: Size, config: Optional[Configuration] = None): def __init__(self, config: Optional[Configuration] = None):
super().__init__(size=size) super().__init__()
self.configuration = config or self.__class__.Configuration() self.configuration = config or self.__class__.Configuration()
def generate(self) -> Iterator[Rect]: def generate(self, map: 'Map') -> Iterator[Rect]:
width = self.size.width map_size = map.bounds.size
height = self.size.height width = map_size.width
height = map_size.height
size = Size(math.floor(width * self.configuration.width_percentage), size = Size(math.floor(width * self.configuration.width_percentage),
math.floor(height * self.configuration.height_percentage)) math.floor(height * self.configuration.height_percentage))
@ -156,23 +168,24 @@ class RandomRectMethod(RectMethod):
minimum_room_size: Size = Size(7, 7) minimum_room_size: Size = Size(7, 7)
maximum_room_size: Size = Size(20, 20) maximum_room_size: Size = Size(20, 20)
def __init__(self, *, size: Size, config: Optional[Configuration] = None): def __init__(self, config: Optional[Configuration] = None):
super().__init__(size=size)
self.configuration = config or self.__class__.Configuration() self.configuration = config or self.__class__.Configuration()
self._rects: List[Rect] = [] self._rects: List[Rect] = []
def generate(self) -> Iterator[Rect]: def generate(self, map: 'Map') -> Iterator[Rect]:
minimum_room_size = self.configuration.minimum_room_size minimum_room_size = self.configuration.minimum_room_size
maximum_room_size = self.configuration.maximum_room_size maximum_room_size = self.configuration.maximum_room_size
width_range = (minimum_room_size.width, maximum_room_size.width) width_range = (minimum_room_size.width, maximum_room_size.width)
height_range = (minimum_room_size.height, maximum_room_size.height) height_range = (minimum_room_size.height, maximum_room_size.height)
map_size = map.size
while len(self._rects) < self.configuration.number_of_rooms: while len(self._rects) < self.configuration.number_of_rooms:
for _ in range(self.__class__.NUMBER_OF_ATTEMPTS_PER_RECT): for _ in range(self.__class__.NUMBER_OF_ATTEMPTS_PER_RECT):
size = Size(random.randint(*width_range), random.randint(*height_range)) size = Size(random.randint(*width_range), random.randint(*height_range))
origin = Point(random.randint(0, self.size.width - size.width), origin = Point(random.randint(0, map_size.width - size.width),
random.randint(0, self.size.height - size.height)) random.randint(0, map_size.height - size.height))
candidate_rect = Rect(origin, size) candidate_rect = Rect(origin, size)
overlaps_any_existing_room = any(candidate_rect.intersects(r) for r in self._rects) overlaps_any_existing_room = any(candidate_rect.intersects(r) for r in self._rects)
@ -186,6 +199,10 @@ class RandomRectMethod(RectMethod):
class BSPRectMethod(RectMethod): class BSPRectMethod(RectMethod):
'''
Generate rectangles with Binary Space Partitioning.
'''
@dataclass @dataclass
class Configuration: class Configuration:
''' '''
@ -219,18 +236,19 @@ class BSPRectMethod(RectMethod):
maximum_room_size: Size = Size(20, 20) maximum_room_size: Size = Size(20, 20)
room_size_ratio: Tuple[float, float] = (1.1, 1.1) room_size_ratio: Tuple[float, float] = (1.1, 1.1)
def __init__(self, *, size: Size, config: Optional[Configuration] = None): def __init__(self, config: Optional[Configuration] = None):
super().__init__(size=size)
self.configuration = config or self.__class__.Configuration() self.configuration = config or self.__class__.Configuration()
def generate(self) -> Iterator[Rect]: def generate(self, map: 'Map') -> Iterator[Rect]:
nodes_with_rooms = set() nodes_with_rooms = set()
minimum_room_size = self.configuration.minimum_room_size minimum_room_size = self.configuration.minimum_room_size
maximum_room_size = self.configuration.maximum_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. # 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 # Add 2 to the minimum width and height to account for walls
bsp.split_recursive( bsp.split_recursive(

View file

@ -4,7 +4,7 @@
Implements an abstract Room class, and subclasses that implement it. Rooms are basic components of maps. 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 import numpy as np
@ -118,3 +118,21 @@ class FreeformRoom(Room):
def __str__(self): def __str__(self):
return '\n'.join(''.join(chr(i['light']['ch']) for i in row) for row in self.tiles) 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