Move map generation into a new MapGenerator subclass; add RoomsAndCorridorsGenerator to uses BSP

This commit is contained in:
Eryn Wells 2022-05-01 10:46:30 -07:00
parent a54828c7fb
commit a072ad507e
2 changed files with 72 additions and 46 deletions

View file

@ -28,9 +28,10 @@ class Engine:
map_size = configuration.map_size map_size = configuration.map_size
self.map = Map(map_size) self.map = Map(map_size)
first_room = self.map.rooms[0] first_room = self.map.generator.rooms[0]
player_start_position = first_room.midpoint player_start_position = first_room.center
self.player = Entity('@', position=player_start_position, fg=tcod.white) self.player = Entity('@', position=player_start_position, fg=tcod.white)
self.entities: AbstractSet[Entity] = {self.player} self.entities: AbstractSet[Entity] = {self.player}
for _ in range(self.rng.randint(1, 15)): for _ in range(self.rng.randint(1, 15)):
position = Point(self.rng.randint(0, map_size.width), self.rng.randint(0, map_size.height)) position = Point(self.rng.randint(0, map_size.width), self.rng.randint(0, map_size.height))

View file

@ -6,7 +6,7 @@ import numpy as np
import tcod import tcod
from .geometry import Point, Rect, Size from .geometry import Point, Rect, Size
from .tile import Floor, Wall from .tile import Floor, Wall
from typing import List from typing import List, Optional
LOG = logging.getLogger('map') LOG = logging.getLogger('map')
@ -14,16 +14,8 @@ class Map:
def __init__(self, size: Size): def __init__(self, size: Size):
self.size = size self.size = size
self.tiles = np.full(self.size.as_tuple, fill_value=Floor, order="F") self.generator = RoomsAndCorridorsGenerator(size=size)
self.tiles = self.generator.generate()
self.rng = tcod.random.Random()
# BSP partitions
self.partitions = list(self.generate_partitions())
# Rooms, which are always some small portion of the above partitions.
self.rooms: List[Rect] = self.generate_rooms(self.partitions)
self.update_tiles()
def tile_is_in_bounds(self, point: Point) -> bool: def tile_is_in_bounds(self, point: Point) -> bool:
return 0 <= point.x < self.size.width and 0 <= point.y < self.size.height return 0 <= point.x < self.size.width and 0 <= point.y < self.size.height
@ -31,22 +23,61 @@ class Map:
def tile_is_walkable(self, point: Point) -> bool: def tile_is_walkable(self, point: Point) -> bool:
return self.tiles[point.x, point.y]['walkable'] return self.tiles[point.x, point.y]['walkable']
def generate_partitions(self): def print_to_console(self, console: tcod.Console) -> None:
size = self.size
console.tiles_rgb[0:size.width, 0:size.height] = self.tiles["dark"]
class MapGenerator:
def __init__(self, *, size: Size):
self.size = size
def generate(self) -> np.ndarray:
'''
Generate a tile grid
Subclasses should implement this and fill in their specific map
generation algorithm.
'''
raise NotImplementedError()
class RoomsAndCorridorsGenerator(MapGenerator):
'''Generate a rooms-and-corridors style map with BSP.'''
class Configuration:
def __init__(self, min_room_size: Size):
self.minimum_room_size = min_room_size
DefaultConfiguration = Configuration(
min_room_size=Size(8, 8)
)
def __init__(self, *, size: Size, config: Optional[Configuration] = None):
super().__init__(size=size)
self.configuration = config if config else RoomsAndCorridorsGenerator.DefaultConfiguration
self.rng = tcod.random.Random()
self.rooms: List['RectanularRoom'] = []
self.tiles: Optional[np.ndarray] = None
def generate(self) -> np.ndarray:
if self.tiles:
return self.tiles
minimum_room_size = self.configuration.minimum_room_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=self.size.width, height=self.size.height)
# TODO: Parameterize this. Maybe a MapConfiguration class?
bsp.split_recursive( bsp.split_recursive(
depth=4, depth=4,
min_width=8, min_height=8, min_width=minimum_room_size.width, min_height=minimum_room_size.height,
max_horizontal_ratio=1.5, max_vertical_ratio=1.5) max_horizontal_ratio=1.5, max_vertical_ratio=1.5)
return bsp.pre_order()
def generate_rooms(self, partitions):
rooms = []
# Generate the rooms
rooms: List[RectangularRoom] = []
# For nicer debug logging # For nicer debug logging
indent = 0 indent = 0
for node in bsp.pre_order():
for node in partitions:
if node.children: if node.children:
if LOG.getEffectiveLevel() == logging.DEBUG: if LOG.getEffectiveLevel() == logging.DEBUG:
LOG.debug(f'{" " * indent}{Rect(node.x, node.y, node.width, node.height)}') LOG.debug(f'{" " * indent}{Rect(node.x, node.y, node.width, node.height)}')
@ -54,42 +85,36 @@ class Map:
# TODO: Connect the two child rooms # TODO: Connect the two child rooms
else: else:
LOG.debug(f'{" " * indent}{Rect(node.x, node.y, node.width, node.height)} (room)') LOG.debug(f'{" " * indent}{Rect(node.x, node.y, node.width, node.height)} (room)')
size = Size(self.rng.randint(5, min(15, max(5, node.width - 2))), size = Size(self.rng.randint(5, min(15, max(5, node.width - 2))),
self.rng.randint(5, min(15, max(5, node.height - 2)))) self.rng.randint(5, min(15, max(5, node.height - 2))))
origin = Point(node.x + self.rng.randint(1, max(1, node.width - size.width - 1)), origin = Point(node.x + self.rng.randint(1, max(1, node.width - size.width - 1)),
node.y + self.rng.randint(1, max(1, node.height - size.height - 1))) node.y + self.rng.randint(1, max(1, node.height - size.height - 1)))
room = Rect(origin.x, origin.y, size.width, size.height) bounds = Rect(origin.x, origin.y, size.width, size.height)
LOG.debug(f'{" " * indent}`-> {room}')
LOG.debug(f'{" " * indent}`-> {bounds}')
room = RectangularRoom(bounds)
rooms.append(room) rooms.append(room)
if LOG.getEffectiveLevel() == logging.DEBUG: if LOG.getEffectiveLevel() == logging.DEBUG:
indent -= 2 indent -= 2
return rooms self.rooms = rooms
def update_tiles(self): tiles = np.full(self.size.as_tuple, fill_value=Wall, order='F')
# Fill the whole map with walls for room in rooms:
width, height = self.size.as_tuple bounds = room.bounds
self.tiles[0:width, 0:height] = Wall tiles[bounds.min_x:bounds.max_x, bounds.min_y:bounds.max_y] = Floor
# Dig out rooms self.tiles = tiles
for room in self.rooms:
for y in range(room.min_y, room.max_y + 1):
for x in range(room.min_x, room.max_x + 1):
self.tiles[x, y] = Floor
def print_to_console(self, console: tcod.Console) -> None: return tiles
# for part in self.partitions:
# console.draw_frame(part.x, part.y, part.width, part.height, bg=(40, 40, 80), clear=True, decoration="···· ····")
# for room in self.rooms:
# console.draw_frame(room.origin.x, room.origin.y, room.size.width, room.size.height,
# fg=(255, 255, 255), bg=(80, 40, 40), clear=True)
size = self.size
console.tiles_rgb[0:size.width, 0:size.height] = self.tiles["dark"]
class RectangularRoom: class RectangularRoom:
def __init__(self, bounds: Rect): def __init__(self, bounds: Rect):
self.bounds = bounds self.bounds = bounds
@property
def center(self) -> Point:
return self.bounds.midpoint