Move the BSP implementation to BSPRectMethod
This commit is contained in:
parent
040803fe61
commit
fd068268f5
4 changed files with 118 additions and 95 deletions
|
@ -18,7 +18,8 @@ from .geometry import Point, Size
|
||||||
from .interface import Interface
|
from .interface import Interface
|
||||||
from .map import Map
|
from .map import Map
|
||||||
from .map.generator import RoomsAndCorridorsGenerator
|
from .map.generator import RoomsAndCorridorsGenerator
|
||||||
from .map.generator.room import RoomGenerator, RandomRectMethod, RectangularRoomMethod
|
from .map.generator.cellular_atomata import CellularAtomataMapGenerator
|
||||||
|
from .map.generator.room import BSPRectMethod, CellularAtomatonRoomMethod, OrRoomMethod, RoomGenerator, RandomRectMethod, RectangularRoomMethod
|
||||||
from .map.generator.corridor import ElbowCorridorGenerator
|
from .map.generator.corridor import ElbowCorridorGenerator
|
||||||
from .messages import MessageLog
|
from .messages import MessageLog
|
||||||
from .object import Actor, Entity, Hero, Monster
|
from .object import Actor, Entity, Hero, Monster
|
||||||
|
@ -61,10 +62,16 @@ class Engine:
|
||||||
RoomGenerator(
|
RoomGenerator(
|
||||||
size=map_size,
|
size=map_size,
|
||||||
config=RoomGenerator.Configuration(
|
config=RoomGenerator.Configuration(
|
||||||
rect_method=RandomRectMethod(
|
rect_method=BSPRectMethod(
|
||||||
size=map_size,
|
size=map_size,
|
||||||
config=RandomRectMethod.Configuration(number_of_rooms=4)),
|
config=BSPRectMethod.Configuration(number_of_rooms=30)),
|
||||||
room_method=RectangularRoomMethod())
|
room_method=OrRoomMethod(
|
||||||
|
methods=[
|
||||||
|
(0.2, CellularAtomatonRoomMethod(CellularAtomataMapGenerator.Configuration())),
|
||||||
|
(0.8, RectangularRoomMethod())
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
),
|
),
|
||||||
ElbowCorridorGenerator())
|
ElbowCorridorGenerator())
|
||||||
self.map = Map(config, map_generator)
|
self.map = Map(config, map_generator)
|
||||||
|
|
|
@ -28,6 +28,7 @@ EVENTS = logging.getLogger(_log_name('events'))
|
||||||
UI = logging.getLogger(_log_name('ui'))
|
UI = logging.getLogger(_log_name('ui'))
|
||||||
|
|
||||||
MAP = logging.getLogger(_log_name('map'))
|
MAP = logging.getLogger(_log_name('map'))
|
||||||
|
MAP_BSP = logging.getLogger(_log_name('map', 'bsp'))
|
||||||
MAP_CELL_ATOM = logging.getLogger(_log_name('map', 'cellular'))
|
MAP_CELL_ATOM = logging.getLogger(_log_name('map', 'cellular'))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -185,6 +185,109 @@ class RandomRectMethod(RectMethod):
|
||||||
yield candidate_rect
|
yield candidate_rect
|
||||||
|
|
||||||
|
|
||||||
|
class BSPRectMethod(RectMethod):
|
||||||
|
@dataclass
|
||||||
|
class Configuration:
|
||||||
|
'''
|
||||||
|
Configuration for the binary space partitioning (BSP) Rect method.
|
||||||
|
|
||||||
|
### Attributes
|
||||||
|
|
||||||
|
number_of_rooms : int
|
||||||
|
The maximum number of rooms to produce
|
||||||
|
maximum_room_size : Size
|
||||||
|
The maximum size of any room
|
||||||
|
minimum_room_size : Size
|
||||||
|
The minimum size of any room
|
||||||
|
room_size_ratio : Tuple[float, float]
|
||||||
|
A pair of floats indicating the maximum proportion the sides of a
|
||||||
|
BSP node can have to each other.
|
||||||
|
|
||||||
|
The first value is the horizontal ratio. BSP nodes will never have a
|
||||||
|
horizontal size (width) bigger than `room_size_ratio[0]` times the
|
||||||
|
vertical size.
|
||||||
|
|
||||||
|
The second value is the vertical ratio. BSP nodes will never have a
|
||||||
|
vertical size (height) larger than `room_size_ratio[1]` times the
|
||||||
|
horizontal size.
|
||||||
|
|
||||||
|
The closer these values are to 1.0, the more square the BSP nodes
|
||||||
|
will be.
|
||||||
|
'''
|
||||||
|
number_of_rooms: int = 30
|
||||||
|
minimum_room_size: Size = Size(7, 7)
|
||||||
|
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)
|
||||||
|
self.configuration = config or self.__class__.Configuration()
|
||||||
|
|
||||||
|
def generate(self) -> Iterator[Rect]:
|
||||||
|
nodes_with_rooms = set()
|
||||||
|
|
||||||
|
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=6,
|
||||||
|
min_width=minimum_room_size.width,
|
||||||
|
min_height=minimum_room_size.height,
|
||||||
|
max_horizontal_ratio=self.configuration.room_size_ratio[0],
|
||||||
|
max_vertical_ratio=self.configuration.room_size_ratio[1])
|
||||||
|
|
||||||
|
log.MAP_BSP.info('Generating room rects via BSP')
|
||||||
|
|
||||||
|
# Visit all nodes in a level before visiting any of their children
|
||||||
|
for bsp_node in bsp.level_order():
|
||||||
|
node_width = bsp_node.w
|
||||||
|
node_height = bsp_node.h
|
||||||
|
|
||||||
|
if node_width > maximum_room_size.width or node_height > maximum_room_size.height:
|
||||||
|
log.MAP_BSP.debug('Node with size (%s, %s) exceeds maximum size %s',
|
||||||
|
node_width, node_height, maximum_room_size)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(nodes_with_rooms) >= self.configuration.number_of_rooms:
|
||||||
|
# Made as many rooms as we're allowed. We're done.
|
||||||
|
log.MAP_BSP.debug("Generated enough rooms (more than %d); we're done",
|
||||||
|
self.configuration.number_of_rooms)
|
||||||
|
return
|
||||||
|
|
||||||
|
if any(node in nodes_with_rooms for node in self.__all_parents_of_node(bsp_node)):
|
||||||
|
# Already made a room for one of this node's parents
|
||||||
|
log.MAP_BSP.debug('Already made a room for parent of %s', bsp_node)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
probability_of_room = max(
|
||||||
|
1.0 / (node_width - minimum_room_size.width),
|
||||||
|
1.0 / (node_height - minimum_room_size.height))
|
||||||
|
except ZeroDivisionError:
|
||||||
|
probability_of_room = 1.0
|
||||||
|
|
||||||
|
log.MAP_BSP.info('Probability of generating room for %s: %f', bsp_node, probability_of_room)
|
||||||
|
|
||||||
|
if random.random() <= probability_of_room:
|
||||||
|
log.MAP_BSP.info('Yielding room for node %s', bsp_node)
|
||||||
|
nodes_with_rooms.add(bsp_node)
|
||||||
|
yield self.__rect_from_bsp_node(bsp_node)
|
||||||
|
|
||||||
|
log.MAP_BSP.info('Finished BSP room rect generation, yielded %d rooms', len(nodes_with_rooms))
|
||||||
|
|
||||||
|
def __rect_from_bsp_node(self, bsp_node: tcod.bsp.BSP) -> Rect:
|
||||||
|
return Rect.from_raw_values(bsp_node.x, bsp_node.y, bsp_node.w, bsp_node.h)
|
||||||
|
|
||||||
|
def __all_parents_of_node(self, node: tcod.bsp.BSP | None) -> Iterable[tcod.bsp.BSP]:
|
||||||
|
while node:
|
||||||
|
yield node
|
||||||
|
node = node.parent
|
||||||
|
|
||||||
|
|
||||||
class RoomMethod:
|
class RoomMethod:
|
||||||
'''An abstract class defining a method for generating rooms.'''
|
'''An abstract class defining a method for generating rooms.'''
|
||||||
|
|
||||||
|
@ -292,94 +395,3 @@ class RandomRectRoomGenerator(RoomGenerator):
|
||||||
return True
|
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))
|
|
||||||
|
|
|
@ -115,3 +115,6 @@ class FreeformRoom(Room):
|
||||||
for y, x in np.ndindex(self.tiles.shape):
|
for y, x in np.ndindex(self.tiles.shape):
|
||||||
if self.tiles[y, x]['walkable']:
|
if self.tiles[y, x]['walkable']:
|
||||||
yield Point(x, y) + room_origin_vector
|
yield Point(x, y) + room_origin_vector
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '\n'.join(''.join(chr(i['light']['ch']) for i in row) for row in self.tiles)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue