[2] Исправил код 1,3 доделал
This commit is contained in:
parent
588e654462
commit
59a488fec2
|
|
@ -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
|
||||||
13
zhigalovrd/lab2/experiment_results.csv
Normal file
13
zhigalovrd/lab2/experiment_results.csv
Normal 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
|
||||||
|
136
zhigalovrd/lab2/main.py
Normal file
136
zhigalovrd/lab2/main.py
Normal 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()
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
7
zhigalovrd/lab2/maze_sample.txt
Normal file
7
zhigalovrd/lab2/maze_sample.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
#######
|
||||||
|
#S #
|
||||||
|
# ### #
|
||||||
|
# # #
|
||||||
|
# # # #
|
||||||
|
# E #
|
||||||
|
#######
|
||||||
112
zhigalovrd/lab2/otch.md
Normal file
112
zhigalovrd/lab2/otch.md
Normal 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
|
||||||
|
|
@ -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 = {}
|
||||||
|
|
|
||||||
24
zhigalovrd/lab2/visualizer.py
Normal file
24
zhigalovrd/lab2/visualizer.py
Normal 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)
|
||||||
Loading…
Reference in New Issue
Block a user