127 lines
3.3 KiB
Python
127 lines
3.3 KiB
Python
from __future__ import annotations
|
|
|
|
import random
|
|
from collections import deque
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
MAZE_DIR = ROOT / "data" / "mazes"
|
|
|
|
|
|
def main() -> None:
|
|
generate_all()
|
|
|
|
|
|
def generate_all() -> None:
|
|
MAZE_DIR.mkdir(parents=True, exist_ok=True)
|
|
_write("small.txt", _small_maze())
|
|
_write("medium.txt", _perfect_maze(50, 50, seed=2026))
|
|
_write("large.txt", _perfect_maze(100, 100, seed=2027))
|
|
_write("empty.txt", _empty_maze(50, 50))
|
|
_write("no_exit.txt", _no_path_maze(30, 30))
|
|
|
|
|
|
def _write(filename: str, rows: list[str]) -> None:
|
|
(MAZE_DIR / filename).write_text("\n".join(rows) + "\n", encoding="utf-8")
|
|
|
|
|
|
def _small_maze() -> list[str]:
|
|
return [
|
|
"##########",
|
|
"#S #E#",
|
|
"# #### # #",
|
|
"# # # #",
|
|
"# # #### #",
|
|
"# # #",
|
|
"# ###### #",
|
|
"# #",
|
|
"######## #",
|
|
"##########",
|
|
]
|
|
|
|
|
|
def _empty_maze(width: int, height: int) -> list[str]:
|
|
grid = _bordered_grid(width, height, fill=" ")
|
|
grid[1][1] = "S"
|
|
grid[height - 2][width - 2] = "E"
|
|
return _to_rows(grid)
|
|
|
|
|
|
def _no_path_maze(width: int, height: int) -> list[str]:
|
|
grid = [["#" for _ in range(width)] for _ in range(height)]
|
|
grid[1][1] = "S"
|
|
grid[height - 2][width - 2] = "E"
|
|
return _to_rows(grid)
|
|
|
|
|
|
def _perfect_maze(width: int, height: int, seed: int) -> list[str]:
|
|
if width < 5 or height < 5:
|
|
raise ValueError("Maze must be at least 5x5")
|
|
|
|
randomizer = random.Random(seed)
|
|
grid = [["#" for _ in range(width)] for _ in range(height)]
|
|
start = (1, 1)
|
|
stack = [start]
|
|
grid[start[1]][start[0]] = " "
|
|
|
|
while stack:
|
|
x, y = stack[-1]
|
|
candidates = []
|
|
for dx, dy in ((0, -2), (2, 0), (0, 2), (-2, 0)):
|
|
nx, ny = x + dx, y + dy
|
|
if 1 <= nx < width - 1 and 1 <= ny < height - 1 and grid[ny][nx] == "#":
|
|
candidates.append((nx, ny, dx, dy))
|
|
|
|
if not candidates:
|
|
stack.pop()
|
|
continue
|
|
|
|
nx, ny, dx, dy = randomizer.choice(candidates)
|
|
grid[y + dy // 2][x + dx // 2] = " "
|
|
grid[ny][nx] = " "
|
|
stack.append((nx, ny))
|
|
|
|
exit_x, exit_y = _farthest_open_cell(grid, start)
|
|
grid[start[1]][start[0]] = "S"
|
|
grid[exit_y][exit_x] = "E"
|
|
return _to_rows(grid)
|
|
|
|
|
|
def _farthest_open_cell(grid: list[list[str]], start: tuple[int, int]) -> tuple[int, int]:
|
|
queue = deque([start])
|
|
distances = {start: 0}
|
|
farthest = start
|
|
|
|
while queue:
|
|
x, y = queue.popleft()
|
|
if distances[(x, y)] > distances[farthest]:
|
|
farthest = (x, y)
|
|
|
|
for dx, dy in ((0, -1), (1, 0), (0, 1), (-1, 0)):
|
|
nx, ny = x + dx, y + dy
|
|
if (nx, ny) not in distances and grid[ny][nx] != "#":
|
|
distances[(nx, ny)] = distances[(x, y)] + 1
|
|
queue.append((nx, ny))
|
|
|
|
return farthest
|
|
|
|
|
|
def _bordered_grid(width: int, height: int, fill: str) -> list[list[str]]:
|
|
grid = [[fill for _ in range(width)] for _ in range(height)]
|
|
for x in range(width):
|
|
grid[0][x] = "#"
|
|
grid[height - 1][x] = "#"
|
|
for y in range(height):
|
|
grid[y][0] = "#"
|
|
grid[y][width - 1] = "#"
|
|
return grid
|
|
|
|
|
|
def _to_rows(grid: list[list[str]]) -> list[str]:
|
|
return ["".join(row) for row in grid]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|