249 lines
8.3 KiB
Python
249 lines
8.3 KiB
Python
# Eryn Wells <eryn@erynwells.me>
|
|
|
|
import random
|
|
from copy import copy
|
|
from dataclasses import dataclass
|
|
from typing import List, Optional, TYPE_CHECKING
|
|
|
|
import tcod
|
|
|
|
from ... import log
|
|
from ...geometry import Point, Rect, Size
|
|
|
|
if TYPE_CHECKING:
|
|
from .. import Map
|
|
|
|
|
|
class RoomGenerator:
|
|
'''Abstract room generator class.'''
|
|
|
|
@dataclass
|
|
class Configuration:
|
|
number_of_rooms: int
|
|
minimum_room_size: Size
|
|
maximum_room_size: Size
|
|
|
|
DefaultConfiguration = Configuration(
|
|
number_of_rooms=30,
|
|
minimum_room_size=Size(7, 7),
|
|
maximum_room_size=Size(20, 20))
|
|
|
|
def __init__(self, *, size: Size, config: Optional[Configuration] = None):
|
|
self.size = size
|
|
self.configuration = config if config else copy(RoomGenerator.DefaultConfiguration)
|
|
|
|
self.rooms: List[Room] = []
|
|
|
|
self.up_stairs: List[Point] = []
|
|
self.down_stairs: List[Point] = []
|
|
|
|
def generate(self):
|
|
'''Generate rooms and stairs'''
|
|
did_generate_rooms = self._generate()
|
|
|
|
if not did_generate_rooms:
|
|
return
|
|
|
|
self._generate_stairs()
|
|
|
|
def _generate(self) -> bool:
|
|
'''
|
|
Generate a list of rooms.
|
|
|
|
Subclasses should implement this and fill in their specific map
|
|
generation algorithm.
|
|
|
|
Returns
|
|
-------
|
|
np.ndarray
|
|
A two-dimensional array of tiles. Dimensions should match the given size.
|
|
'''
|
|
raise NotImplementedError()
|
|
|
|
# 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)
|
|
|
|
def _apply(self, map: 'Map'):
|
|
'''
|
|
Apply the generated list of rooms to an array of tiles. Subclasses must implement this.
|
|
|
|
Arguments
|
|
---------
|
|
map: Map
|
|
The game map to apply the generated room to
|
|
'''
|
|
tiles = map.tiles
|
|
|
|
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 OneBigRoomGenerator(RoomGenerator):
|
|
'''Generates one big room in the center of the map.'''
|
|
|
|
def _generate(self) -> bool:
|
|
if self.rooms:
|
|
return True
|
|
|
|
origin = Point(self.size.width // 4, self.size.height // 4)
|
|
size = Size(self.size.width // 2, self.size.height // 2)
|
|
room = RectangularRoom(Rect(origin, size))
|
|
|
|
self.rooms.append(room)
|
|
|
|
return True
|
|
|
|
|
|
class RandomRectRoomGenerator(RoomGenerator):
|
|
'''Generate rooms by repeatedly attempting to place rects of random size across the map.'''
|
|
|
|
NUMBER_OF_ATTEMPTS_PER_ROOM = 30
|
|
|
|
def _generate(self) -> bool:
|
|
number_of_attempts = 0
|
|
|
|
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)
|
|
|
|
while len(self.rooms) < self.configuration.number_of_rooms:
|
|
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))
|
|
candidate_room_rect = Rect(origin, size)
|
|
|
|
overlaps_any_existing_room = any(candidate_room_rect.intersects(room.bounds) for room in self.rooms)
|
|
if not overlaps_any_existing_room:
|
|
self.rooms.append(RectangularRoom(candidate_room_rect))
|
|
number_of_attempts = 0
|
|
continue
|
|
|
|
number_of_attempts += 1
|
|
if number_of_attempts > RandomRectRoomGenerator.NUMBER_OF_ATTEMPTS_PER_ROOM:
|
|
break
|
|
|
|
return True
|
|
|
|
|
|
class BSPRoomGenerator(RoomGenerator):
|
|
'''Generate a rooms-and-corridors style map with BSP.'''
|
|
|
|
def __init__(self, *, size: Size, config: Optional[RoomGenerator.Configuration] = None):
|
|
super().__init__(size=size, config=config)
|
|
self.rng: tcod.random.Random = tcod.random.Random()
|
|
|
|
def _generate(self) -> bool:
|
|
if self.rooms:
|
|
return True
|
|
|
|
minimum_room_size = self.configuration.minimum_room_size
|
|
maximum_room_size = self.configuration.maximum_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)
|
|
|
|
# Add 2 to the minimum width and height to account for walls
|
|
bsp.split_recursive(
|
|
depth=4,
|
|
min_width=minimum_room_size.width,
|
|
min_height=minimum_room_size.height,
|
|
max_horizontal_ratio=1.1,
|
|
max_vertical_ratio=1.1)
|
|
|
|
# Generate the rooms
|
|
rooms: List[Room] = []
|
|
|
|
room_attrname = f'{__class__.__name__}.room'
|
|
|
|
for node in bsp.post_order():
|
|
node_bounds = self.__rect_from_bsp_node(node)
|
|
|
|
if node.children:
|
|
continue
|
|
|
|
log.MAP.debug('%s (room) %s', node_bounds, node)
|
|
|
|
# Generate a room size between minimum_room_size and maximum_room_size. The minimum value is
|
|
# straight-forward, but the maximum value needs to be clamped between minimum_room_size and the size of
|
|
# the node.
|
|
width_range = (
|
|
minimum_room_size.width,
|
|
min(maximum_room_size.width, max(
|
|
minimum_room_size.width, node.width - 2))
|
|
)
|
|
height_range = (
|
|
minimum_room_size.height,
|
|
min(maximum_room_size.height, max(
|
|
minimum_room_size.height, node.height - 2))
|
|
)
|
|
|
|
log.MAP.debug('|-> min room size %s', minimum_room_size)
|
|
log.MAP.debug('|-> max room size %s', maximum_room_size)
|
|
log.MAP.debug('|-> node size %s x %s', node.width, node.height)
|
|
log.MAP.debug('|-> width range %s', width_range)
|
|
log.MAP.debug('|-> height range %s', width_range)
|
|
|
|
size = Size(self.rng.randint(*width_range),
|
|
self.rng.randint(*height_range))
|
|
origin = Point(node.x + self.rng.randint(1, max(1, node.width - size.width - 2)),
|
|
node.y + self.rng.randint(1, max(1, node.height - size.height - 2)))
|
|
bounds = Rect(origin, size)
|
|
|
|
log.MAP.debug('`-> %s', bounds)
|
|
|
|
room = RectangularRoom(bounds)
|
|
setattr(node, room_attrname, room)
|
|
rooms.append(room)
|
|
|
|
if not hasattr(node.parent, room_attrname):
|
|
setattr(node.parent, room_attrname, room)
|
|
elif random.random() < 0.5:
|
|
setattr(node.parent, room_attrname, room)
|
|
|
|
# Pass up a random child room so that parent nodes can connect subtrees to each other.
|
|
parent = node.parent
|
|
if parent:
|
|
node_room = getattr(node, room_attrname)
|
|
if not hasattr(node.parent, room_attrname):
|
|
setattr(node.parent, room_attrname, node_room)
|
|
elif random.random() < 0.5:
|
|
setattr(node.parent, room_attrname, node_room)
|
|
|
|
self.rooms = rooms
|
|
|
|
return True
|
|
|
|
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))
|