[2] добавлены модель лабиринта и паттерн Builder
This commit is contained in:
parent
1b7ad33278
commit
4db9491cec
10
pogodinda/lab2/data/demo_maze.txt
Normal file
10
pogodinda/lab2/data/demo_maze.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
###############
|
||||
#S ##
|
||||
# ####### # # #
|
||||
# # # # # #
|
||||
# # ### # # #
|
||||
# # ##### #
|
||||
##### # #
|
||||
# # ##### #
|
||||
# ##### E
|
||||
###############
|
||||
84
pogodinda/lab2/src/maze.py
Normal file
84
pogodinda/lab2/src/maze.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
from typing import List, Optional
|
||||
|
||||
|
||||
class Cell:
|
||||
"""Клетка лабиринта."""
|
||||
|
||||
def __init__(self, x: int, y: int, is_wall: bool = False,
|
||||
is_start: bool = False, is_exit: bool = False, weight: int = 1):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.is_wall = is_wall
|
||||
self.is_start = is_start
|
||||
self.is_exit = is_exit
|
||||
self.weight = weight # для взвешенных лабиринтов
|
||||
|
||||
def is_passable(self) -> bool:
|
||||
return not self.is_wall
|
||||
|
||||
def __repr__(self):
|
||||
if self.is_start:
|
||||
return 'S'
|
||||
elif self.is_exit:
|
||||
return 'E'
|
||||
elif self.is_wall:
|
||||
return '#'
|
||||
else:
|
||||
return ' '
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, Cell):
|
||||
return self.x == other.x and self.y == other.y
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.x, self.y))
|
||||
|
||||
|
||||
class Maze:
|
||||
"""Лабиринт — сетка клеток."""
|
||||
|
||||
def __init__(self, width: int, height: int):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.cells: List[List[Cell]] = []
|
||||
self.start: Optional[Cell] = None
|
||||
self.exit: Optional[Cell] = None
|
||||
|
||||
for y in range(height):
|
||||
row = []
|
||||
for x in range(width):
|
||||
row.append(Cell(x, y))
|
||||
self.cells.append(row)
|
||||
|
||||
def get_cell(self, x: int, y: int) -> Optional[Cell]:
|
||||
if 0 <= x < self.width and 0 <= y < self.height:
|
||||
return self.cells[y][x]
|
||||
return None
|
||||
|
||||
def set_cell(self, x: int, y: int, cell: Cell):
|
||||
if 0 <= x < self.width and 0 <= y < self.height:
|
||||
self.cells[y][x] = cell
|
||||
if cell.is_start:
|
||||
self.start = cell
|
||||
if cell.is_exit:
|
||||
self.exit = cell
|
||||
|
||||
def get_neighbors(self, cell: Cell) -> List[Cell]:
|
||||
"""Соседи сверху/снизу/слева/справа, если проходимы."""
|
||||
neighbors = []
|
||||
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
|
||||
|
||||
for dx, dy in directions:
|
||||
nx, ny = cell.x + dx, cell.y + dy
|
||||
neighbor = self.get_cell(nx, ny)
|
||||
if neighbor and neighbor.is_passable():
|
||||
neighbors.append(neighbor)
|
||||
|
||||
return neighbors
|
||||
|
||||
def __repr__(self):
|
||||
lines = []
|
||||
for row in self.cells:
|
||||
lines.append(''.join(str(cell) for cell in row))
|
||||
return '\n'.join(lines)
|
||||
121
pogodinda/lab2/src/maze_builder.py
Normal file
121
pogodinda/lab2/src/maze_builder.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# maze_builder.py
|
||||
from abc import ABC, abstractmethod
|
||||
from maze import Maze, Cell
|
||||
|
||||
|
||||
class MazeBuilder(ABC):
|
||||
"""Интерфейс строителя лабиринта (Builder pattern)."""
|
||||
|
||||
@abstractmethod
|
||||
def build_from_file(self, filename: str) -> Maze:
|
||||
"""Принимает путь к файлу, возвращает готовый Maze."""
|
||||
pass
|
||||
|
||||
|
||||
class TextFileMazeBuilder(MazeBuilder):
|
||||
"""
|
||||
Строитель лабиринта из текстового файла.
|
||||
|
||||
Формат файла:
|
||||
# — стена
|
||||
. или пробел — проход
|
||||
S — старт
|
||||
E — выход
|
||||
"""
|
||||
|
||||
def build_from_file(self, filename: str) -> Maze:
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
lines = [line.rstrip('\n') for line in f.readlines()]
|
||||
|
||||
if not lines:
|
||||
raise ValueError("Файл лабиринта пуст")
|
||||
|
||||
height = len(lines)
|
||||
width = max(len(line) for line in lines)
|
||||
|
||||
maze = Maze(width, height)
|
||||
start_found = False
|
||||
exit_found = False
|
||||
|
||||
for y, line in enumerate(lines):
|
||||
for x, char in enumerate(line):
|
||||
is_wall = (char == '#')
|
||||
is_start = (char == 'S')
|
||||
is_exit = (char == 'E')
|
||||
|
||||
if is_start:
|
||||
start_found = True
|
||||
if is_exit:
|
||||
exit_found = True
|
||||
|
||||
cell = Cell(x, y, is_wall=is_wall, is_start=is_start, is_exit=is_exit)
|
||||
maze.set_cell(x, y, cell)
|
||||
|
||||
if not start_found or not exit_found:
|
||||
raise ValueError("Лабиринт должен содержать старт (S) и выход (E)")
|
||||
|
||||
return maze
|
||||
|
||||
|
||||
class RandomMazeBuilder(MazeBuilder):
|
||||
"""
|
||||
Строитель случайного лабиринта.
|
||||
Алгоритм: рекурсивный бэктрекинг.
|
||||
"""
|
||||
|
||||
def __init__(self, width: int, height: int):
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
def build_from_file(self, filename: str = None) -> Maze:
|
||||
"""
|
||||
filename игнорируется — лабиринт генерируется случайно.
|
||||
Название метода общее для всех Builder'ов.
|
||||
"""
|
||||
import random
|
||||
|
||||
maze = Maze(self.width, self.height)
|
||||
|
||||
# Шаг 1: Заполняем всё стенами
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
maze.set_cell(x, y, Cell(x, y, is_wall=True))
|
||||
|
||||
# Шаг 2: Рекурсивно прокладываем пути
|
||||
visited = set()
|
||||
|
||||
def carve(x, y):
|
||||
"""Прокладывает проход из точки (x, y)."""
|
||||
visited.add((x, y))
|
||||
maze.set_cell(x, y, Cell(x, y, is_wall=False))
|
||||
|
||||
# 4 направления, перемешанные случайно
|
||||
directions = [(0, -2), (0, 2), (-2, 0), (2, 0)]
|
||||
random.shuffle(directions)
|
||||
|
||||
for dx, dy in directions:
|
||||
nx, ny = x + dx, y + dy
|
||||
|
||||
# Проверяем границы и что ещё не посещали
|
||||
if (0 <= nx < self.width and 0 <= ny < self.height
|
||||
and (nx, ny) not in visited):
|
||||
|
||||
# Убираем стену между текущей и новой клеткой
|
||||
wall_x = x + dx // 2
|
||||
wall_y = y + dy // 2
|
||||
maze.set_cell(wall_x, wall_y, Cell(wall_x, wall_y, is_wall=False))
|
||||
|
||||
# Рекурсия в новую клетку
|
||||
carve(nx, ny)
|
||||
|
||||
# Начинаем с (1, 1) — нечётные координаты для коридоров
|
||||
carve(1, 1)
|
||||
|
||||
# Шаг 3: Устанавливаем старт и выход в углах лабиринта
|
||||
maze.set_cell(1, 1, Cell(1, 1, is_wall=False, is_start=True))
|
||||
maze.set_cell(
|
||||
self.width - 2, self.height - 2,
|
||||
Cell(self.width - 2, self.height - 2, is_wall=False, is_exit=True)
|
||||
)
|
||||
|
||||
return maze
|
||||
26
pogodinda/lab2/tests/test_builder.py
Normal file
26
pogodinda/lab2/tests/test_builder.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from src.maze_builder import TextFileMazeBuilder, RandomMazeBuilder
|
||||
# Тест 1: Загрузка из файла
|
||||
print("=" * 50)
|
||||
print("ТЕСТ 1: Загрузка из файла")
|
||||
print("=" * 50)
|
||||
|
||||
builder = TextFileMazeBuilder()
|
||||
maze = builder.build_from_file("data/demo_maze.txt")
|
||||
|
||||
print(maze)
|
||||
print(f"\nРазмер: {maze.width}x{maze.height}")
|
||||
print(f"Старт: ({maze.start.x}, {maze.start.y})")
|
||||
print(f"Выход: ({maze.exit.x}, {maze.exit.y})")
|
||||
|
||||
# Тест 2: Случайный лабиринт
|
||||
print("\n" + "=" * 50)
|
||||
print("ТЕСТ 2: Случайный лабиринт 21x11")
|
||||
print("=" * 50)
|
||||
|
||||
random_builder = RandomMazeBuilder(21, 11)
|
||||
random_maze = random_builder.build_from_file()
|
||||
|
||||
print(random_maze)
|
||||
print(f"\nРазмер: {random_maze.width}x{random_maze.height}")
|
||||
print(f"Старт: ({random_maze.start.x}, {random_maze.start.y})")
|
||||
print(f"Выход: ({random_maze.exit.x}, {random_maze.exit.y})")
|
||||
19
pogodinda/lab2/tests/test_maze.py
Normal file
19
pogodinda/lab2/tests/test_maze.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from src.maze import Maze, Cell
|
||||
|
||||
maze = Maze(5, 5)
|
||||
|
||||
# Заполняем границы стенами
|
||||
for y in range(5):
|
||||
for x in range(5):
|
||||
if x == 0 or x == 4 or y == 0 or y == 4:
|
||||
maze.set_cell(x, y, Cell(x, y, is_wall=True))
|
||||
|
||||
# Старт, выход, одна стена внутри
|
||||
maze.set_cell(1, 1, Cell(1, 1, is_start=True))
|
||||
maze.set_cell(3, 3, Cell(3, 3, is_exit=True))
|
||||
maze.set_cell(2, 2, Cell(2, 2, is_wall=True))
|
||||
|
||||
print(maze)
|
||||
print("Старт:", maze.start)
|
||||
print("Выход:", maze.exit)
|
||||
print("Соседи старта:", maze.get_neighbors(maze.start))
|
||||
Loading…
Reference in New Issue
Block a user