[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