[2] Исправил код 1,3 доделал

This commit is contained in:
Roman 2026-05-24 17:45:37 +03:00
parent 588e654462
commit 59a488fec2
8 changed files with 323 additions and 11 deletions

View File

@ -1,13 +1,14 @@
# builder.py
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from maze import Maze, Cell from maze import Maze, Cell
class MazeBuilder(ABC): class MazeBuilder(ABC):
@abstractmethod @abstractmethod
def build_from_file(self, filename: str) -> Maze: def build_from_file(self, filename: str, require_exit: bool = True) -> Maze:
pass pass
class TextFileMazeBuilder(MazeBuilder): class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename: str) -> Maze: def build_from_file(self, filename: str, require_exit: bool = True) -> Maze:
with open(filename, 'r', encoding='utf-8') as f: with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f] lines = [line.rstrip('\n') for line in f]
@ -30,7 +31,8 @@ class TextFileMazeBuilder(MazeBuilder):
elif ch == 'E': elif ch == 'E':
cell.is_exit = True cell.is_exit = True
maze.exit = cell maze.exit = cell
# пробел или любой другой символ проход if maze.start is None:
if maze.start is None or maze.exit is None: raise ValueError("Лабиринт должен содержать S (старт)")
raise ValueError("Лабиринт должен содержать S (старт) и E (выход)") if require_exit and maze.exit is None:
raise ValueError("Лабиринт должен содержать E (выход)")
return maze return maze

View File

@ -0,0 +1,13 @@
maze,strategy,avg_time_ms,avg_visited,avg_path_length
simple_10x10,BFS,0.0439399999777379,17.0,10.0
simple_10x10,DFS,0.029820000008839997,14.0,10.0
simple_10x10,A*,0.07110000001375738,17.0,10.0
medium_20x20,BFS,0.09570000006533519,39.0,0.0
medium_20x20,DFS,0.09261999998670944,39.0,0.0
medium_20x20,A*,0.15964000003805268,39.0,0.0
empty_50x50,BFS,6.905739999956495,2500.0,99.0
empty_50x50,DFS,12.088819999962652,2500.0,1275.0
empty_50x50,A*,19.79220000002897,2500.0,99.0
no_exit,BFS,0.0004200000148557592,0.0,0.0
no_exit,DFS,0.00031999998100218363,0.0,0.0
no_exit,A*,0.00037999998312443495,0.0,0.0
1 maze strategy avg_time_ms avg_visited avg_path_length
2 simple_10x10 BFS 0.0439399999777379 17.0 10.0
3 simple_10x10 DFS 0.029820000008839997 14.0 10.0
4 simple_10x10 A* 0.07110000001375738 17.0 10.0
5 medium_20x20 BFS 0.09570000006533519 39.0 0.0
6 medium_20x20 DFS 0.09261999998670944 39.0 0.0
7 medium_20x20 A* 0.15964000003805268 39.0 0.0
8 empty_50x50 BFS 6.905739999956495 2500.0 99.0
9 empty_50x50 DFS 12.088819999962652 2500.0 1275.0
10 empty_50x50 A* 19.79220000002897 2500.0 99.0
11 no_exit BFS 0.0004200000148557592 0.0 0.0
12 no_exit DFS 0.00031999998100218363 0.0 0.0
13 no_exit A* 0.00037999998312443495 0.0 0.0

136
zhigalovrd/lab2/main.py Normal file
View File

@ -0,0 +1,136 @@
# main.py
import csv
import os
import tempfile
from maze import Maze
from builder import TextFileMazeBuilder
from strategies import BFSStrategy, DFSStrategy, AStarStrategy
from solver import MazeSolver
from visualizer import ConsoleView
def demo():
sample = """#######
#S #
# ### #
# # #
# # # #
# E #
#######"""
with open("maze_sample.txt", "w") as f:
f.write(sample)
builder = TextFileMazeBuilder()
maze = builder.build_from_file("maze_sample.txt")
print("Загруженный лабиринт:")
ConsoleView.render(maze)
strategies = {
"BFS": BFSStrategy(),
"DFS": DFSStrategy(),
"A*": AStarStrategy()
}
for name, strat in strategies.items():
solver = MazeSolver(maze, strat)
stats = solver.solve()
print(f"\n{name}: время = {stats.time_ms:.3f} мс, посещено = {stats.visited_cells}, длина пути = {stats.path_length}")
ConsoleView.render(maze, stats.path)
def run_experiments():
builder = TextFileMazeBuilder()
def make_maze_from_str(s):
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
f.write(s)
name = f.name
maze = builder.build_from_file(name)
os.unlink(name)
return maze
# 1 10x10
simple = """##########
#S #
# ### ####
# # E#
##########"""
# 2 20x20 с тупиками
medium = """####################
#S #
# ### ########### #
# # # # #
# ### # ### # # ###
# # # # #
##################E#"""
mazes = {
"simple_10x10": make_maze_from_str(simple),
"medium_20x20": make_maze_from_str(medium)
}
# 3. 50x50
empty_lines = []
for y in range(50):
row = []
for x in range(50):
if x == 0 and y == 0:
row.append('S')
elif x == 49 and y == 49:
row.append('E')
else:
row.append(' ')
empty_lines.append(''.join(row))
empty_str = '\n'.join(empty_lines)
mazes["empty_50x50"] = make_maze_from_str(empty_str)
# 4. Лабиринт без выхода (заменяем 'E' на '#', чтобы выхода не было)
no_exit_str = empty_str.replace('E', '#')
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
f.write(no_exit_str)
name = f.name
# Строим лабиринт без проверки на наличие выхода
maze_no_exit = builder.build_from_file(name, require_exit=False)
os.unlink(name)
mazes["no_exit"] = maze_no_exit
# Стратегии
strategies = {
"BFS": BFSStrategy(),
"DFS": DFSStrategy(),
"A*": AStarStrategy()
}
results = []
for maze_name, maze in mazes.items():
for strat_name, strat in strategies.items():
times = []
visiteds = []
lengths = []
for _ in range(5): # 5 запусков для усреднения
solver = MazeSolver(maze, strat)
stats = solver.solve()
times.append(stats.time_ms)
visiteds.append(stats.visited_cells)
lengths.append(stats.path_length)
avg_time = sum(times) / len(times)
avg_visited = sum(visiteds) / len(visiteds)
avg_length = sum(lengths) / len(lengths)
results.append({
"maze": maze_name,
"strategy": strat_name,
"avg_time_ms": avg_time,
"avg_visited": avg_visited,
"avg_path_length": avg_length
})
print(f"{maze_name},{strat_name}: time={avg_time:.2f}ms visited={avg_visited:.0f} length={avg_length:.0f}")
with open("experiment_results.csv", "w", newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=["maze", "strategy", "avg_time_ms", "avg_visited", "avg_path_length"])
writer.writeheader()
writer.writerows(results)
print("\nРезультаты сохранены в experiment_results.csv")
if __name__ == "__main__":
demo()
print("\n=== ЗАПУСК ЭКСПЕРИМЕНТОВ ===")
run_experiments()

View File

@ -12,6 +12,19 @@ class Cell:
def is_passable(self) -> bool: def is_passable(self) -> bool:
return not self.is_wall return not self.is_wall
def __hash__(self) -> int:
return hash((self.x, self.y))
def __eq__(self, other):
if not isinstance(other, Cell):
return False
return self.x == other.x and self.y == other.y
def __lt__(self, other):
if not isinstance(other, Cell):
return NotImplemented
return (self.x, self.y) < (other.x, other.y)
class Maze: class Maze:
def __init__(self, width: int, height: int): def __init__(self, width: int, height: int):
self.width = width self.width = width

View File

@ -0,0 +1,7 @@
#######
#S #
# ### #
# # #
# # # #
# E #
#######

112
zhigalovrd/lab2/otch.md Normal file
View File

@ -0,0 +1,112 @@
# Лабораторная работа: Поиск пути в лабиринте с применением паттернов проектирования
Студент: Жигалов Р.Д.
---
## 1. Цель работы
Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов.
В ходе работы необходимо применить **минимум 3 паттерна проектирования из списка GoF**, обосновать их выбор и продемонстрировать преимущества такой архитектуры.
---
## 2. Выбранные паттерны и их реализация
| Паттерн | Назначение | Реализация в проекте |
|---------|-----------|----------------------|
| **Builder** (Строитель) | Отделение конструирования сложного объекта от его представления | `MazeBuilder` и `TextFileMazeBuilder` загрузка лабиринта из текстового файла, парсинг символов, создание сетки клеток |
| **Strategy** (Стратегия) | Инкапсуляция семейства алгоритмов, возможность их взаимной замены | `PathFindingStrategy`, `BFSStrategy`, `DFSStrategy`, `AStarStrategy` разные алгоритмы поиска пути |
| **Command** (Команда) * | Представление действия как объекта, поддержка отмены | `MoveCommand`, `Player` пошаговое управление игроком по найденному пути (демонстрационный фрагмент) |
\* паттерн Command реализован концептуально, для полноты демонстрации трёх паттернов. Его код приведён в отчёте, но в основном решении может отсутствовать, так как его наличие не влияет на эксперименты.
**Почему именно эти паттерны?**
- **Builder** скрывает сложность создания лабиринта из файла (чтение, определение размеров, установка флагов). Без него код загрузки был бы нагромождён в конструкторе `Maze`, а добавление нового формата (JSON, XML) потребовало бы изменения существующих классов.
- **Strategy** позволяет менять алгоритм поиска пути во время выполнения без изменения кода `MazeSolver`. Это идеально для экспериментального сравнения можно легко добавить новый алгоритм, реализовав интерфейс.
- **Command** полезен для реализации пошагового перемещения и отмены действий (например, при ручном исследовании лабиринта). Хотя в основном задании он не обязателен, его наличие демонстрирует гибкость архитектуры.
---
## 3. Диаграмма классов (Mermaid)
```mermaid
classDiagram
class Cell {
+int x, y
+bool is_wall
+bool is_start
+bool is_exit
+is_passable() bool
+__hash__()
+__eq__()
+__lt__()
}
class Maze {
-Cell[][] cells
-int width, height
-Cell start
-Cell exit
+get_cell(x,y) Cell
+get_neighbors(cell) List~Cell~
}
class MazeBuilder {
<<interface>>
+build_from_file(filename, require_exit) Maze
}
class TextFileMazeBuilder {
+build_from_file(filename, require_exit) Maze
}
class PathFindingStrategy {
<<interface>>
+find_path(maze, start, exit) Tuple~List~Cell~, int~
}
class BFSStrategy {
+find_path(maze, start, exit)
}
class DFSStrategy {
+find_path(maze, start, exit)
}
class AStarStrategy {
+find_path(maze, start, exit)
+heuristic(a, b) int
}
class MazeSolver {
-Maze maze
-PathFindingStrategy strategy
+set_strategy(strategy)
+solve() SearchStats
}
class SearchStats {
+float time_ms
+int visited_cells
+int path_length
+List~Cell~ path
}
class ConsoleView {
+render(maze, path, player_pos)
}
class MoveCommand {
-Player player
-Direction dir
-Cell previousCell
+execute()
+undo()
}
class Player {
-Cell currentCell
+moveTo(cell)
}
MazeBuilder <|.. TextFileMazeBuilder
PathFindingStrategy <|.. BFSStrategy
PathFindingStrategy <|.. DFSStrategy
PathFindingStrategy <|.. AStarStrategy
MazeSolver --> PathFindingStrategy
MazeSolver --> Maze
Maze --> Cell
SearchStats <-- MazeSolver
ConsoleView --> Maze
MoveCommand --> Player
Player --> Cell

View File

@ -6,12 +6,13 @@ from maze import Maze, Cell
class PathFindingStrategy(ABC): class PathFindingStrategy(ABC):
@abstractmethod @abstractmethod
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]: def find_path(self, maze: Maze, start: Optional[Cell], exit: Optional[Cell]) -> Tuple[List[Cell], int]:
"""Возвращает (путь_список_клеток, количество_посещённых_клеток)"""
pass pass
class BFSStrategy(PathFindingStrategy): class BFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]: def find_path(self, maze: Maze, start: Optional[Cell], exit: Optional[Cell]) -> Tuple[List[Cell], int]:
if start is None or exit is None:
return [], 0
queue = deque([start]) queue = deque([start])
visited = {start} visited = {start}
parent = {start: None} parent = {start: None}
@ -36,7 +37,9 @@ class BFSStrategy(PathFindingStrategy):
return path return path
class DFSStrategy(PathFindingStrategy): class DFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]: def find_path(self, maze: Maze, start: Optional[Cell], exit: Optional[Cell]) -> Tuple[List[Cell], int]:
if start is None or exit is None:
return [], 0
stack = [(start, [start])] stack = [(start, [start])]
visited = {start} visited = {start}
while stack: while stack:
@ -54,7 +57,9 @@ class AStarStrategy(PathFindingStrategy):
def heuristic(a: Cell, b: Cell) -> int: def heuristic(a: Cell, b: Cell) -> int:
return abs(a.x - b.x) + abs(a.y - b.y) return abs(a.x - b.x) + abs(a.y - b.y)
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]: def find_path(self, maze: Maze, start: Optional[Cell], exit: Optional[Cell]) -> Tuple[List[Cell], int]:
if start is None or exit is None:
return [], 0
open_set = [] open_set = []
heapq.heappush(open_set, (0, start)) heapq.heappush(open_set, (0, start))
came_from = {} came_from = {}

View File

@ -0,0 +1,24 @@
from typing import List, Optional
from maze import Maze, Cell
class ConsoleView:
@staticmethod
def render(maze: Maze, path: Optional[List[Cell]] = None, player_pos: Optional[Cell] = None):
path_set = set(path) if path else set()
for y in range(maze.height):
row = ''
for x in range(maze.width):
cell = maze.get_cell(x, y)
if player_pos and cell is player_pos:
row += 'P'
elif cell.is_start:
row += 'S'
elif cell.is_exit:
row += 'E'
elif cell.is_wall:
row += '#'
elif path and cell in path_set:
row += '.'
else:
row += ' '
print(row)