From 4db9491cec133ec475c845da69528b93b910e04f Mon Sep 17 00:00:00 2001 From: pogodinda Date: Sat, 23 May 2026 21:33:40 +0300 Subject: [PATCH] =?UTF-8?q?[2]=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20?= =?UTF-8?q?=D0=BB=D0=B0=D0=B1=D0=B8=D1=80=D0=B8=D0=BD=D1=82=D0=B0=20=D0=B8?= =?UTF-8?q?=20=D0=BF=D0=B0=D1=82=D1=82=D0=B5=D1=80=D0=BD=20Builder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pogodinda/lab2/data/demo_maze.txt | 10 +++ pogodinda/lab2/src/maze.py | 84 +++++++++++++++++++ pogodinda/lab2/src/maze_builder.py | 121 +++++++++++++++++++++++++++ pogodinda/lab2/tests/test_builder.py | 26 ++++++ pogodinda/lab2/tests/test_maze.py | 19 +++++ 5 files changed, 260 insertions(+) create mode 100644 pogodinda/lab2/data/demo_maze.txt create mode 100644 pogodinda/lab2/src/maze.py create mode 100644 pogodinda/lab2/src/maze_builder.py create mode 100644 pogodinda/lab2/tests/test_builder.py create mode 100644 pogodinda/lab2/tests/test_maze.py diff --git a/pogodinda/lab2/data/demo_maze.txt b/pogodinda/lab2/data/demo_maze.txt new file mode 100644 index 0000000..bf243d3 --- /dev/null +++ b/pogodinda/lab2/data/demo_maze.txt @@ -0,0 +1,10 @@ +############### +#S ## +# ####### # # # +# # # # # # +# # ### # # # +# # ##### # +##### # # +# # ##### # +# ##### E +############### \ No newline at end of file diff --git a/pogodinda/lab2/src/maze.py b/pogodinda/lab2/src/maze.py new file mode 100644 index 0000000..9ac3543 --- /dev/null +++ b/pogodinda/lab2/src/maze.py @@ -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) \ No newline at end of file diff --git a/pogodinda/lab2/src/maze_builder.py b/pogodinda/lab2/src/maze_builder.py new file mode 100644 index 0000000..1745078 --- /dev/null +++ b/pogodinda/lab2/src/maze_builder.py @@ -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 \ No newline at end of file diff --git a/pogodinda/lab2/tests/test_builder.py b/pogodinda/lab2/tests/test_builder.py new file mode 100644 index 0000000..237b45a --- /dev/null +++ b/pogodinda/lab2/tests/test_builder.py @@ -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})") \ No newline at end of file diff --git a/pogodinda/lab2/tests/test_maze.py b/pogodinda/lab2/tests/test_maze.py new file mode 100644 index 0000000..a5ddbfb --- /dev/null +++ b/pogodinda/lab2/tests/test_maze.py @@ -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)) \ No newline at end of file