""" This example procedurally develops a random cave based on Binary Space Partitioning (BSP) For more information, see: http://roguebasin.roguelikedevelopment.org/index.php?title=Basic_BSP_Dungeon_generation https://github.com/DanaL/RLDungeonGenerator If Python and Arcade are installed, this example can be run from the command line with: python -m arcade.examples.procedural_caves_bsp """ import random import arcade import timeit import math import os # Sprite scaling. Make this larger, like 0.5 to zoom in and add # 'mystery' to what you can see. Make it smaller, like 0.1 to see # more of the map. WALL_SPRITE_SCALING = 0.25 PLAYER_SPRITE_SCALING = 0.20 WALL_SPRITE_SIZE = 128 * WALL_SPRITE_SCALING # How big the grid is GRID_WIDTH = 100 GRID_HEIGHT = 100 AREA_WIDTH = GRID_WIDTH * WALL_SPRITE_SIZE AREA_HEIGHT = GRID_HEIGHT * WALL_SPRITE_SIZE # How fast the player moves MOVEMENT_SPEED = 5 # How close the player can get to the edge before we scroll. VIEWPORT_MARGIN = 300 # How big the window is WINDOW_WIDTH = 800 WINDOW_HEIGHT = 600 WINDOW_TITLE = "Procedural Caves BSP Example" MERGE_SPRITES = True class Room: def __init__(self, r, c, h, w): self.row = r self.col = c self.height = h self.width = w class RLDungeonGenerator: def __init__(self, w, h): self.MAX = 15 # Cutoff for when we want to stop dividing sections self.width = w self.height = h self.leaves = [] self.dungeon = [] self.rooms = [] for h in range(self.height): row = [] for w in range(self.width): row.append('#') self.dungeon.append(row) def random_split(self, min_row, min_col, max_row, max_col): # We want to keep splitting until the sections get down to the threshold seg_height = max_row - min_row seg_width = max_col - min_col if seg_height < self.MAX and seg_width < self.MAX: self.leaves.append((min_row, min_col, max_row, max_col)) elif seg_height < self.MAX <= seg_width: self.split_on_vertical(min_row, min_col, max_row, max_col) elif seg_height >= self.MAX > seg_width: self.split_on_horizontal(min_row, min_col, max_row, max_col) else: if random.random() < 0.5: self.split_on_horizontal(min_row, min_col, max_row, max_col) else: self.split_on_vertical(min_row, min_col, max_row, max_col) def split_on_horizontal(self, min_row, min_col, max_row, max_col): split = (min_row + max_row) // 2 + random.choice((-2, -1, 0, 1, 2)) self.random_split(min_row, min_col, split, max_col) self.random_split(split + 1, min_col, max_row, max_col) def split_on_vertical(self, min_row, min_col, max_row, max_col): split = (min_col + max_col) // 2 + random.choice((-2, -1, 0, 1, 2)) self.random_split(min_row, min_col, max_row, split) self.random_split(min_row, split + 1, max_row, max_col) def carve_rooms(self): for leaf in self.leaves: # We don't want to fill in every possible room or the # dungeon looks too uniform if random.random() > 0.80: continue section_width = leaf[3] - leaf[1] section_height = leaf[2] - leaf[0] # The actual room's height and width will be 60-100% of the # available section. room_width = round(random.randrange(60, 100) / 100 * section_width) room_height = round(random.randrange(60, 100) / 100 * section_height) # If the room doesn't occupy the entire section we are carving it from, # 'jiggle' it a bit in the square if section_height > room_height: room_start_row = leaf[0] + random.randrange(section_height - room_height) else: room_start_row = leaf[0] if section_width > room_width: room_start_col = leaf[1] + random.randrange(section_width - room_width) else: room_start_col = leaf[1] self.rooms.append(Room(room_start_row, room_start_col, room_height, room_width)) for r in range(room_start_row, room_start_row + room_height): for c in range(room_start_col, room_start_col + room_width): self.dungeon[r][c] = '.' def are_rooms_adjacent(self, room1, room2): adj_rows = [] adj_cols = [] for r in range(room1.row, room1.row + room1.height): if room2.row <= r < room2.row + room2.height: adj_rows.append(r) for c in range(room1.col, room1.col + room1.width): if room2.col <= c < room2.col + room2.width: adj_cols.append(c) return adj_rows, adj_cols def distance_between_rooms(self, room1, room2): centre1 = (room1.row + room1.height // 2, room1.col + room1.width // 2) centre2 = (room2.row + room2.height // 2, room2.col + room2.width // 2) return math.sqrt((centre1[0] - centre2[0]) ** 2 + (centre1[1] - centre2[1]) ** 2) def carve_corridor_between_rooms(self, room1, room2): if room2[2] == 'rows': row = random.choice(room2[1]) # Figure out which room is to the left of the other if room1.col + room1.width < room2[0].col: start_col = room1.col + room1.width end_col = room2[0].col else: start_col = room2[0].col + room2[0].width end_col = room1.col for c in range(start_col, end_col): self.dungeon[row][c] = '.' if end_col - start_col >= 4: self.dungeon[row][start_col] = '+' self.dungeon[row][end_col - 1] = '+' elif start_col == end_col - 1: self.dungeon[row][start_col] = '+' else: col = random.choice(room2[1]) # Figure out which room is above the other if room1.row + room1.height < room2[0].row: start_row = room1.row + room1.height end_row = room2[0].row else: start_row = room2[0].row + room2[0].height end_row = room1.row for r in range(start_row, end_row): self.dungeon[r][col] = '.' if end_row - start_row >= 4: self.dungeon[start_row][col] = '+' self.dungeon[end_row - 1][col] = '+' elif start_row == end_row - 1: self.dungeon[start_row][col] = '+' # Find two nearby rooms that are in difference groups, draw # a corridor between them and merge the groups def find_closest_unconnect_groups(self, groups, room_dict): shortest_distance = 99999 start = None start_group = None nearest = None for group in groups: for room in group: key = (room.row, room.col) for other in room_dict[key]: if not other[0] in group and other[3] < shortest_distance: shortest_distance = other[3] start = room nearest = other start_group = group self.carve_corridor_between_rooms(start, nearest) # Merge the groups other_group = None for group in groups: if nearest[0] in group: other_group = group break start_group += other_group groups.remove(other_group) def connect_rooms(self): # Build a dictionary containing an entry for each room. Each bucket will # hold a list of the adjacent rooms, weather they are adjacent along rows or # columns and the distance between them. # # Also build the initial groups (which start of as a list of individual rooms) groups = [] room_dict = {} for room in self.rooms: key = (room.row, room.col) room_dict[key] = [] for other in self.rooms: other_key = (other.row, other.col) if key == other_key: continue adj = self.are_rooms_adjacent(room, other) if len(adj[0]) > 0: room_dict[key].append((other, adj[0], 'rows', self.distance_between_rooms(room, other))) elif len(adj[1]) > 0: room_dict[key].append((other, adj[1], 'cols', self.distance_between_rooms(room, other))) groups.append([room]) while len(groups) > 1: self.find_closest_unconnect_groups(groups, room_dict) def generate_map(self): self.random_split(1, 1, self.height - 1, self.width - 1) self.carve_rooms() self.connect_rooms() class MyGame(arcade.Window): """ Main application class. """ def __init__(self, width, height, title): super().__init__(width, height, title) # Set the working directory (where we expect to find files) to the same # directory this .py file is in. You can leave this out of your own # code, but it is needed to easily run the examples using "python -m" # as mentioned at the top of this program. file_path = os.path.dirname(os.path.abspath(__file__)) os.chdir(file_path) self.grid = None self.wall_list = None self.player_list = None self.player_sprite = None self.view_bottom = 0 self.view_left = 0 self.physics_engine = None self.processing_time = 0 self.draw_time = 0 arcade.set_background_color(arcade.color.BLACK) def setup(self): self.wall_list = arcade.SpriteList() self.player_list = arcade.SpriteList() # Create cave system using a 2D grid dg = RLDungeonGenerator(GRID_WIDTH, GRID_HEIGHT) dg.generate_map() # Create sprites based on 2D grid if not MERGE_SPRITES: # This is the simple-to-understand method. Each grid location # is a sprite. for row in range(dg.height): for column in range(dg.width): value = dg.dungeon[row][column] if value.sqr == '#': wall = arcade.Sprite("images/grassCenter.png", WALL_SPRITE_SCALING) wall.center_x = column * WALL_SPRITE_SIZE + WALL_SPRITE_SIZE / 2 wall.center_y = row * WALL_SPRITE_SIZE + WALL_SPRITE_SIZE / 2 self.wall_list.append(wall) else: # This uses new Arcade 1.3.1 features, that allow me to create a # larger sprite with a repeating texture. So if there are multiple # cells in a row with a wall, we merge them into one sprite, with a # repeating texture for each cell. This reduces our sprite count. for row in range(dg.height): column = 0 while column < dg.width: while column < dg.width and dg.dungeon[row][column] != '#': column += 1 start_column = column while column < dg.width and dg.dungeon[row][column] == '#': column += 1 end_column = column - 1 column_count = end_column - start_column + 1 column_mid = (start_column + end_column) / 2 wall = arcade.Sprite("images/grassCenter.png", WALL_SPRITE_SCALING, repeat_count_x=column_count) wall.center_x = column_mid * WALL_SPRITE_SIZE + WALL_SPRITE_SIZE / 2 wall.center_y = row * WALL_SPRITE_SIZE + WALL_SPRITE_SIZE / 2 wall.width = WALL_SPRITE_SIZE * column_count self.wall_list.append(wall) # Set up the player self.player_sprite = arcade.Sprite("images/character.png", PLAYER_SPRITE_SCALING) self.player_list.append(self.player_sprite) # Randomly place the player. If we are in a wall, repeat until we aren't. placed = False while not placed: # Randomly position self.player_sprite.center_x = random.randrange(AREA_WIDTH) self.player_sprite.center_y = random.randrange(AREA_HEIGHT) # Are we in a wall? walls_hit = arcade.check_for_collision_with_list(self.player_sprite, self.wall_list) if len(walls_hit) == 0: # Not in a wall! Success! placed = True self.physics_engine = arcade.PhysicsEngineSimple(self.player_sprite, self.wall_list) def on_draw(self): """ Render the screen. """ # Start timing how long this takes draw_start_time = timeit.default_timer() # This command should happen before we start drawing. It will clear # the screen to the background color, and erase what we drew last frame. arcade.start_render() # Draw the sprites self.wall_list.draw() self.player_list.draw() # Draw info on the screen sprite_count = len(self.wall_list) output = f"Sprite Count: {sprite_count}" arcade.draw_text(output, self.view_left + 20, WINDOW_HEIGHT - 20 + self.view_bottom, arcade.color.WHITE, 16) output = f"Drawing time: {self.draw_time:.3f}" arcade.draw_text(output, self.view_left + 20, WINDOW_HEIGHT - 40 + self.view_bottom, arcade.color.WHITE, 16) output = f"Processing time: {self.processing_time:.3f}" arcade.draw_text(output, self.view_left + 20, WINDOW_HEIGHT - 60 + self.view_bottom, arcade.color.WHITE, 16) self.draw_time = timeit.default_timer() - draw_start_time def on_key_press(self, key, modifiers): """Called whenever a key is pressed. """ if key == arcade.key.UP: self.player_sprite.change_y = MOVEMENT_SPEED elif key == arcade.key.DOWN: self.player_sprite.change_y = -MOVEMENT_SPEED elif key == arcade.key.LEFT: self.player_sprite.change_x = -MOVEMENT_SPEED elif key == arcade.key.RIGHT: self.player_sprite.change_x = MOVEMENT_SPEED def on_key_release(self, key, modifiers): """Called when the user releases a key. """ if key == arcade.key.UP or key == arcade.key.DOWN: self.player_sprite.change_y = 0 elif key == arcade.key.LEFT or key == arcade.key.RIGHT: self.player_sprite.change_x = 0 def update(self, delta_time): """ Movement and game logic """ start_time = timeit.default_timer() # Call update on all sprites (The sprites don't do much in this # example though.) self.physics_engine.update() # --- Manage Scrolling --- # Track if we need to change the viewport changed = False # Scroll left left_bndry = self.view_left + VIEWPORT_MARGIN if self.player_sprite.left < left_bndry: self.view_left -= left_bndry - self.player_sprite.left changed = True # Scroll right right_bndry = self.view_left + WINDOW_WIDTH - VIEWPORT_MARGIN if self.player_sprite.right > right_bndry: self.view_left += self.player_sprite.right - right_bndry changed = True # Scroll up top_bndry = self.view_bottom + WINDOW_HEIGHT - VIEWPORT_MARGIN if self.player_sprite.top > top_bndry: self.view_bottom += self.player_sprite.top - top_bndry changed = True # Scroll down bottom_bndry = self.view_bottom + VIEWPORT_MARGIN if self.player_sprite.bottom < bottom_bndry: self.view_bottom -= bottom_bndry - self.player_sprite.bottom changed = True if changed: arcade.set_viewport(self.view_left, WINDOW_WIDTH + self.view_left, self.view_bottom, WINDOW_HEIGHT + self.view_bottom) # Save the time it took to do this. self.processing_time = timeit.default_timer() - start_time def main(): game = MyGame(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE) game.setup() arcade.run() if __name__ == "__main__": main()