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