Break up room and corridor generation into generate and apply phases

- Generate creates rooms and corridors, and apply applies them to a tile grid.
- Add up and down stairs generation to the Room Generators.
- Clean up Room.wall_points and Room.floor_points to make it easier to
  write a generic apply() method on RoomGenerator
This commit is contained in:
Eryn Wells 2023-02-10 21:06:34 -08:00
parent d4c4b5d879
commit c59dc1b907
4 changed files with 122 additions and 72 deletions

View file

@ -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')

View file

@ -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:

View file

@ -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))

View file

@ -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})'