Merge branch 'task-2-oop' into ProninVV
This commit is contained in:
commit
303bcf3eae
41
ProninVV/task-2-oop/AStarStrategy.py
Normal file
41
ProninVV/task-2-oop/AStarStrategy.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
from Maze import Cell, Maze
|
||||||
|
from strategy import PathFindingStrategy
|
||||||
|
|
||||||
|
|
||||||
|
class AStarStrategy(PathFindingStrategy):
|
||||||
|
def findPath(self, maze, start, exit):
|
||||||
|
|
||||||
|
def heuristik(cell):
|
||||||
|
return abs(cell.x - exit.x) + abs(cell.y - exit.y)
|
||||||
|
|
||||||
|
parents = {start: None}
|
||||||
|
|
||||||
|
queue = [start]
|
||||||
|
|
||||||
|
if not start or not exit:
|
||||||
|
return [], 0
|
||||||
|
|
||||||
|
while len(queue) != 0:
|
||||||
|
best_cell = queue[0]
|
||||||
|
for cell in queue:
|
||||||
|
if heuristik(cell) < heuristik(best_cell):
|
||||||
|
best_cell = cell
|
||||||
|
|
||||||
|
u = best_cell
|
||||||
|
queue.remove(u)
|
||||||
|
|
||||||
|
if u == exit:
|
||||||
|
path = []
|
||||||
|
current = exit
|
||||||
|
while current is not None:
|
||||||
|
path.append(current)
|
||||||
|
current = parents[current]
|
||||||
|
path.reverse()
|
||||||
|
return path, len(parents)
|
||||||
|
|
||||||
|
childs = maze.getNeighbors(u)
|
||||||
|
for child in childs:
|
||||||
|
if child not in parents:
|
||||||
|
parents[child] = u
|
||||||
|
queue.append(child)
|
||||||
|
return [], len(parents)
|
||||||
32
ProninVV/task-2-oop/BreadthFirstSearch.py
Normal file
32
ProninVV/task-2-oop/BreadthFirstSearch.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
from strategy import PathFindingStrategy
|
||||||
|
from Maze import Maze, Cell
|
||||||
|
|
||||||
|
|
||||||
|
class BFSStrategy(PathFindingStrategy):
|
||||||
|
def findPath(self, maze: Maze, start: Cell, exit: Cell):
|
||||||
|
|
||||||
|
# очерель: перывй вошел - первый вышел
|
||||||
|
queue = [start]
|
||||||
|
# будем хранить откуда в какую клетку пришли
|
||||||
|
parents = {start: None}
|
||||||
|
|
||||||
|
if not start or not exit:
|
||||||
|
return [], 0
|
||||||
|
|
||||||
|
while (len(queue) != 0):
|
||||||
|
u = queue.pop(0)
|
||||||
|
if u == exit:
|
||||||
|
path = []
|
||||||
|
current = exit
|
||||||
|
while current is not None:
|
||||||
|
path.append(current)
|
||||||
|
current = parents[current]
|
||||||
|
path.reverse()
|
||||||
|
return path, len(parents)
|
||||||
|
|
||||||
|
childs = maze.getNeighbors(u)
|
||||||
|
for child in childs:
|
||||||
|
if child not in parents:
|
||||||
|
parents[child] = u
|
||||||
|
queue.append(child)
|
||||||
|
return [], len(parents)
|
||||||
45
ProninVV/task-2-oop/Command.py
Normal file
45
ProninVV/task-2-oop/Command.py
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class Player:
|
||||||
|
def __init__(self, start_cell):
|
||||||
|
self.current_cell = start_cell
|
||||||
|
|
||||||
|
|
||||||
|
class Command(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def execute(self) -> bool:
|
||||||
|
"""Выполняет действие. Возвращает True, если ход успешен."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def undo(self) -> None:
|
||||||
|
"""Откатывает действие назад."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MoveCommand(Command):
|
||||||
|
def __init__(self, player: Player, maze, dx: int, dy: int):
|
||||||
|
self.player = player
|
||||||
|
self.maze = maze
|
||||||
|
self.dx = dx
|
||||||
|
self.dy = dy
|
||||||
|
self.previous_cell = None
|
||||||
|
|
||||||
|
def execute(self) -> bool:
|
||||||
|
new_x = self.player.current_cell.x + self.dx
|
||||||
|
new_y = self.player.current_cell.y + self.dy
|
||||||
|
|
||||||
|
next_cell = self.maze.getCell(new_x, new_y)
|
||||||
|
|
||||||
|
if next_cell and next_cell.isPassable():
|
||||||
|
self.previous_cell = self.player.current_cell
|
||||||
|
self.player.current_cell = next_cell
|
||||||
|
return True
|
||||||
|
|
||||||
|
print("Ошибка: Там стена или край лабиринта!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def undo(self) -> None:
|
||||||
|
if self.previous_cell:
|
||||||
|
self.player.current_cell = self.previous_cell
|
||||||
40
ProninVV/task-2-oop/ConsoleView.py
Normal file
40
ProninVV/task-2-oop/ConsoleView.py
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
from Observer import Observer, Event
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleView(Observer):
|
||||||
|
def update(self, event: Event) -> None:
|
||||||
|
if event.type == "maze_loaded":
|
||||||
|
print("\n[Система] Лабиринт успешно загружен!")
|
||||||
|
self.render(event.data.get("maze"))
|
||||||
|
|
||||||
|
elif event.type == "path_found":
|
||||||
|
print("\n[Система] Алгоритм нашёл решение!")
|
||||||
|
self.render(event.data.get("maze"), path=event.data.get("path"))
|
||||||
|
|
||||||
|
elif event.type == "move":
|
||||||
|
print(
|
||||||
|
f"\n[Игрок] Переместился в точку: ({event.data.get('player_pos').x}, {event.data.get('player_pos').y})")
|
||||||
|
self.render(event.data.get("maze"),
|
||||||
|
player_position=event.data.get("player_pos"))
|
||||||
|
|
||||||
|
def render(self, maze, player_position=None, path=None) -> None:
|
||||||
|
path_set = set(path) if path else set()
|
||||||
|
|
||||||
|
for y in range(maze.height):
|
||||||
|
row_chars = []
|
||||||
|
for x in range(maze.width):
|
||||||
|
cell = maze.getCell(x, y)
|
||||||
|
|
||||||
|
if player_position and cell == player_position:
|
||||||
|
row_chars.append("P")
|
||||||
|
elif cell.isStart:
|
||||||
|
row_chars.append("S")
|
||||||
|
elif cell.isExit:
|
||||||
|
row_chars.append("E")
|
||||||
|
elif cell in path_set:
|
||||||
|
row_chars.append(".")
|
||||||
|
elif cell.isWall:
|
||||||
|
row_chars.append("#")
|
||||||
|
else:
|
||||||
|
row_chars.append(" ")
|
||||||
|
print("".join(row_chars))
|
||||||
43
ProninVV/task-2-oop/Deikstra.py
Normal file
43
ProninVV/task-2-oop/Deikstra.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from strategy import PathFindingStrategy
|
||||||
|
from Maze import Maze, Cell
|
||||||
|
|
||||||
|
|
||||||
|
class DeikstraFind(PathFindingStrategy):
|
||||||
|
def findPath(self, maze, start, exit):
|
||||||
|
|
||||||
|
if not start or not exit:
|
||||||
|
return [], len(parents)
|
||||||
|
|
||||||
|
queue = [start]
|
||||||
|
|
||||||
|
distances = {start: 0}
|
||||||
|
parents = {start: None}
|
||||||
|
|
||||||
|
while len(queue) != 0:
|
||||||
|
best_cell = queue[0]
|
||||||
|
for cell in queue:
|
||||||
|
if distances[cell] < distances[best_cell]:
|
||||||
|
best_cell = cell
|
||||||
|
|
||||||
|
u = best_cell
|
||||||
|
queue.remove(u)
|
||||||
|
|
||||||
|
if u == exit:
|
||||||
|
path = []
|
||||||
|
current = exit
|
||||||
|
while current is not None:
|
||||||
|
path.append(current)
|
||||||
|
current = parents[current]
|
||||||
|
path.reverse()
|
||||||
|
return path, len(parents)
|
||||||
|
|
||||||
|
for child in maze.getNeighbors(u):
|
||||||
|
distance_through_u = distances[u] + 1
|
||||||
|
|
||||||
|
if distance_through_u < distances.get(child, float('inf')):
|
||||||
|
distances[child] = distance_through_u
|
||||||
|
parents[child] = u
|
||||||
|
if child not in queue:
|
||||||
|
queue.append(child)
|
||||||
|
|
||||||
|
return [], len(parents)
|
||||||
40
ProninVV/task-2-oop/DepthFirstSearch.py
Normal file
40
ProninVV/task-2-oop/DepthFirstSearch.py
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from strategy import PathFindingStrategy
|
||||||
|
from Maze import Maze, Cell
|
||||||
|
|
||||||
|
sys.setrecursionlimit(15000)
|
||||||
|
|
||||||
|
|
||||||
|
class DFSStrategy(PathFindingStrategy):
|
||||||
|
def findPath(self, maze: Maze, start, exit):
|
||||||
|
|
||||||
|
if not start or not exit:
|
||||||
|
return [], 0
|
||||||
|
|
||||||
|
visited = set()
|
||||||
|
path = []
|
||||||
|
|
||||||
|
count_cell = 0
|
||||||
|
|
||||||
|
def dfs(root: Cell) -> bool:
|
||||||
|
visited.add(root)
|
||||||
|
path.append(root)
|
||||||
|
# count_cell += 1
|
||||||
|
|
||||||
|
if root == exit:
|
||||||
|
return True
|
||||||
|
|
||||||
|
neighbors = maze.getNeighbors(root)
|
||||||
|
for neighbor in neighbors:
|
||||||
|
if neighbor not in visited:
|
||||||
|
if dfs(neighbor):
|
||||||
|
return True
|
||||||
|
|
||||||
|
path.pop()
|
||||||
|
return False
|
||||||
|
|
||||||
|
if dfs(start):
|
||||||
|
return path, len(visited)
|
||||||
|
|
||||||
|
return [], len(visited)
|
||||||
49
ProninVV/task-2-oop/Maze.py
Normal file
49
ProninVV/task-2-oop/Maze.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# модель клетки лабиринта
|
||||||
|
|
||||||
|
class Cell:
|
||||||
|
def __init__(self, x, y, isWall=False, isStart=False, isExit=False):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.isWall = isWall
|
||||||
|
self.isStart = isStart
|
||||||
|
self.isExit = isExit
|
||||||
|
|
||||||
|
def isPassable(self):
|
||||||
|
return not self.isWall
|
||||||
|
|
||||||
|
|
||||||
|
# модель лабиринта
|
||||||
|
|
||||||
|
class Maze:
|
||||||
|
|
||||||
|
def __init__(self, height, width, start=None, exit=None):
|
||||||
|
self.height = height # строки
|
||||||
|
self.width = width # столбцы
|
||||||
|
self.__grid = [[Cell(x, y) for x in range(width)]
|
||||||
|
for y in range(height)]
|
||||||
|
self.start = start
|
||||||
|
self.exit = exit
|
||||||
|
|
||||||
|
def getCell(self, x, y) -> Cell:
|
||||||
|
if (0 <= x < self.width) and (0 <= y < self.height):
|
||||||
|
return self.__grid[y][x]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getNeighbors(self, cell):
|
||||||
|
dirs = {'left': (-1, 0), 'right': (1, 0),
|
||||||
|
'up': (0, 1), 'down': (0, -1)}
|
||||||
|
neighbors = []
|
||||||
|
for _, val in dirs.items():
|
||||||
|
dx, dy = val
|
||||||
|
nx, ny = cell.x + dx, cell.y + dy
|
||||||
|
neighbor = self.getCell(nx, ny)
|
||||||
|
|
||||||
|
if neighbor and isinstance(neighbor, Cell) and neighbor.isPassable():
|
||||||
|
neighbors.append(neighbor)
|
||||||
|
return neighbors
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
maze1 = Maze(height=5, width=5, start=0, exit=4)
|
||||||
|
cell1 = maze1.getCell(2, 2)
|
||||||
|
print(maze1.getNeighbors(cell1))
|
||||||
47
ProninVV/task-2-oop/MazeBuilder.py
Normal file
47
ProninVV/task-2-oop/MazeBuilder.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from Maze import Maze, Cell
|
||||||
|
|
||||||
|
|
||||||
|
class MazeBuilder(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def buildFromFile(self, filename):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TextFileMazeBuilder(MazeBuilder):
|
||||||
|
def __init__(self):
|
||||||
|
self._maze = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def maze(self):
|
||||||
|
return self._maze
|
||||||
|
|
||||||
|
def buildFromFile(self, filename: str):
|
||||||
|
|
||||||
|
with open(filename, mode='r', encoding='utf-8') as file:
|
||||||
|
lines = file.read().splitlines()
|
||||||
|
|
||||||
|
height = len(lines)
|
||||||
|
width = len(lines[0])
|
||||||
|
self._maze = Maze(height, width)
|
||||||
|
|
||||||
|
for y, line in enumerate(lines):
|
||||||
|
for x, char in enumerate(line):
|
||||||
|
cell = self._maze.getCell(x, y)
|
||||||
|
|
||||||
|
if char == '#':
|
||||||
|
cell.isWall = True
|
||||||
|
elif char == 'S':
|
||||||
|
cell.isStart = True
|
||||||
|
self._maze.start = cell
|
||||||
|
elif char == 'E':
|
||||||
|
cell.isExit = True
|
||||||
|
self._maze.exit = cell
|
||||||
|
self._validate()
|
||||||
|
return self._maze
|
||||||
|
|
||||||
|
def _validate(self):
|
||||||
|
if self._maze.start is None:
|
||||||
|
raise "в лабиринте нет старта"
|
||||||
|
if self._maze.exit is None:
|
||||||
|
raise "в лабиринте нет начала"
|
||||||
57
ProninVV/task-2-oop/MazeSolver.py
Normal file
57
ProninVV/task-2-oop/MazeSolver.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import time
|
||||||
|
from Maze import Maze
|
||||||
|
from strategy import PathFindingStrategy
|
||||||
|
|
||||||
|
|
||||||
|
class SearchStats:
|
||||||
|
def __init__(self, execution_time, visited_count, path_length, path):
|
||||||
|
self.execution_time = execution_time
|
||||||
|
self.visited_count = visited_count
|
||||||
|
self.path_length = path_length
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return ("f == Статистика поиска == =\n"
|
||||||
|
f"Время выполнения: {self.execution_time_ms:.4f} мс\n"
|
||||||
|
f"Посещено клеток: {self.visited_count}\n"
|
||||||
|
f"Длина пути: {self.path_length} клеток\n")
|
||||||
|
|
||||||
|
|
||||||
|
class MazeSolver:
|
||||||
|
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
|
||||||
|
self._maze = maze
|
||||||
|
self._strategy = strategy
|
||||||
|
self._observers = []
|
||||||
|
|
||||||
|
def addObserver(self, observer):
|
||||||
|
"""Регистрация нового наблюдателя (например, ConsoleView)"""
|
||||||
|
self._observers.append(observer)
|
||||||
|
|
||||||
|
def notify(self, event):
|
||||||
|
"""Уведомление всех подписчиков о событии"""
|
||||||
|
for observer in self._observers:
|
||||||
|
observer.update(event)
|
||||||
|
|
||||||
|
def setStrategy(self, strategy):
|
||||||
|
self._strategy = strategy
|
||||||
|
|
||||||
|
def solve(self):
|
||||||
|
|
||||||
|
if not self._maze or not self._strategy:
|
||||||
|
raise ValueError("Не задан лабиринт или стратегия поиска!")
|
||||||
|
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
|
||||||
|
path, visited_count = self._strategy.findPath(
|
||||||
|
self._maze, self._maze.start, self._maze.exit)
|
||||||
|
|
||||||
|
end_time = time.perf_counter()
|
||||||
|
|
||||||
|
execution_time_ms = (end_time - start_time) * 1000
|
||||||
|
|
||||||
|
path_length = len(path)
|
||||||
|
|
||||||
|
from ConsoleView import Event
|
||||||
|
self.notify(Event("path_found", {"maze": self._maze, "path": path}))
|
||||||
|
|
||||||
|
return SearchStats(execution_time_ms, visited_count, path_length, path)
|
||||||
13
ProninVV/task-2-oop/Observer.py
Normal file
13
ProninVV/task-2-oop/Observer.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class Event:
|
||||||
|
def __init__(self, type: str, data: dict = None):
|
||||||
|
self.type = type # "maze_loaded", "move", "path_found"
|
||||||
|
self.data = data if data else {}
|
||||||
|
|
||||||
|
|
||||||
|
class Observer(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def update(self, event: Event) -> None:
|
||||||
|
pass
|
||||||
8
ProninVV/task-2-oop/main.py
Normal file
8
ProninVV/task-2-oop/main.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from MazeBuilder import TextFileMazeBuilder
|
||||||
|
from BreadthFirstSearch import BFSStrategy
|
||||||
|
from Maze import Maze
|
||||||
|
|
||||||
|
|
||||||
|
maze1 = TextFileMazeBuilder().buildFromFile("text.txt")
|
||||||
|
pathh = BFSStrategy.findPath(maze1, maze1.start, maze1.exit)
|
||||||
|
print(pathh)
|
||||||
2264
ProninVV/task-2-oop/report/cells.eps
Normal file
2264
ProninVV/task-2-oop/report/cells.eps
Normal file
File diff suppressed because it is too large
Load Diff
BIN
ProninVV/task-2-oop/report/document.pdf
Normal file
BIN
ProninVV/task-2-oop/report/document.pdf
Normal file
Binary file not shown.
302
ProninVV/task-2-oop/report/document.tex
Normal file
302
ProninVV/task-2-oop/report/document.tex
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
\input{preambule.tex}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
\thispagestyle{empty}
|
||||||
|
|
||||||
|
\centerline{МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РФ}
|
||||||
|
\centerline{НАЦИОНАЛЬНЫЙ ИССЛЕДОВАТЕЛЬСКИЙ НИЖЕГОРОДСКИЙ}
|
||||||
|
\centerline{ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ ИМ Н. И. ЛОБАЧЕВСКОГО}
|
||||||
|
\centerline{Радиофизический факультет}
|
||||||
|
|
||||||
|
\vfill
|
||||||
|
|
||||||
|
\centerline{\Large{Отчет к лабораторной работе}}
|
||||||
|
\centerline{\large{по Методам программирования}}
|
||||||
|
\centerline{\Large{Поиск выхода из лабиринта }}
|
||||||
|
\centerline{\Large{(объектно-ориентированная реализация с паттернами)}}
|
||||||
|
\vfill
|
||||||
|
|
||||||
|
Студент группы 427 \hfill Пронин Владислав Владимирович
|
||||||
|
|
||||||
|
Преподаватель \hfill Морозов Н. С.
|
||||||
|
|
||||||
|
\vfill
|
||||||
|
|
||||||
|
\centerline{Н. Новгород, 2026}
|
||||||
|
\clearpage
|
||||||
|
|
||||||
|
\newpage
|
||||||
|
|
||||||
|
\tableofcontents
|
||||||
|
|
||||||
|
\newpage
|
||||||
|
|
||||||
|
\section{Цель работы}
|
||||||
|
|
||||||
|
Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры.
|
||||||
|
|
||||||
|
|
||||||
|
\section{Описание задачи и выбранных паттернов}
|
||||||
|
|
||||||
|
|
||||||
|
Используемые Паттерны:
|
||||||
|
\begin{itemize}
|
||||||
|
|
||||||
|
\item Strategy (Стратегия) \textemdash \ это поведенческий паттерн проектирования, который определяет семейство схожих алгоритмов и помещает каждый из них в собственный класс, после чего алгоритмы можно взаимозаменять прямо во время исполнения программы. Выбран, так как в данной лабораторной работе используются несколько алгоритмов, выполняющих одно и то же действие \ \textemdash \ обход графа.
|
||||||
|
|
||||||
|
\item Builder (строитель) \textemdash \ абстрактный класс/интерфейс, который определяет все этапы, необходимые для производства сложного объекта-продукта. Позволяет отделить построение сложного объекта от его представления, создает сложные объекты, используя простые объекты и поэтапный подход. Выбран для изоляции сложного процесса парсинга текстовоо файла.
|
||||||
|
|
||||||
|
\item Observer (Наблюдатель) \ \textemdash \ это поведенческий паттерн проектирования, который создаёт механизм подписки, позволяющий одним объектам следить и реагировать на события, происходящие в других объектах. Выбран для отделения логики приложения от вывода на экран (принцип MVC). Класс ConsoleView подписывается на события GameController и перерисовывает карту только тогда, когда игрок перемещается или путь найден.
|
||||||
|
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
\section{Диаграмма классов}
|
||||||
|
|
||||||
|
\begin{figure}[H]
|
||||||
|
\centering
|
||||||
|
\includegraphics[scale=0.06]{plan.png}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
|
||||||
|
\section{Листиги Классов}
|
||||||
|
\subsection{Maze Solver}
|
||||||
|
|
||||||
|
\begin{lstlisting}
|
||||||
|
import time
|
||||||
|
from Maze import Maze
|
||||||
|
from strategy import PathFindingStrategy
|
||||||
|
|
||||||
|
class MazeSolver:
|
||||||
|
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
|
||||||
|
self._maze = maze
|
||||||
|
self._strategy = strategy
|
||||||
|
self._observers = []
|
||||||
|
|
||||||
|
def addObserver(self, observer):
|
||||||
|
"""Регистрация нового наблюдателя (например, ConsoleView)"""
|
||||||
|
self._observers.append(observer)
|
||||||
|
|
||||||
|
def notify(self, event):
|
||||||
|
"""Уведомление всех подписчиков о событии"""
|
||||||
|
for observer in self._observers:
|
||||||
|
observer.update(event)
|
||||||
|
|
||||||
|
def setStrategy(self, strategy):
|
||||||
|
self._strategy = strategy
|
||||||
|
|
||||||
|
def solve(self):
|
||||||
|
|
||||||
|
if not self._maze or not self._strategy:
|
||||||
|
raise ValueError("Не задан лабиринт или стратегия поиска!")
|
||||||
|
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
|
||||||
|
path, visited_count = self._strategy.findPath(
|
||||||
|
self._maze, self._maze.start, self._maze.exit)
|
||||||
|
|
||||||
|
end_time = time.perf_counter()
|
||||||
|
|
||||||
|
execution_time_ms = (end_time - start_time) * 1000
|
||||||
|
|
||||||
|
path_length = len(path)
|
||||||
|
|
||||||
|
from ConsoleView import Event
|
||||||
|
self.notify(Event("path_found", {"maze": self._maze, "path": path}))
|
||||||
|
|
||||||
|
return SearchStats(execution_time_ms, visited_count, path_length, path)
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsection{Maze Builder}
|
||||||
|
|
||||||
|
\begin{lstlisting}
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from Maze import Maze, Cell
|
||||||
|
|
||||||
|
class MazeBuilder(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def buildFromFile(self, filename):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TextFileMazeBuilder(MazeBuilder):
|
||||||
|
def __init__(self):
|
||||||
|
self._maze = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def maze(self):
|
||||||
|
return self._maze
|
||||||
|
|
||||||
|
def buildFromFile(self, filename: str):
|
||||||
|
|
||||||
|
with open(filename, mode='r', encoding='utf-8') as file:
|
||||||
|
lines = file.read().splitlines()
|
||||||
|
|
||||||
|
height = len(lines)
|
||||||
|
width = len(lines[0])
|
||||||
|
self._maze = Maze(height, width)
|
||||||
|
|
||||||
|
for y, line in enumerate(lines):
|
||||||
|
for x, char in enumerate(line):
|
||||||
|
cell = self._maze.getCell(x, y)
|
||||||
|
|
||||||
|
if char == '#':
|
||||||
|
cell.isWall = True
|
||||||
|
elif char == 'S':
|
||||||
|
cell.isStart = True
|
||||||
|
self._maze.start = cell
|
||||||
|
elif char == 'E':
|
||||||
|
cell.isExit = True
|
||||||
|
self._maze.exit = cell
|
||||||
|
self._validate()
|
||||||
|
return self._maze
|
||||||
|
|
||||||
|
def _validate(self):
|
||||||
|
if self._maze.start is None:
|
||||||
|
raise "в лабиринте нет старта"
|
||||||
|
if self._maze.exit is None:
|
||||||
|
raise "в лабиринте нет начала"
|
||||||
|
|
||||||
|
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\subsection{OBserver}
|
||||||
|
|
||||||
|
\begin{lstlisting}
|
||||||
|
|
||||||
|
|
||||||
|
from Observer import Observer, Event
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleView(Observer):
|
||||||
|
def update(self, event: Event) -> None:
|
||||||
|
if event.type == "maze_loaded":
|
||||||
|
print("\n[Система] Лабиринт успешно загружен!")
|
||||||
|
self.render(event.data.get("maze"))
|
||||||
|
|
||||||
|
elif event.type == "path_found":
|
||||||
|
print("\n[Система] Алгоритм нашёл решение!")
|
||||||
|
self.render(event.data.get("maze"), path=event.data.get("path"))
|
||||||
|
|
||||||
|
elif event.type == "move":
|
||||||
|
print(
|
||||||
|
f"\n[Игрок] Переместился в точку: ({event.data.get('player_pos').x}, {event.data.get('player_pos').y})")
|
||||||
|
self.render(event.data.get("maze"),
|
||||||
|
player_position=event.data.get("player_pos"))
|
||||||
|
|
||||||
|
def render(self, maze, player_position=None, path=None) -> None:
|
||||||
|
path_set = set(path) if path else set()
|
||||||
|
|
||||||
|
for y in range(maze.height):
|
||||||
|
row_chars = []
|
||||||
|
for x in range(maze.width):
|
||||||
|
cell = maze.getCell(x, y)
|
||||||
|
|
||||||
|
if player_position and cell == player_position:
|
||||||
|
row_chars.append("P")
|
||||||
|
elif cell.isStart:
|
||||||
|
row_chars.append("S")
|
||||||
|
elif cell.isExit:
|
||||||
|
row_chars.append("E")
|
||||||
|
elif cell in path_set:
|
||||||
|
row_chars.append(".")
|
||||||
|
elif cell.isWall:
|
||||||
|
row_chars.append("#")
|
||||||
|
else:
|
||||||
|
row_chars.append(" ")
|
||||||
|
print("".join(row_chars))
|
||||||
|
|
||||||
|
\end{lstlisting}
|
||||||
|
|
||||||
|
\section{Результаты}
|
||||||
|
|
||||||
|
Таблицы замеров времени и посещенных клеток:
|
||||||
|
|
||||||
|
\begin{table}[H]
|
||||||
|
\centering
|
||||||
|
\caption{Результаты экспериментального сравнения алгоритмов поиска пути}
|
||||||
|
\label{tab:maze_benchmark}
|
||||||
|
\begin{tabular}{llccc}
|
||||||
|
\toprule
|
||||||
|
\textbf{Лабиринт} & \textbf{Стратегия} & \textbf{Время (мс)} & \textbf{Посещено клеток} & \textbf{Длина пути} \\
|
||||||
|
\midrule
|
||||||
|
\multirow{4}{*}{Маленький (10×10)}
|
||||||
|
& BFS & 0.0516 & 28 & 15 \\
|
||||||
|
& DFS & 0.0275 & 15 & 15 \\
|
||||||
|
& A* & 0.0360 & 16 & 15 \\
|
||||||
|
& Дейкстра & 0.0722 & 28 & 15 \\
|
||||||
|
\midrule
|
||||||
|
\multirow{4}{*}{Пустой (30×30)}
|
||||||
|
& BFS & 1.1863 & 870 & 58 \\
|
||||||
|
& DFS & 1.5568 & 842 & 842 \\
|
||||||
|
& A* & 0.4405 & 113 & 58 \\
|
||||||
|
& Дейкстра & 2.8607 & 870 & 58 \\
|
||||||
|
\midrule
|
||||||
|
\multirow{4}{*}{Без выхода (15×15)}
|
||||||
|
& BFS & 0.2230 & 160 & 0 \\
|
||||||
|
& DFS & 0.2959 & 160 & 0 \\
|
||||||
|
& A* & 0.9378 & 160 & 0 \\
|
||||||
|
& Дейкстра & 0.4148 & 160 & 0 \\
|
||||||
|
\midrule
|
||||||
|
\multirow{4}{*}{Средний (50×50)}
|
||||||
|
& BFS & 3.2247 & 1779 & 95 \\
|
||||||
|
& DFS & 1.6985 & 873 & 873 \\
|
||||||
|
& A* & 0.7348 & 158 & 95 \\
|
||||||
|
& Дейкстра & 6.1264 & 1779 & 95 \\
|
||||||
|
\midrule
|
||||||
|
\multirow{4}{*}{Большой (100×100)}
|
||||||
|
& BFS & 10.1308 & 7320 & 195 \\
|
||||||
|
& DFS & 6.1878 & 3549 & 3549 \\
|
||||||
|
& A* & 2.8441 & 328 & 195 \\
|
||||||
|
& Дейкстра & 35.2250 & 7320 & 195 \\
|
||||||
|
\bottomrule
|
||||||
|
\end{tabular}
|
||||||
|
\end{table}
|
||||||
|
|
||||||
|
|
||||||
|
Графики:
|
||||||
|
|
||||||
|
\begin{figure}[H]
|
||||||
|
\includegraphics[scale=0.6]{time.eps}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
|
||||||
|
\begin{figure}[H]
|
||||||
|
\includegraphics[scale=0.6]{cells.eps}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
\section{Анализ эффективности}
|
||||||
|
|
||||||
|
Так как в нашем лабиринте вес всех ребер равны 1, то Дейкстра выродился в Поиск в ширину. Также Дейкстра несколько медленнее из за дополнительных расчетов на сортировку стоимостей.
|
||||||
|
|
||||||
|
Самым лучшим по скорости стал алгоритм А*. Он в среднем 3-4 раза быстрее поиска в ширину, так как на каждом шаге он выбирает самого оптимального соседа для каждого узла, а поиск в ширину проверяет всех соседей.
|
||||||
|
|
||||||
|
В разработанной рекурсивной стратегии DFS метрика посещенных клеток совпадает с длиной пути, так как алгоритм фиксирует состояние успешно развернутого стека вызовов в момент достижения целевой точки. Все тупиковые ветви, из которых рекурсия вышла до момента нахождения exit, отсекаются архитектурой возврата флага True, что демонстрирует специфику работы рекурсивного бэктрекинга в Python
|
||||||
|
|
||||||
|
|
||||||
|
\section{Выводы по ООП}
|
||||||
|
|
||||||
|
В ходе выполнения лабораторной работы была спроектирована и реализована объектно-ориентированная система поиска пути в лабиринтах. Применение принципов ООП и паттернов проектирования GoF позволило полностью разделить зоны ответственности классов (принцип Single Responsibility) и обеспечить высокий уровень гибкости и расширяемости приложения.
|
||||||
|
|
||||||
|
1. Как паттерны помогли сделать код гибким и расширяемым
|
||||||
|
\begin{itemize}
|
||||||
|
\item Разделение логики построения и представления (Паттерн Builder):
|
||||||
|
Процесс создания лабиринта инкапсулирован внутри класса TextFileMazeBuilder. Сам лабиринт (Maze) и алгоритмы поиска никак не завязаны на формат хранения данных. Если в будущем потребуется сменить текстовый формат .txt на структуру .json достаточно будет создать нового строителя, реализующего интерфейс MazeBuilder.
|
||||||
|
|
||||||
|
\item Изоляция и динамическая смена алгоритмов (Паттерн Strategy):
|
||||||
|
Каждый алгоритм обхода графа вынесен в отдельный класс-стратегию с единым интерфейсом PathfindingStrategy. Класс-оркестратор MazeSolver работает исключительно с абстракцией.
|
||||||
|
|
||||||
|
\item Использование Observer позволило отделить вычислительную составляющую от графической. Maze SOlver никак не учитывает где и как будут отображаться данные, он только отдает сигнал о событиях. Это позволяет если нужно изменить графический инт6ерфейс.
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
\end{document}
|
||||||
BIN
ProninVV/task-2-oop/report/plan.png
Normal file
BIN
ProninVV/task-2-oop/report/plan.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
83
ProninVV/task-2-oop/report/preambule.tex
Normal file
83
ProninVV/task-2-oop/report/preambule.tex
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
%\documentclass[a4paper, 12pt]{article}
|
||||||
|
\documentclass[a4paper, 14pt]{extarticle}
|
||||||
|
|
||||||
|
\usepackage[english, russian]{babel}
|
||||||
|
\usepackage[T2A]{fontenc}
|
||||||
|
\usepackage[utf8]{inputenc}
|
||||||
|
\usepackage{comment}
|
||||||
|
|
||||||
|
|
||||||
|
\usepackage{fontspec}
|
||||||
|
\setmainfont{Times New Roman}
|
||||||
|
|
||||||
|
\usepackage{amsmath}
|
||||||
|
\usepackage{amssymb}
|
||||||
|
|
||||||
|
\usepackage{geometry}
|
||||||
|
\usepackage{titleps}
|
||||||
|
\usepackage{graphicx}
|
||||||
|
\DeclareGraphicsExtensions{.pdf, .jpg}
|
||||||
|
\usepackage{wrapfig}
|
||||||
|
|
||||||
|
|
||||||
|
\usepackage{indentfirst}
|
||||||
|
|
||||||
|
|
||||||
|
\geometry{top=20mm}
|
||||||
|
\geometry{bottom=25mm}
|
||||||
|
\geometry{left=30mm}
|
||||||
|
\geometry{right=10mm}
|
||||||
|
|
||||||
|
\usepackage{float}
|
||||||
|
\usepackage{wrapfig}
|
||||||
|
|
||||||
|
\newpagestyle{main}{
|
||||||
|
\setheadrule{0.4pt}
|
||||||
|
\sethead{ННГУ им Н.И. Лобачесвкого}{}{В. В. Пронин}
|
||||||
|
|
||||||
|
\setfoot{}{\thepage}{}
|
||||||
|
}
|
||||||
|
\pagestyle{main}
|
||||||
|
%\setcounter{page}{2}
|
||||||
|
|
||||||
|
\linespread{1.5}
|
||||||
|
\setlength{\parindent}{10mm}
|
||||||
|
\setlength{\parskip}{1ex}
|
||||||
|
|
||||||
|
|
||||||
|
\usepackage{listings}
|
||||||
|
\usepackage{xcolor}
|
||||||
|
|
||||||
|
% Настройка цветов для аккуратного кода
|
||||||
|
\definecolor{codegreen}{rgb}{0,0.5,0}
|
||||||
|
\definecolor{codegray}{rgb}{0.5,0.5,0.5}
|
||||||
|
\definecolor{codepurple}{rgb}{0.58,0,0.82}
|
||||||
|
\definecolor{backcolour}{rgb}{0.97,0.97,0.96}
|
||||||
|
|
||||||
|
\lstset{
|
||||||
|
backgroundcolor=\color{backcolour},
|
||||||
|
commentstyle=\color{codegreen},
|
||||||
|
keywordstyle=\color{blue}\bfseries,
|
||||||
|
numberstyle=\tiny\color{codegray},
|
||||||
|
stringstyle=\color{codepurple},
|
||||||
|
basicstyle=\ttfamily\small, % Моноширинный аккуратный шрифт
|
||||||
|
breakatwhitespace=false,
|
||||||
|
breaklines=true, % Автоперенос длинных строк
|
||||||
|
captionpos=b, % Подпись снизу
|
||||||
|
keepspaces=true,
|
||||||
|
numbers=left, % Нумерация строк слева
|
||||||
|
numbersep=8pt,
|
||||||
|
showspaces=false,
|
||||||
|
showstringspaces=false,
|
||||||
|
showtabs=false,
|
||||||
|
tabsize=4,
|
||||||
|
language=Python,
|
||||||
|
frame=single, % Тонкая рамка вокруг кода
|
||||||
|
rulecolor=\color{lightgray}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
\usepackage{booktabs} % Для красивых горизонтальных линий
|
||||||
|
\usepackage{multirow} % Для объединения строк по вертикали
|
||||||
|
\usepackage{float} % Для точного позиционирования таблицы [H]
|
||||||
|
|
||||||
1947
ProninVV/task-2-oop/report/time.eps
Normal file
1947
ProninVV/task-2-oop/report/time.eps
Normal file
File diff suppressed because it is too large
Load Diff
12
ProninVV/task-2-oop/strategy.py
Normal file
12
ProninVV/task-2-oop/strategy.py
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import List
|
||||||
|
from Maze import Maze, Cell
|
||||||
|
|
||||||
|
# интерфейс стратегий
|
||||||
|
|
||||||
|
|
||||||
|
class PathFindingStrategy(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def findPath(maze: Maze, start, exit) -> List[Cell]:
|
||||||
|
""" возвращает список клеток пути (от старта до выхода включительно) или пустой список, если пути нет """
|
||||||
|
pass
|
||||||
134
ProninVV/task-2-oop/test.py
Normal file
134
ProninVV/task-2-oop/test.py
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
import csv
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from MazeBuilder import TextFileMazeBuilder
|
||||||
|
from MazeSolver import MazeSolver, SearchStats
|
||||||
|
from DepthFirstSearch import DFSStrategy
|
||||||
|
from BreadthFirstSearch import BFSStrategy
|
||||||
|
from Deikstra import DeikstraFind
|
||||||
|
from AStarStrategy import AStarStrategy
|
||||||
|
from ConsoleView import ConsoleView
|
||||||
|
|
||||||
|
|
||||||
|
def run_benchmarks():
|
||||||
|
|
||||||
|
files = ["mazes/maze_small.txt", "mazes/maze_empty.txt",
|
||||||
|
"mazes/maze_no_exit.txt", "mazes/maze_medium.txt", "mazes/maze_large.txt"]
|
||||||
|
strategies = {
|
||||||
|
"BFS": BFSStrategy(),
|
||||||
|
"DFS": DFSStrategy(),
|
||||||
|
"A*": AStarStrategy(),
|
||||||
|
"Deikstra": DeikstraFind()
|
||||||
|
}
|
||||||
|
|
||||||
|
view = ConsoleView()
|
||||||
|
|
||||||
|
NUM_RUNS = 5
|
||||||
|
results = []
|
||||||
|
|
||||||
|
print("Запуск экспериментов...")
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
if not os.path.exists(file):
|
||||||
|
print(f"Файл {file} не найден. Пропуск.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
for name, strategy in strategies.items():
|
||||||
|
total_time = 0.0
|
||||||
|
visited_counts = []
|
||||||
|
path_lengths = []
|
||||||
|
|
||||||
|
print(f" работает {name}")
|
||||||
|
for _ in range(NUM_RUNS):
|
||||||
|
# Пересоздаем лабиринт
|
||||||
|
builder = TextFileMazeBuilder()
|
||||||
|
builder.buildFromFile(file)
|
||||||
|
maze = builder.maze
|
||||||
|
|
||||||
|
solver = MazeSolver(maze, strategy)
|
||||||
|
|
||||||
|
solver.addObserver(view)
|
||||||
|
|
||||||
|
stats = solver.solve()
|
||||||
|
|
||||||
|
total_time += stats.execution_time
|
||||||
|
visited_counts.append(stats.visited_count)
|
||||||
|
path_lengths.append(stats.path_length)
|
||||||
|
|
||||||
|
# средние значения
|
||||||
|
avg_time = total_time / NUM_RUNS
|
||||||
|
avg_visited = int(np.mean(visited_counts))
|
||||||
|
avg_path = int(np.mean(path_lengths))
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"лабиринт": file,
|
||||||
|
"стратегия": name,
|
||||||
|
"время_мс": round(avg_time, 4),
|
||||||
|
"посещено_клеток": avg_visited,
|
||||||
|
"длина_пути": avg_path
|
||||||
|
})
|
||||||
|
|
||||||
|
# Запись в CSV
|
||||||
|
csv_file = "results/maze_benchmark_results.csv"
|
||||||
|
with open(csv_file, mode="w", encoding="utf-8", newline="") as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=[
|
||||||
|
"лабиринт", "стратегия", "время_мс", "посещено_клеток", "длина_пути"])
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(results)
|
||||||
|
|
||||||
|
print(f"Результаты успешно сохранены в {csv_file}")
|
||||||
|
return results
|
||||||
|
|
||||||
|
# Построение графиков
|
||||||
|
|
||||||
|
|
||||||
|
def plot_results(results):
|
||||||
|
print("Генерация графиков...")
|
||||||
|
mazes = sorted(list(set(r["лабиринт"] for r in results)))
|
||||||
|
strategies = ["BFS", "DFS", "A*"]
|
||||||
|
|
||||||
|
# График Количество посещенных клеток
|
||||||
|
fig, ax = plt.subplots(figsize=(10, 6))
|
||||||
|
x = np.arange(len(mazes))
|
||||||
|
width = 0.25
|
||||||
|
|
||||||
|
for i, strat in enumerate(strategies):
|
||||||
|
visited = [next(r["посещено_клеток"] for r in results if r["лабиринт"]
|
||||||
|
== m and r["стратегия"] == strat) for m in mazes]
|
||||||
|
ax.bar(x + i*width, visited, width, label=strat)
|
||||||
|
|
||||||
|
ax.set_ylabel('Количество посещенных клеток')
|
||||||
|
ax.set_title('Сравнение эффективности обхода лабиринтов (меньше = лучше)')
|
||||||
|
ax.set_xticks(x + width)
|
||||||
|
ax.set_xticklabels(mazes, rotation=15)
|
||||||
|
ax.legend()
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig("results/benchmark_visited_cells.png", dpi=200)
|
||||||
|
plt.savefig("results/benchmark_visited_cells.eps", dpi=200)
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
# График Время выполнения
|
||||||
|
fig, ax = plt.subplots(figsize=(10, 6))
|
||||||
|
for i, strat in enumerate(strategies):
|
||||||
|
times = [next(r["время_мс"] for r in results if r["лабиринт"]
|
||||||
|
== m and r["стратегия"] == strat) for m in mazes]
|
||||||
|
ax.bar(x + i*width, times, width, label=strat)
|
||||||
|
|
||||||
|
ax.set_ylabel('Время выполнения (мс)')
|
||||||
|
ax.set_title('Сравнение времени работы алгоритмов')
|
||||||
|
ax.set_xticks(x + width)
|
||||||
|
ax.set_xticklabels(mazes, rotation=15)
|
||||||
|
ax.legend()
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig("results/benchmark_execution_time.png", dpi=200)
|
||||||
|
plt.savefig("results/benchmark_execution_time.eps", dpi=200)
|
||||||
|
plt.close()
|
||||||
|
print("Графики сохранены в текущую директорию.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
data = run_benchmarks()
|
||||||
|
plot_results(data)
|
||||||
Loading…
Reference in New Issue
Block a user