Skip to content

Commit

Permalink
Add procedural level generation
Browse files Browse the repository at this point in the history
- Implement procedural level generation using Randomized Prim's Algorithm
- Find farthest points in the maze for placing start and end positions
- Add quarks randomly throughout the maze
- Utilize pathfinding library for BFS to find the farthest points
- Fix IndexError in the generate_maze method by adding bounds checks

This commit adds the ability to generate an infinite number of game levels without manual effort.
  • Loading branch information
saifkhichi96 committed Mar 27, 2023
1 parent 3be512d commit 24e7b7e
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 14 deletions.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ altgraph==0.17.3
macholib==1.16.2
numpy==1.24.2
opencv-python==4.7.0.72
pathfinding==1.0.1
pygame==2.3.0
pyinstaller==5.9.0
pyinstaller-hooks-contrib==2023.1
2 changes: 1 addition & 1 deletion src/character/character.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def set_position(self, x, y):

def clamp_position(self, screen):
self.x = max(0, min(screen.get_width() - self.bbox.width, self.x))
self.y = max(0, min(screen.get_height() - self.bbox.height, self.y))
self.y = max(24, min(screen.get_height() - self.bbox.height, self.y))
self.bbox.x, self.bbox.y = self.x, self.y

def update(self, dt):
Expand Down
6 changes: 4 additions & 2 deletions src/levels/level.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@ def load_level(self, level_data):
tile_row = []
for j, tile_value in enumerate(row):
x = j * self.tile_size
y = i * self.tile_size
y = i * self.tile_size + 24 # Add 24 to account for the top bar.

try:
if tile_value in self.quark_interactions.keys():
quark_image = f"quarks/{tile_value}.png"
quark_interaction = self.quark_interactions[tile_value]
quark = Quark(x, y, self.tile_size, self.tile_size, quark_image, quark_interaction)
size = self.tile_size * 2 // 3
offset = (self.tile_size - size) // 2
quark = Quark(x + offset, y + offset, size, size, quark_image, quark_interaction)
self.quarks.append(quark)
tile = None # Set tile to None when it's a quark.
else:
Expand Down
13 changes: 8 additions & 5 deletions src/levels/level_manager.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
from src.data.levels import level1, level2, level3, level4
from src.levels.level import Level
from src.levels.procedural_level import ProceduralLevel


class LevelManager:
def __init__(self, game):
self.game = game
self.levels = [level1, level2, level3, level4]
self.current_level_id = 0
self.NUM_LEVELS = 100
self.current_level_id = 1
self.load_level(self.current_level_id)
self.more_levels = True

def load_level(self, level_id):
WIDTH = 800 // 32
HEIGHT = 600 // 32
self.current_level_id = level_id
self.current_level = Level(self.game.manager.screen, self.levels[level_id])
level_data = str(ProceduralLevel(level_id, WIDTH, HEIGHT)).splitlines()
self.current_level = Level(self.game.manager.screen, level_data)
self.start_x, self.start_y = self.current_level.get_start_position()
self.game.update_level(self)

def next_level(self):
if self.current_level_id < len(self.levels) - 1:
if self.current_level_id < self.NUM_LEVELS:
self.load_level(self.current_level_id + 1)
else:
self.more_levels = False
Expand Down
111 changes: 111 additions & 0 deletions src/levels/procedural_level.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import random
import numpy as np
from pathfinding.core.grid import Grid
from pathfinding.finder.breadth_first import BreadthFirstFinder


class ProceduralLevel:
def __init__(self,id, width, height):
random.seed(id) # Seed the random number generator for reproducibility.
self.id = id
self.width = width
self.height = height
self.grid = self.generate()

def generate_maze(self, width, height):
maze = np.zeros((height, width), dtype=bool)

if width % 2 == 0:
width -= 1
if height % 2 == 0:
height -= 1

maze.fill(False)

frontier = []

start_x, start_y = random.randrange(1, width, 2), random.randrange(1, height, 2)
maze[start_y, start_x] = True
frontier.extend([(start_x + 2, start_y), (start_x - 2, start_y), (start_x, start_y + 2), (start_x, start_y - 2)])

while frontier:
x, y = random.choice(frontier)
frontier.remove((x, y))

neighbors = [(x + 2, y), (x - 2, y), (x, y + 2), (x, y - 2)]
visited_neighbors = [n for n in neighbors if 0 <= n[1] < height and 0 <= n[0] < width and maze[n[1], n[0]]]

if len(visited_neighbors) == 1:
visited_x, visited_y = visited_neighbors[0]

if 0 <= y < height and 0 <= x < width: # Add the bounds check
maze[y, x] = True

if 0 <= (y + visited_y) // 2 < height and 0 <= (x + visited_x) // 2 < width: # Add the bounds check
maze[(y + visited_y) // 2, (x + visited_x) // 2] = True

for nx, ny in neighbors:
if 0 <= ny < height and 0 <= nx < width and not maze[ny, nx] and (nx, ny) not in frontier:
frontier.append((nx, ny))

return maze

def find_farthest_points(self, maze):
grid = Grid(matrix=maze.astype(int))

# Find a random open space in the maze
open_spaces = [(x, y) for y, row in enumerate(maze) for x, cell in enumerate(row) if cell]
start = random.choice(open_spaces)

# Perform BFS search to find the farthest point from the start
farthest_point, max_distance = self.bfs(maze, grid, start)

# Find all points with the max distance
farthest_points = [point for point in open_spaces if self.bfs(maze, grid, start)[1] < max_distance]

# Choose a random farthest point
farthest_point2 = random.choice(farthest_points)

return farthest_point, farthest_point2

def bfs(self, maze, grid, start):
start_node = grid.node(*start)
finder = BreadthFirstFinder()
max_distance = 0
farthest_point = start

for y, row in enumerate(maze):
for x, cell in enumerate(row):
if cell:
end_node = grid.node(x, y)
path, _ = finder.find_path(start_node, end_node, grid)
if len(path) - 1 > max_distance:
max_distance = len(path) - 1
farthest_point = (x, y)

return farthest_point, max_distance

def generate(self):
grid = self.generate_maze(self.width, self.height)
level = [['W' if cell else ' ' for cell in row] for row in grid.tolist()]
level = self.add_start_and_end(level, grid)
level = self.add_obstacles_and_quarks(level)
return level

def add_obstacles_and_quarks(self, level):
for y, row in enumerate(level):
for x, cell in enumerate(row):
if cell == ' ': # Empty space
if random.random() < 0.05: # 5% chance of placing a quark
level[y][x] = 'Q'
return level

def add_start_and_end(self, level_data, occupancy_grid):
start, end = self.find_farthest_points(occupancy_grid)
level_data[start[1]][start[0]] = 'S'
level_data[end[1]][end[0]] = 'E'
self.start, self.end = start, end
return level_data

def __str__(self):
return '\n'.join([''.join(row) for row in self.grid])
14 changes: 8 additions & 6 deletions src/scenes/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,16 @@ def set_over(self, won, reason=""):
class GameUI:
def __init__(self, game):
self.game = game
self.score_label = Label("Score: 0", position=(25, 2), font_size=24, font_color=COLORS['white'],
font_variant="Bold", align="left")
self.timer_label = Label("Time: 0", position=("center", 16), font_size=24, font_color=COLORS['white'],
font_variant="Bold", align="center")
self.level_label = Label("Level: 0", position=(-25, 2), font_size=24, font_color=COLORS['white'],
font_variant="Bold", align="right")
self.top_bar = pygame.Surface((self.game.manager.width, 24))
self.score_label = Label("Score: 0", position=(5, 0), font_size=18, font_color=COLORS['white'],
font_variant="Regular", align="left")
self.timer_label = Label("Time: 0", position=("center", 12), font_size=18, font_color=COLORS['white'],
font_variant="Regular", align="center")
self.level_label = Label("Level: 0", position=(-5, 0), font_size=18, font_color=COLORS['white'],
font_variant="Regular", align="right")

def draw(self, screen):
screen.blit(self.top_bar, (0, 0))
self.draw_level_number(screen)
self.draw_timer(screen)
self.draw_score(screen)
Expand Down

0 comments on commit 24e7b7e

Please sign in to comment.