diff --git a/VildyaevAV/docs/image.png b/VildyaevAV/docs/image.png new file mode 100644 index 0000000..19c61c7 Binary files /dev/null and b/VildyaevAV/docs/image.png differ diff --git a/VildyaevAV/docs/task2/blocked_time.png b/VildyaevAV/docs/task2/blocked_time.png new file mode 100644 index 0000000..c9c6a1c Binary files /dev/null and b/VildyaevAV/docs/task2/blocked_time.png differ diff --git a/VildyaevAV/docs/task2/blocked_visited.png b/VildyaevAV/docs/task2/blocked_visited.png new file mode 100644 index 0000000..fe741aa Binary files /dev/null and b/VildyaevAV/docs/task2/blocked_visited.png differ diff --git a/VildyaevAV/docs/task2/builder.py b/VildyaevAV/docs/task2/builder.py new file mode 100644 index 0000000..b424282 --- /dev/null +++ b/VildyaevAV/docs/task2/builder.py @@ -0,0 +1,66 @@ +from abc import ABC, abstractmethod + +from cell import Cell +from maze import Maze + + +class MazeBuilder(ABC): + + @abstractmethod + def build_from_file(self, filename): + pass + + +class TextFileMazeBuilder(MazeBuilder): + + def build_from_file(self, filename): + + with open(filename, "r", encoding="utf-8") as file: + lines = [line.rstrip("\n") for line in file] + + height = len(lines) + width = len(lines[0]) + + cells = [] + + start = None + exit_cell = None + + for y in range(height): + + row = [] + + for x in range(width): + + symbol = lines[y][x] + + cell = Cell(x, y) + + if symbol == "#": + cell.is_wall = True + + elif symbol == "S": + cell.is_start = True + start = cell + + elif symbol == "E": + cell.is_exit = True + exit_cell = cell + + row.append(cell) + + cells.append(row) + + if start is None: + raise ValueError("Start cell S not found") + + if exit_cell is None: + raise ValueError("Exit cell E not found") + + return Maze( + cells, + width, + height, + start, + exit_cell + ) \ No newline at end of file diff --git a/VildyaevAV/docs/task2/cell.py b/VildyaevAV/docs/task2/cell.py new file mode 100644 index 0000000..25d8762 --- /dev/null +++ b/VildyaevAV/docs/task2/cell.py @@ -0,0 +1,11 @@ +class Cell: + def __init__(self, x, y, is_wall=False): + self.x = x + self.y = y + + self.is_wall = is_wall + self.is_start = False + self.is_exit = False + + def is_passable(self): + return not self.is_wall \ No newline at end of file diff --git a/VildyaevAV/docs/task2/empty_time.png b/VildyaevAV/docs/task2/empty_time.png new file mode 100644 index 0000000..1e74e0a Binary files /dev/null and b/VildyaevAV/docs/task2/empty_time.png differ diff --git a/VildyaevAV/docs/task2/empty_visited.png b/VildyaevAV/docs/task2/empty_visited.png new file mode 100644 index 0000000..20c1036 Binary files /dev/null and b/VildyaevAV/docs/task2/empty_visited.png differ diff --git a/VildyaevAV/docs/task2/image.png b/VildyaevAV/docs/task2/image.png new file mode 100644 index 0000000..bc778f8 Binary files /dev/null and b/VildyaevAV/docs/task2/image.png differ diff --git a/VildyaevAV/docs/task2/large_time.png b/VildyaevAV/docs/task2/large_time.png new file mode 100644 index 0000000..7eaea4f Binary files /dev/null and b/VildyaevAV/docs/task2/large_time.png differ diff --git a/VildyaevAV/docs/task2/large_visited.png b/VildyaevAV/docs/task2/large_visited.png new file mode 100644 index 0000000..5edb524 Binary files /dev/null and b/VildyaevAV/docs/task2/large_visited.png differ diff --git a/VildyaevAV/docs/task2/main.py b/VildyaevAV/docs/task2/main.py new file mode 100644 index 0000000..3c252dc --- /dev/null +++ b/VildyaevAV/docs/task2/main.py @@ -0,0 +1,112 @@ +import csv + +from builder import TextFileMazeBuilder +from strategies import BFSStrategy, DFSStrategy, AStarStrategy +from solver import MazeSolver +from observer import ConsoleView + + +def print_path(maze, path): + path_coords = {(cell.x, cell.y) for cell in path} + + for row in maze.cells: + line = "" + + for cell in row: + if cell.is_wall: + line += "#" + elif cell.is_start: + line += "S" + elif cell.is_exit: + line += "E" + elif (cell.x, cell.y) in path_coords: + line += "*" + else: + line += " " + + print(line) + + +builder = TextFileMazeBuilder() + +maze_files = [ + "docs/task2/mazes/small.txt", + "docs/task2/mazes/medium.txt", + "docs/task2/mazes/blocked.txt", + "docs/task2/mazes/large.txt", + "docs/task2/mazes/empty.txt", + "docs/task2/mazes/no_exit.txt" +] + +strategies = [ + BFSStrategy(), + DFSStrategy(), + AStarStrategy() +] + +results = [] + +for maze_file in maze_files: + print("\n======================") + print("Maze:", maze_file) + + try: + maze = builder.build_from_file(maze_file) + + except Exception as e: + print("Error:", e) + continue + + for strategy in strategies: + print("\nStrategy:", strategy.__class__.__name__) + + solver = MazeSolver(maze, strategy) + + observer = ConsoleView() + solver.add_observer(observer) + + runs = 5 + total_time = 0 + last_stats = None + last_path = [] + + for _ in range(runs): + stats, path = solver.solve() + + total_time += stats.time_ms + last_stats = stats + last_path = path + + average_time = total_time / runs + + print("Average time ms:", round(average_time, 4)) + print("Visited:", last_stats.visited_cells) + print("Path length:", last_stats.path_length) + + if last_path: + print_path(maze, last_path) + else: + print("Path not found") + + results.append([ + maze_file, + strategy.__class__.__name__, + round(average_time, 4), + last_stats.visited_cells, + last_stats.path_length + ]) + +with open("maze_results.csv", "w", newline="", encoding="utf-8") as file: + writer = csv.writer(file) + + writer.writerow([ + "maze", + "strategy", + "time_ms", + "visited_cells", + "path_length" + ]) + + writer.writerows(results) + +print("\nResults saved to maze_results.csv") \ No newline at end of file diff --git a/VildyaevAV/docs/task2/maze.py b/VildyaevAV/docs/task2/maze.py new file mode 100644 index 0000000..4fa362d --- /dev/null +++ b/VildyaevAV/docs/task2/maze.py @@ -0,0 +1,35 @@ +class Maze: + def __init__(self, cells, width, height, start, exit_cell): + self.cells = cells + self.width = width + self.height = height + + self.start = start + self.exit = exit_cell + + def get_cell(self, x, y): + if 0 <= y < self.height and 0 <= x < self.width: + return self.cells[y][x] + + return None + + def get_neighbors(self, cell): + directions = [ + (0, -1), + (0, 1), + (-1, 0), + (1, 0) + ] + + neighbors = [] + + for dx, dy in directions: + nx = cell.x + dx + ny = cell.y + dy + + neighbor = self.get_cell(nx, ny) + + if neighbor and neighbor.is_passable(): + neighbors.append(neighbor) + + return neighbors \ No newline at end of file diff --git a/VildyaevAV/docs/task2/maze_results.csv b/VildyaevAV/docs/task2/maze_results.csv new file mode 100644 index 0000000..b43d9ae --- /dev/null +++ b/VildyaevAV/docs/task2/maze_results.csv @@ -0,0 +1,16 @@ +maze,strategy,time_ms,visited_cells,path_length +docs/task2/mazes/small.txt,BFSStrategy,0.0228,10,7 +docs/task2/mazes/small.txt,DFSStrategy,0.0163,10,7 +docs/task2/mazes/small.txt,AStarStrategy,0.0252,10,7 +docs/task2/mazes/medium.txt,BFSStrategy,0.0238,18,0 +docs/task2/mazes/medium.txt,DFSStrategy,0.0257,18,0 +docs/task2/mazes/medium.txt,AStarStrategy,0.0344,18,0 +docs/task2/mazes/blocked.txt,BFSStrategy,0.0085,3,0 +docs/task2/mazes/blocked.txt,DFSStrategy,0.006,3,0 +docs/task2/mazes/blocked.txt,AStarStrategy,0.0059,3,0 +docs/task2/mazes/large.txt,BFSStrategy,0.0558,45,0 +docs/task2/mazes/large.txt,DFSStrategy,0.0522,45,0 +docs/task2/mazes/large.txt,AStarStrategy,0.0757,45,0 +docs/task2/mazes/empty.txt,BFSStrategy,0.0708,56,14 +docs/task2/mazes/empty.txt,DFSStrategy,0.039,49,28 +docs/task2/mazes/empty.txt,AStarStrategy,0.1058,56,14 diff --git a/VildyaevAV/docs/task2/mazes/blocked.txt b/VildyaevAV/docs/task2/mazes/blocked.txt new file mode 100644 index 0000000..e4f82a5 --- /dev/null +++ b/VildyaevAV/docs/task2/mazes/blocked.txt @@ -0,0 +1,5 @@ +####### +#S# #E# +# # # # +# ### # +####### \ No newline at end of file diff --git a/VildyaevAV/docs/task2/mazes/empty.txt b/VildyaevAV/docs/task2/mazes/empty.txt new file mode 100644 index 0000000..f9c2085 --- /dev/null +++ b/VildyaevAV/docs/task2/mazes/empty.txt @@ -0,0 +1,9 @@ +########## +#S # +# # +# # +# # +# # +# # +# E# +########## \ No newline at end of file diff --git a/VildyaevAV/docs/task2/mazes/large.txt b/VildyaevAV/docs/task2/mazes/large.txt new file mode 100644 index 0000000..b3c7e0f --- /dev/null +++ b/VildyaevAV/docs/task2/mazes/large.txt @@ -0,0 +1,11 @@ +#################### +#S # # # +##### ### ### ### ## +# # # # # +# ### # ##### ###### +# # # # # +# # ####### ###### # +# # # # +# ####### ######## # +# # #E# +#################### \ No newline at end of file diff --git a/VildyaevAV/docs/task2/mazes/medium.txt b/VildyaevAV/docs/task2/mazes/medium.txt new file mode 100644 index 0000000..9880b5e --- /dev/null +++ b/VildyaevAV/docs/task2/mazes/medium.txt @@ -0,0 +1,7 @@ +############ +#S # # +### ###### # +# # # +# #### # ### +# # # E# +############ \ No newline at end of file diff --git a/VildyaevAV/docs/task2/mazes/no_exit.txt b/VildyaevAV/docs/task2/mazes/no_exit.txt new file mode 100644 index 0000000..208e355 --- /dev/null +++ b/VildyaevAV/docs/task2/mazes/no_exit.txt @@ -0,0 +1,5 @@ +####### +#S# # +# ### # +# # +####### \ No newline at end of file diff --git a/VildyaevAV/docs/task2/mazes/small.txt b/VildyaevAV/docs/task2/mazes/small.txt new file mode 100644 index 0000000..d642d01 --- /dev/null +++ b/VildyaevAV/docs/task2/mazes/small.txt @@ -0,0 +1,5 @@ +####### +#S ## +# ### # +# E# +####### \ No newline at end of file diff --git a/VildyaevAV/docs/task2/medium_time.png b/VildyaevAV/docs/task2/medium_time.png new file mode 100644 index 0000000..49e604a Binary files /dev/null and b/VildyaevAV/docs/task2/medium_time.png differ diff --git a/VildyaevAV/docs/task2/medium_visited.png b/VildyaevAV/docs/task2/medium_visited.png new file mode 100644 index 0000000..7ff6aa7 Binary files /dev/null and b/VildyaevAV/docs/task2/medium_visited.png differ diff --git a/VildyaevAV/docs/task2/observer.py b/VildyaevAV/docs/task2/observer.py new file mode 100644 index 0000000..4b94cf9 --- /dev/null +++ b/VildyaevAV/docs/task2/observer.py @@ -0,0 +1,14 @@ +from abc import ABC, abstractmethod + + +class Observer(ABC): + + @abstractmethod + def update(self, event): + pass + + +class ConsoleView(Observer): + + def update(self, event): + print(f"[Observer] {event}") \ No newline at end of file diff --git a/VildyaevAV/docs/task2/plot_results.py b/VildyaevAV/docs/task2/plot_results.py new file mode 100644 index 0000000..4fadd5c --- /dev/null +++ b/VildyaevAV/docs/task2/plot_results.py @@ -0,0 +1,50 @@ +import pandas as pd +import matplotlib.pyplot as plt + +df = pd.read_csv("maze_results.csv") + +mazes = df["maze"].unique() + +for maze in mazes: + maze_df = df[df["maze"] == maze] + + plt.figure(figsize=(8, 5)) + + plt.bar( + maze_df["strategy"], + maze_df["time_ms"] + ) + + plt.title(f"Time for {maze}") + plt.ylabel("Time ms") + plt.xlabel("Strategy") + + plt.tight_layout() + + filename = maze.split("/")[-1].replace(".txt", "_time.png") + plt.savefig(filename) + + plt.close() + +for maze in mazes: + maze_df = df[df["maze"] == maze] + + plt.figure(figsize=(8, 5)) + + plt.bar( + maze_df["strategy"], + maze_df["visited_cells"] + ) + + plt.title(f"Visited cells for {maze}") + plt.ylabel("Visited cells") + plt.xlabel("Strategy") + + plt.tight_layout() + + filename = maze.split("/")[-1].replace(".txt", "_visited.png") + plt.savefig(filename) + + plt.close() + +print("Graphs created") \ No newline at end of file diff --git a/VildyaevAV/docs/task2/small_time.png b/VildyaevAV/docs/task2/small_time.png new file mode 100644 index 0000000..46eca7b Binary files /dev/null and b/VildyaevAV/docs/task2/small_time.png differ diff --git a/VildyaevAV/docs/task2/small_visited.png b/VildyaevAV/docs/task2/small_visited.png new file mode 100644 index 0000000..de989b4 Binary files /dev/null and b/VildyaevAV/docs/task2/small_visited.png differ diff --git a/VildyaevAV/docs/task2/solver.py b/VildyaevAV/docs/task2/solver.py new file mode 100644 index 0000000..17e5c17 --- /dev/null +++ b/VildyaevAV/docs/task2/solver.py @@ -0,0 +1,50 @@ +import time + + +class SearchStats: + def __init__(self, time_ms, visited_cells, path_length): + self.time_ms = time_ms + self.visited_cells = visited_cells + self.path_length = path_length + + +class MazeSolver: + def __init__(self, maze, strategy): + self.maze = maze + self.strategy = strategy + self.observers = [] + + def set_strategy(self, strategy): + self.strategy = strategy + + def add_observer(self, observer): + self.observers.append(observer) + + def notify(self, event): + for observer in self.observers: + observer.update(event) + + def solve(self): + self.notify("search_started") + + start_time = time.perf_counter() + + path, visited_cells = self.strategy.find_path( + self.maze, + self.maze.start, + self.maze.exit + ) + + end_time = time.perf_counter() + + self.notify("search_finished") + + time_ms = (end_time - start_time) * 1000 + + stats = SearchStats( + time_ms=time_ms, + visited_cells=visited_cells, + path_length=len(path) + ) + + return stats, path \ No newline at end of file diff --git a/VildyaevAV/docs/task2/strategies.py b/VildyaevAV/docs/task2/strategies.py new file mode 100644 index 0000000..894b56c --- /dev/null +++ b/VildyaevAV/docs/task2/strategies.py @@ -0,0 +1,187 @@ +from abc import ABC, abstractmethod + +from collections import deque +import heapq + + +class PathFindingStrategy(ABC): + + @abstractmethod + def find_path(self, maze, start, exit_cell): + pass + + +class BFSStrategy(PathFindingStrategy): + + def find_path(self, maze, start, exit_cell): + + queue = deque([start]) + + visited = set() + visited.add((start.x, start.y)) + + parent = {} + + while queue: + + current = queue.popleft() + + if current == exit_cell: + return self.restore_path(parent, start, exit_cell), len(visited) + + for neighbor in maze.get_neighbors(current): + + key = (neighbor.x, neighbor.y) + + if key not in visited: + + visited.add(key) + + parent[key] = current + + queue.append(neighbor) + + return [], len(visited) + + def restore_path(self, parent, start, exit_cell): + + path = [] + + current = exit_cell + + while current != start: + + path.append(current) + + current = parent[(current.x, current.y)] + + path.append(start) + + path.reverse() + + return path + + +class DFSStrategy(PathFindingStrategy): + + def find_path(self, maze, start, exit_cell): + + stack = [start] + + visited = set() + visited.add((start.x, start.y)) + + parent = {} + + while stack: + + current = stack.pop() + + if current == exit_cell: + return self.restore_path(parent, start, exit_cell), len(visited) + + for neighbor in maze.get_neighbors(current): + + key = (neighbor.x, neighbor.y) + + if key not in visited: + + visited.add(key) + + parent[key] = current + + stack.append(neighbor) + + return [], len(visited) + + def restore_path(self, parent, start, exit_cell): + + path = [] + + current = exit_cell + + while current != start: + + path.append(current) + + current = parent[(current.x, current.y)] + + path.append(start) + + path.reverse() + + return path + + +class AStarStrategy(PathFindingStrategy): + + def heuristic(self, cell, exit_cell): + + return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y) + + def find_path(self, maze, start, exit_cell): + + heap = [] + + heapq.heappush(heap, (0, start.x, start.y, start)) + + visited = set() + + parent = {} + + g_score = { + (start.x, start.y): 0 + } + + while heap: + + _, _, _, current = heapq.heappop(heap) + + key_current = (current.x, current.y) + + if key_current in visited: + continue + + visited.add(key_current) + + if current == exit_cell: + return self.restore_path(parent, start, exit_cell), len(visited) + + for neighbor in maze.get_neighbors(current): + + key = (neighbor.x, neighbor.y) + + tentative = g_score[key_current] + 1 + + if key not in g_score or tentative < g_score[key]: + + g_score[key] = tentative + + priority = tentative + self.heuristic(neighbor, exit_cell) + + heapq.heappush( + heap, + (priority, neighbor.x, neighbor.y, neighbor) + ) + + parent[key] = current + + return [], len(visited) + + def restore_path(self, parent, start, exit_cell): + + path = [] + + current = exit_cell + + while current != start: + + path.append(current) + + current = parent[(current.x, current.y)] + + path.append(start) + + path.reverse() + + return path \ No newline at end of file diff --git a/VildyaevAV/docs/task2/task2_report.md b/VildyaevAV/docs/task2/task2_report.md new file mode 100644 index 0000000..a3a5fd6 --- /dev/null +++ b/VildyaevAV/docs/task2/task2_report.md @@ -0,0 +1,243 @@ +# Отчет по заданию 2 + +## Тема + +Реализация поиска пути в лабиринте с использованием паттернов проектирования и различных алгоритмов поиска. + +--- + +# Цель работы + +Изучить применение ООП и паттернов проектирования при реализации алгоритмов поиска пути в лабиринте. + +Реализовать: +- BFS +- DFS +- A* + +Сравнить эффективность алгоритмов по: +- времени выполнения +- количеству посещённых клеток +- длине найденного пути + +--- + +# Используемые паттерны + +## Builder +Используется для загрузки лабиринта из файла. + +## Strategy +Используется для переключения алгоритмов поиска пути. + +## Observer +Используется для уведомлений о начале и окончании поиска. + +--- + +# Структура проекта + +```text +docs/task2/ + +├── mazes/ +│ ├── small.txt +│ ├── medium.txt +│ ├── blocked.txt +│ ├── no_exit.txt +│ ├── large.txt +│ └── empty.txt +│ +├── cell.py +├── maze.py +├── builder.py +├── strategies.py +├── solver.py +├── observer.py +├── main.py +├── plot_results.py +├── maze_results.csv +└── task2_report.md +``` + +--- + +# UML диаграмма + +В проекте была построена UML-диаграмма классов с использованием Mermaid. + +![alt text](image.png) + +--- + +# Описание классов + +## Cell +Класс клетки лабиринта. + +Хранит: +- координаты +- тип клетки +- признаки стены, старта и выхода + +--- + +## Maze +Класс лабиринта. + +Содержит: +- двумерный массив клеток +- размеры лабиринта +- стартовую клетку +- выход + +--- + +## TextFileMazeBuilder +Загружает лабиринт из текстового файла. + +--- + +## BFSStrategy +Алгоритм поиска в ширину. + +Находит кратчайший путь. + +--- + +## DFSStrategy +Алгоритм поиска в глубину. + +Работает быстро, но путь может быть не кратчайшим. + +--- + +## AStarStrategy +Эвристический алгоритм поиска. + +Использует манхэттенское расстояние. + +--- + +## MazeSolver +Основной класс-оркестратор. + +Запускает алгоритм поиска и собирает статистику. + +--- + +# Результаты экспериментов + +Результаты сохраняются в файл: + +```text +maze_results.csv +``` + +Проводилось сравнение: +- времени работы +- количества посещённых клеток +- длины пути + +## Таблица результатов + +| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути | +|---|---|---|---|---| +| small | BFS | 0.0396 | 10 | 7 | +| small | DFS | 0.0251 | 10 | 7 | +| small | A* | 0.0359 | 10 | 7 | +| medium | BFS | 0.0312 | 18 | 0 | +| medium | DFS | 0.0277 | 18 | 0 | +| medium | A* | 0.0359 | 18 | 0 | +| blocked | BFS | 0.0123 | 3 | 0 | +| blocked | DFS | 0.0089 | 3 | 0 | +| blocked | A* | 0.0133 | 3 | 0 | +| large | BFS | 0.0602 | 45 | 0 | +| large | DFS | 0.0509 | 45 | 0 | +| large | A* | 0.0682 | 45 | 0 | +| empty | BFS | 0.0711 | 56 | 14 | +| empty | DFS | 0.0419 | 49 | 28 | +| empty | A* | 0.1144 | 56 | 14 | + +--- + +## Графики + +### Время выполнения + +![blocked](blocked_time.png) + +![small](small_time.png) + +![medium](medium_time.png) + +![large](large_time.png) + +![empty](empty_time.png) + +--- + +### Количество посещённых клеток + +![blocked](blocked_visited.png) + +![small](small_visited.png) + +![medium](medium_visited.png) + +![large](large_visited.png) + +![empty](empty_visited.png) +--- + +# Графики + +Построены графики: +- времени выполнения +- количества посещённых клеток + +Для каждого лабиринта. + +--- + +# Анализ эффективности алгоритмов + +В ходе экспериментов были сравнены алгоритмы BFS, DFS и A* на лабиринтах различной сложности. + +## BFS + +Алгоритм BFS гарантированно находит кратчайший путь, однако может посещать большое количество клеток. На больших лабиринтах время работы увеличивается. + +## DFS + +DFS работает быстрее других алгоритмов, так как уходит в глубину и не исследует все возможные пути. Однако найденный путь может быть не кратчайшим. + +## A* + +Алгоритм A* использует эвристику и старается двигаться к выходу наиболее оптимальным образом. На простых лабиринтах показывает хорошие результаты, однако на некоторых картах из-за вычисления эвристики работает медленнее DFS. + +## Вывод по экспериментам + +- DFS показал наименьшее время выполнения. +- BFS обеспечивает поиск кратчайшего пути. +- A* хорошо подходит для сложных лабиринтов с большим количеством вариантов движения. +- На лабиринтах без выхода все алгоритмы посещают примерно одинаковое количество клеток. + +# Выводы + +В ходе работы была реализована система поиска пути в лабиринте с использованием объектно-ориентированного подхода и паттернов проектирования. + +Были использованы паттерны: + +- Builder — для загрузки лабиринта из файла. +- Strategy — для переключения алгоритмов поиска пути. +- Observer — для уведомлений о событиях поиска. + +Использование паттернов позволило сделать архитектуру гибкой и расширяемой. + +Например: +- можно легко добавить новый алгоритм поиска пути; +- можно реализовать другой способ загрузки лабиринта; +- можно подключить новые способы отображения информации. + +Без применения паттернов код был бы более связанным и сложным для расширения и поддержки. \ No newline at end of file