[2] Исправил код 1,3 доделал
This commit is contained in:
parent
588e654462
commit
59a488fec2
|
|
@ -1,13 +1,14 @@
|
|||
# builder.py
|
||||
from abc import ABC, abstractmethod
|
||||
from maze import Maze, Cell
|
||||
|
||||
class MazeBuilder(ABC):
|
||||
@abstractmethod
|
||||
def build_from_file(self, filename: str) -> Maze:
|
||||
def build_from_file(self, filename: str, require_exit: bool = True) -> Maze:
|
||||
pass
|
||||
|
||||
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:
|
||||
lines = [line.rstrip('\n') for line in f]
|
||||
|
||||
|
|
@ -30,7 +31,8 @@ class TextFileMazeBuilder(MazeBuilder):
|
|||
elif ch == 'E':
|
||||
cell.is_exit = True
|
||||
maze.exit = cell
|
||||
# пробел или любой другой символ – проход
|
||||
if maze.start is None or maze.exit is None:
|
||||
raise ValueError("Лабиринт должен содержать S (старт) и E (выход)")
|
||||
if maze.start is None:
|
||||
raise ValueError("Лабиринт должен содержать S (старт)")
|
||||
if require_exit and maze.exit is None:
|
||||
raise ValueError("Лабиринт должен содержать E (выход)")
|
||||
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:
|
||||
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:
|
||||
def __init__(self, width: int, height: int):
|
||||
self.width = width
|
||||
|
|
@ -27,7 +40,7 @@ class Maze:
|
|||
|
||||
def get_neighbors(self, cell: Cell) -> List[Cell]:
|
||||
neighbors = []
|
||||
for dx, dy in ((0,1), (0,-1), (1,0), (-1,0)):
|
||||
for dx, dy in ((0, 1), (0, -1), (1, 0), (-1, 0)):
|
||||
nx, ny = cell.x + dx, cell.y + dy
|
||||
neighbor = self.get_cell(nx, ny)
|
||||
if neighbor and neighbor.is_passable():
|
||||
|
|
|
|||
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):
|
||||
@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
|
||||
|
||||
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])
|
||||
visited = {start}
|
||||
parent = {start: None}
|
||||
|
|
@ -36,7 +37,9 @@ class BFSStrategy(PathFindingStrategy):
|
|||
return path
|
||||
|
||||
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])]
|
||||
visited = {start}
|
||||
while stack:
|
||||
|
|
@ -54,7 +57,9 @@ class AStarStrategy(PathFindingStrategy):
|
|||
def heuristic(a: Cell, b: Cell) -> int:
|
||||
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 = []
|
||||
heapq.heappush(open_set, (0, start))
|
||||
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