157 lines
6.0 KiB
Python
157 lines
6.0 KiB
Python
import numpy as np
|
||
import random
|
||
from collections import deque
|
||
|
||
def generate_maze(rows, cols, generation_algorithm="Recursive Backtracker", seed=None, wall_density=0.3, difficulty=5):
|
||
if seed is not None:
|
||
random.seed(seed)
|
||
np.random.seed(seed)
|
||
else:
|
||
seed = random.randint(0, 999999) # Generate a random seed if not provided
|
||
|
||
# Adjust wall density and complexity based on difficulty
|
||
adjusted_wall_density = wall_density + (difficulty * 0.08) # More severe increase in wall density
|
||
adjusted_wall_density = min(adjusted_wall_density, 0.7) # Cap wall density for solvability
|
||
|
||
maze = None
|
||
is_solved = False
|
||
|
||
# Use a loop to regenerate maze if unsolvable
|
||
while not is_solved:
|
||
if generation_algorithm == "Recursive Backtracker":
|
||
maze = recursive_backtracker_maze(rows, cols, adjusted_wall_density, difficulty)
|
||
elif generation_algorithm == "Prim's":
|
||
maze = prim_maze(rows, cols, adjusted_wall_density, difficulty)
|
||
else:
|
||
raise ValueError(f"Unknown generation algorithm: {generation_algorithm}")
|
||
|
||
# Check if the generated maze is solvable
|
||
is_solved = is_solvable(maze, (1, 1), (rows - 2, cols - 2))
|
||
|
||
return maze, seed # Return the generated maze along with the seed
|
||
|
||
def recursive_backtracker_maze(rows, cols, wall_density, difficulty):
|
||
maze = np.ones((rows, cols), dtype=int)
|
||
|
||
# Initialize the stack with the starting point
|
||
stack = [(1, 1)]
|
||
maze[1][1] = 0 # Start point
|
||
|
||
# Track the main path
|
||
main_path = []
|
||
|
||
# Iterative backtracker logic
|
||
while stack:
|
||
r, c = stack[-1] # Get the current cell from the stack
|
||
directions = [(2, 0), (-2, 0), (0, 2), (0, -2)]
|
||
random.shuffle(directions)
|
||
carved = False
|
||
|
||
for dr, dc in directions:
|
||
nr, nc = r + dr, c + dc
|
||
if 0 < nr < rows-1 and 0 < nc < cols-1 and maze[nr][nc] == 1:
|
||
maze[nr - dr//2][nc - dc//2] = 0 # Remove wall between
|
||
maze[nr][nc] = 0 # Mark the next cell as a passage
|
||
stack.append((nr, nc)) # Add the next cell to the stack
|
||
carved = True
|
||
main_path.append((nr, nc)) # Track the main path
|
||
break # Stop after carving one passage
|
||
|
||
if not carved:
|
||
stack.pop() # Backtrack if no carving was possible
|
||
|
||
# Add complexity based on difficulty - Branch off from the main path
|
||
branch_factor = difficulty * 2 # Scale with difficulty (higher means more branches)
|
||
for _ in range(branch_factor): # Use `_` to indicate we don't need the index
|
||
if len(main_path) > 2: # Only branch if there’s space
|
||
r, c = random.choice(main_path)
|
||
directions = [(2, 0), (-2, 0), (0, 2), (0, -2)]
|
||
random.shuffle(directions)
|
||
for dr, dc in directions:
|
||
nr, nc = r + dr, c + dc
|
||
if 0 < nr < rows-1 and 0 < nc < cols-1 and maze[nr][nc] == 1:
|
||
maze[nr - dr//2][nc - dc//2] = 0 # Remove wall between
|
||
maze[nr][nc] = 0 # Create false branch
|
||
break # Stop after adding one branch
|
||
|
||
# Add random walls based on wall_density (avoid adding walls along the main path)
|
||
for r in range(1, rows-1):
|
||
for c in range(1, cols-1):
|
||
if maze[r][c] == 0 and random.random() < wall_density * 0.05:
|
||
maze[r][c] = 1 # Add wall
|
||
|
||
return maze
|
||
|
||
def prim_maze(rows, cols, wall_density, difficulty):
|
||
maze = np.ones((rows, cols), dtype=int)
|
||
start_r, start_c = 1, 1
|
||
maze[start_r][start_c] = 0
|
||
walls = []
|
||
main_path = [(start_r, start_c)] # Track the main path
|
||
|
||
def add_walls(r, c):
|
||
directions = [(-1,0), (1,0), (0,-1), (0,1)]
|
||
for dr, dc in directions:
|
||
nr, nc = r + dr, c + dc
|
||
if 0 < nr < rows-1 and 0 < nc < cols-1 and maze[nr][nc] == 1:
|
||
walls.append((nr, nc, r, c))
|
||
|
||
add_walls(start_r, start_c)
|
||
|
||
while walls:
|
||
idx = random.randint(0, len(walls)-1)
|
||
wall = walls.pop(idx)
|
||
wr, wc, pr, pc = wall
|
||
opposite_r, opposite_c = wr + (wr - pr), wc + (wc - pc)
|
||
if 0 < opposite_r < rows-1 and 0 < opposite_c < cols-1:
|
||
if maze[opposite_r][opposite_c] == 1:
|
||
maze[wr][wc] = 0
|
||
maze[opposite_r][opposite_c] = 0
|
||
main_path.append((opposite_r, opposite_c)) # Track main path
|
||
add_walls(opposite_r, opposite_c)
|
||
|
||
# Add complexity based on difficulty - More branches from the main path
|
||
branch_factor = difficulty * 2 # More branches with higher difficulty
|
||
for _ in range(branch_factor): # Use `_` to indicate we don't need the index
|
||
if len(main_path) > 2:
|
||
r, c = random.choice(main_path)
|
||
directions = [(-1,0), (1,0), (0,-1), (0,1)]
|
||
random.shuffle(directions)
|
||
for dr, dc in directions:
|
||
nr, nc = r + dr, c + dc
|
||
if 0 < nr < rows-1 and 0 < nc < cols-1 and maze[nr][nc] == 1:
|
||
maze[nr][nc] = 0 # Create a false branch
|
||
break
|
||
|
||
# Add random walls based on wall_density
|
||
for r in range(1, rows-1):
|
||
for c in range(1, cols-1):
|
||
if maze[r][c] == 0 and random.random() < wall_density * 0.05:
|
||
maze[r][c] = 1 # Add wall
|
||
|
||
return maze
|
||
|
||
def is_solvable(maze, start, end):
|
||
queue = deque([start])
|
||
visited = set([start])
|
||
while queue:
|
||
current = queue.popleft()
|
||
if current == end:
|
||
return True
|
||
for neighbor in get_adjacent_cells(current, maze):
|
||
if neighbor not in visited:
|
||
visited.add(neighbor)
|
||
queue.append(neighbor)
|
||
return False
|
||
|
||
# Helper to get adjacent cells
|
||
def get_adjacent_cells(cell, maze):
|
||
row, col = cell
|
||
directions = [(-1,0), (1,0), (0,-1), (0,1)]
|
||
neighbors = []
|
||
for dr, dc in directions:
|
||
r, c = row + dr, col + dc
|
||
if 0 <= r < maze.shape[0] and 0 <= c < maze.shape[1] and maze[r][c] == 0:
|
||
neighbors.append((r, c))
|
||
return neighbors
|