240 lines
7.5 KiB
Python
240 lines
7.5 KiB
Python
from __future__ import annotations
|
||
|
||
import heapq
|
||
import os
|
||
import random
|
||
|
||
WALL = "M"
|
||
|
||
|
||
def _maze_path() -> str:
|
||
return os.path.join(os.path.dirname(__file__), "maze.txt")
|
||
|
||
|
||
def _read_int(
|
||
prompt: str, *, min_value: int | None = None, max_value: int | None = None
|
||
) -> int:
|
||
while True:
|
||
try:
|
||
s = input(prompt).strip()
|
||
except EOFError:
|
||
raise
|
||
try:
|
||
v = int(s)
|
||
except ValueError:
|
||
print("Ошибка: нужно целое число.")
|
||
continue
|
||
if min_value is not None and v < min_value:
|
||
print(f"Ошибка: число должно быть не меньше {min_value}.")
|
||
continue
|
||
if max_value is not None and v > max_value:
|
||
print(f"Ошибка: число должно быть не больше {max_value}.")
|
||
continue
|
||
return v
|
||
|
||
|
||
def _normalize_grid(rows: list[str], n: int) -> list[str]:
|
||
if len(rows) != n:
|
||
raise ValueError(f"Ожидалось {n} строк, получено {len(rows)}.")
|
||
grid = [(r[:n] + (" " * max(0, n - len(r)))) for r in rows]
|
||
return grid
|
||
|
||
|
||
def read_maze_from_file(n: int) -> list[str]:
|
||
path = _maze_path()
|
||
if not os.path.exists(path):
|
||
raise FileNotFoundError("Файл maze.txt не найден.")
|
||
with open(path, "r", encoding="utf-8") as f:
|
||
rows = [ln.rstrip("\n") for ln in f.readlines()]
|
||
rows = rows[:n]
|
||
return _normalize_grid(rows, n)
|
||
|
||
|
||
def write_maze_to_file(grid: list[str]) -> None:
|
||
path = _maze_path()
|
||
with open(path, "w", encoding="utf-8") as f:
|
||
for r in grid:
|
||
f.write(r + "\n")
|
||
|
||
|
||
def print_maze_with_coords(grid: list[str]) -> None:
|
||
n = len(grid)
|
||
|
||
# Заголовок столбцов
|
||
print(" ", end="")
|
||
for j in range(n):
|
||
print(f"{j + 1:2}", end=" ")
|
||
print()
|
||
|
||
# Лабиринт с номерами строк
|
||
for i, row in enumerate(grid):
|
||
print(f"{i + 1:2} ", end="")
|
||
for cell in row:
|
||
print(f" {cell if cell != ' ' else '.'}", end=" ")
|
||
print()
|
||
|
||
|
||
def _is_exit(grid: list[str], i: int, j: int) -> bool:
|
||
n = len(grid)
|
||
return (i == 0 or j == 0 or i == n - 1 or j == n - 1) and grid[i][j] != WALL
|
||
|
||
|
||
def a_star_shortest_path(grid: list[str], start: tuple[int, int]) -> str:
|
||
n = len(grid)
|
||
sx, sy = start
|
||
if not (0 <= sx < n and 0 <= sy < n):
|
||
raise ValueError("Стартовые координаты вне лабиринта.")
|
||
if grid[sx][sy] == WALL:
|
||
raise ValueError("Стартовая клетка является стеной.")
|
||
|
||
exits = [(i, j) for i in range(n) for j in range(n) if _is_exit(grid, i, j)]
|
||
if not exits:
|
||
raise ValueError("В лабиринте нет выходов.")
|
||
if _is_exit(grid, sx, sy):
|
||
return ""
|
||
|
||
def h(i: int, j: int) -> int:
|
||
return min(abs(i - ei) + abs(j - ej) for ei, ej in exits)
|
||
|
||
inf = 10**9
|
||
gscore = [[inf] * n for _ in range(n)]
|
||
gscore[sx][sy] = 0
|
||
|
||
prev: list[list[tuple[int, int] | None]] = [[None] * n for _ in range(n)]
|
||
prev_move: list[list[str | None]] = [[None] * n for _ in range(n)]
|
||
|
||
heap: list[tuple[int, int, int, int]] = [(h(sx, sy), 0, sx, sy)]
|
||
closed = [[False] * n for _ in range(n)]
|
||
dirs = ((0, -1, "L"), (0, 1, "R"), (-1, 0, "U"), (1, 0, "D"))
|
||
|
||
while heap:
|
||
_, g, i, j = heapq.heappop(heap)
|
||
if closed[i][j]:
|
||
continue
|
||
closed[i][j] = True
|
||
|
||
if _is_exit(grid, i, j):
|
||
path: list[str] = []
|
||
ci, cj = i, j
|
||
while (ci, cj) != (sx, sy):
|
||
mv = prev_move[ci][cj]
|
||
p = prev[ci][cj]
|
||
if mv is None or p is None:
|
||
raise RuntimeError("Ошибка восстановления пути.")
|
||
path.append(mv)
|
||
ci, cj = p
|
||
path.reverse()
|
||
return "".join(path)
|
||
|
||
for di, dj, mv in dirs:
|
||
ni, nj = i + di, j + dj
|
||
if not (0 <= ni < n and 0 <= nj < n):
|
||
continue
|
||
if grid[ni][nj] == WALL:
|
||
continue
|
||
ng = g + 1
|
||
if ng < gscore[ni][nj]:
|
||
gscore[ni][nj] = ng
|
||
prev[ni][nj] = (i, j)
|
||
prev_move[ni][nj] = mv
|
||
heapq.heappush(heap, (ng + h(ni, nj), ng, ni, nj))
|
||
|
||
raise ValueError("Пути к выходу нет.")
|
||
|
||
|
||
def _random_grid(n: int, wall_percent: int) -> list[str]:
|
||
p = wall_percent / 100.0
|
||
rows: list[str] = []
|
||
for i in range(n):
|
||
row_chars: list[str] = []
|
||
for j in range(n):
|
||
if random.random() < p:
|
||
row_chars.append(WALL)
|
||
else:
|
||
row_chars.append(" ")
|
||
rows.append("".join(row_chars))
|
||
return rows
|
||
|
||
|
||
def _random_passage_cell(grid: list[str]) -> tuple[int, int]:
|
||
n = len(grid)
|
||
cells = [(i, j) for i in range(n) for j in range(n) if grid[i][j] != WALL]
|
||
if not cells:
|
||
raise ValueError("В лабиринте нет проходов.")
|
||
return random.choice(cells)
|
||
|
||
|
||
def generate_maze_file_interactive() -> None:
|
||
n = _read_int("Введите N (1..15): ", min_value=1, max_value=15)
|
||
wall_percent = _read_int(
|
||
"Введите процент стен M (0..90): ", min_value=0, max_value=90
|
||
)
|
||
tries = 0
|
||
while True:
|
||
tries += 1
|
||
grid = _random_grid(n, wall_percent)
|
||
for j in range(n):
|
||
if random.random() < 0.25:
|
||
grid[0] = grid[0][:j] + " " + grid[0][j + 1 :]
|
||
start = _random_passage_cell(grid)
|
||
try:
|
||
_ = a_star_shortest_path(grid, start)
|
||
write_maze_to_file(grid)
|
||
print(
|
||
f"Файл maze.txt создан. N={n}. Старт: X={start[0] + 1} Y={start[1] + 1}."
|
||
)
|
||
print_maze_with_coords(grid)
|
||
return
|
||
except Exception:
|
||
if tries >= 500:
|
||
raise ValueError("Не удалось сгенерировать лабиринт.")
|
||
|
||
|
||
def print_maze_file_interactive() -> None:
|
||
n = _read_int("Введите N (1..15): ", min_value=1, max_value=15)
|
||
grid = read_maze_from_file(n)
|
||
print("maze.txt:")
|
||
print_maze_with_coords(grid)
|
||
|
||
|
||
def solve_from_file_interactive() -> None:
|
||
n = _read_int("Введите N (1..15): ", min_value=1, max_value=15)
|
||
grid = read_maze_from_file(n)
|
||
print_maze_with_coords(grid)
|
||
x = _read_int("Введите строку (1..N): ", min_value=1, max_value=n)
|
||
y = _read_int("Введите колонку (1..N): ", min_value=1, max_value=n)
|
||
start = (x - 1, y - 1)
|
||
path = a_star_shortest_path(grid, start)
|
||
print("Путь:", path)
|
||
|
||
|
||
def main() -> None:
|
||
while True:
|
||
print()
|
||
print("Меню:")
|
||
print("1) Создать лабиринт")
|
||
print("2) Показать лабиринт")
|
||
print("3) Найти путь")
|
||
print("0) Выход")
|
||
try:
|
||
choice = input("Выберите пункт: ").strip()
|
||
except EOFError:
|
||
return
|
||
try:
|
||
if choice == "0":
|
||
return
|
||
if choice == "1":
|
||
generate_maze_file_interactive()
|
||
elif choice == "2":
|
||
print_maze_file_interactive()
|
||
elif choice == "3":
|
||
solve_from_file_interactive()
|
||
else:
|
||
print("Неизвестный пункт меню.")
|
||
except Exception as e:
|
||
print(f"Ошибка: {e}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|