\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}