diff --git a/main.py b/main.py index c3c3d0c..9671b48 100755 --- a/main.py +++ b/main.py @@ -12,7 +12,11 @@ def main() -> None: screen_height = 50 map_width = 80 - map_height = 50 + map_height = 45 + + room_max_size = 10 + room_min_size = 6 + max_rooms = 30 tileset = tcod.tileset.load_tilesheet( "dejavu10x10_gs_tc.png", @@ -27,7 +31,14 @@ def main() -> None: npc = Entity(int(screen_width / 2 - 5), int(screen_height / 2), "@", (255, 255, 0)) entities = {npc, player} - game_map = generate_dungeon(map_width, map_height) + game_map = generate_dungeon( + max_rooms, + room_min_size, + room_max_size, + map_width, + map_height, + player, + ) engine = Engine(entities, event_handler, game_map, player) diff --git a/procgen.py b/procgen.py index 6e1d3d8..e042cd5 100644 --- a/procgen.py +++ b/procgen.py @@ -1,8 +1,16 @@ -from typing import Tuple +from __future__ import annotations + +import random +from typing import Iterator, List, Tuple, TYPE_CHECKING + +import tcod from game_map import GameMap import tile_types +if TYPE_CHECKING: + from entity import Entity + class RectangularRoom: def __init__(self, x: int, y: int, width: int, height: int): @@ -23,14 +31,75 @@ class RectangularRoom: """Return the inner area of this room as a 2D array index.""" return slice(self.x1 + 1, self.x2), slice(self.y1 + 1, self.y2) + def intersects(self, other: RectangularRoom) -> bool: + """Return True if this room overlaps with another RectangularRoom.""" + return ( + self.x1 <= other.x2 + and self.x2 >= other.x1 + and self.y1 <= other.y2 + and self.y2 >= other.y1 + ) -def generate_dungeon(map_width, map_height) -> GameMap: + +def tunnel_between(start: Tuple[int, int], end: Tuple[int, int]) -> Iterator[Tuple[int, int]]: + """Return an L-shaped tunnel between these two points.""" + x1, y1 = start + x2, y2 = end + if random.random() < 0.5: # 50% chance. + # Move horizontally, then vertically + corner_x, corner_y = x2, y1 + else: + # Move vertically, then horizontally + corner_x, corner_y = x1, y2 + + # Generate the coordinates for this tunnel + for x, y in tcod.los.bresenham((x1, y1), (corner_x, corner_y)).tolist(): + yield x, y + + for x, y in tcod.los.bresenham((corner_x, corner_y), (x2, y2)).tolist(): + yield x, y + + +def generate_dungeon( + max_rooms: int, + room_min_size: int, + room_max_size: int, + map_width: int, + map_height: int, + player: Entity, +) -> GameMap: + """Generate a new dungeon map.""" dungeon = GameMap(map_width, map_height) - room_1 = RectangularRoom(x=20, y=15, width=10, height=15) - room_2 = RectangularRoom(x=35, y=15, width=10, height=15) + rooms: List[RectangularRoom] = [] - dungeon.tiles[room_1.inner] = tile_types.floor - dungeon.tiles[room_2.inner] = tile_types.floor + for r in range(max_rooms): + room_width = random.randint(room_min_size, room_max_size) + room_height = random.randint(room_min_size, room_max_size) + + x = random.randint(0, dungeon.width - room_width - 1) + y = random.randint(0, dungeon.height - room_height - 1) + + # "RectangularRoom" class makes rectangles easier to work with + new_room = RectangularRoom(x, y, room_width, room_height) + + # Run through the other rooms and see if they intersect with this one. + # If there are no intersections then the room is valid + if any(new_room.intersects(other_room) for other_room in rooms): + continue # This room intersects, so go to the next attempt. + + # Dig out this room's inner area. + dungeon.tiles[new_room.inner] = tile_types.floor + + if len(rooms) == 0: + # The first room, where the player starts. + player.x, player.y = new_room.center + else: # All rooms after the first. + # Dig out a tunnel between this room and the previous one + for x, y in tunnel_between(rooms[-1].center, new_room.center): + dungeon.tiles[x, y] = tile_types.floor + + # Finally, append the new room to the list. + rooms.append(new_room) return dungeon