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:
parent
e1523cd9c0
commit
b0d91c9c5d
6 changed files with 111 additions and 62 deletions
|
@ -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())),
|
||||
|
|
|
@ -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'''
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
# 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 .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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue