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:
		
							parent
							
								
									d4c4b5d879
								
							
						
					
					
						commit
						c59dc1b907
					
				
					 4 changed files with 122 additions and 72 deletions
				
			
		| 
						 | 
				
			
			@ -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')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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})'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue