diff --git a/MashinDD/lab2/docs/data/benchmark.py b/MashinDD/lab2/docs/data/benchmark.py new file mode 100644 index 0000000..a1225d3 --- /dev/null +++ b/MashinDD/lab2/docs/data/benchmark.py @@ -0,0 +1,153 @@ +import time +import csv +import os +import random + +from maze_builder import TextFileMazeBuilder +from maze_solver import MazeSolver +from maze_strategies import BFSStrategy, DFSStrategy, AStarStrategy + +REPEATS = 7 +OUTPUT_DIR = os.path.dirname(os.path.abspath(__file__)) +CSV_PATH = os.path.join(OUTPUT_DIR, 'results.csv') + +STRATEGIES = { + 'BFS': BFSStrategy, + 'DFS': DFSStrategy, + 'A*': AStarStrategy, +} + +MAZES = [ + ('small_10x10', 'maze_small.txt'), + ('medium_50x50', 'maze_medium.txt'), + ('large_100x100', 'maze_large.txt'), + ('open_50x50', 'maze_open.txt'), + ('no_exit_20x20', 'maze_no_exit.txt'), +] + +def _make_grid(width, height, density=0.0, has_exit=True, seed=42): + + rng = random.Random(seed) + grid = [] + for y in range(height): + row = [] + for x in range(width): + on_border = (x == 0 or x == width - 1 or y == 0 or y == height - 1) + row.append('#' if on_border else ' ') + grid.append(row) + + for y in range(1, height - 1): + for x in range(1, width - 1): + if rng.random() < density: + grid[y][x] = '#' + + grid[1][1] = 'S' + if has_exit: + grid[height - 2][width - 2] = 'E' + + return '\n'.join(''.join(row) for row in grid) + + +def generate_maze_files(): + mazes_data = { + 'maze_small.txt': _make_grid(10, 10, density=0.15), + 'maze_medium.txt': _make_grid(50, 50, density=0.28), + 'maze_large.txt': _make_grid(100, 100, density=0.30), + 'maze_open.txt': _make_grid(50, 50, density=0.0), + 'maze_no_exit.txt': _make_grid(20, 20, density=0.20, has_exit=False), + } + no_exit = list(mazes_data['maze_no_exit.txt'].splitlines()) + no_exit[18] = no_exit[18][:18] + 'E' + no_exit[18][19:] + no_exit[17] = no_exit[17][:18] + '#' + no_exit[17][19:] + no_exit[18] = no_exit[18][:17] + '#' + no_exit[18][18:] + mazes_data['maze_no_exit.txt'] = '\n'.join(no_exit) + + maze_dir = os.path.dirname(os.path.abspath(__file__)) + for fname, content in mazes_data.items(): + path = os.path.join(maze_dir, fname) + with open(path, 'w', encoding='utf-8') as f: + f.write(content) + + print("✅ Файлы лабиринтов созданы") +def avg(lst): + return sum(lst) / len(lst) if lst else 0 + + +def run_benchmark(): + builder = TextFileMazeBuilder() + maze_dir = os.path.dirname(os.path.abspath(__file__)) + + all_results = [ + ['лабиринт', 'стратегия', 'время_мс', 'посещено_клеток', 'длина_пути'] + + [f'замер_{i+1}' for i in range(REPEATS)] + ] + + print(f"\nЗапуск бенчмарков (повторений: {REPEATS})\n") + print(f" {'Лабиринт':<18} {'Алгоритм':<6} {'Время мс':>10} " + f"{'Посещено':>10} {'Путь':>6}") + print(' ' + '-' * 56) + + for maze_label, maze_file in MAZES: + maze_path = os.path.join(maze_dir, maze_file) + try: + maze = builder.build_from_file(maze_path) + except Exception as e: + print(f" ❌ {maze_file}: {e}") + continue + + solver = MazeSolver(maze) + + for strat_name, StratClass in STRATEGIES.items(): + times_ms, visited_list, path_len = [], [], 0 + + for _ in range(REPEATS): + strat = StratClass() + solver.set_strategy(strat) + stats = solver.solve() + times_ms.append(stats.time_ms) + visited_list.append(stats.visited_cells) + path_len = stats.path_length + + mean_t = avg(times_ms) + mean_v = avg(visited_list) + + print(f" {maze_label:<18} {strat_name:<6} " + f"{mean_t:>10.3f} {mean_v:>10.0f} {path_len:>6}") + + all_results.append([ + maze_label, strat_name, + f"{mean_t:.4f}", f"{mean_v:.0f}", str(path_len) + ] + [f"{t:.4f}" for t in times_ms]) + + with open(CSV_PATH, 'w', newline='', encoding='utf-8') as f: + csv.writer(f).writerows(all_results) + + print(f"\n✅ Результаты сохранены: {CSV_PATH}") + +def smoke_test(): + print("=== Smoke Test ===\n") + + maze_dir = os.path.dirname(os.path.abspath(__file__)) + test_path = os.path.join(maze_dir, '_test_maze.txt') + + with open(test_path, 'w', encoding='utf-8') as f: + f.write("#######\n#S #\n# #\n# E#\n#######") + + builder = TextFileMazeBuilder() + maze = builder.build_from_file(test_path) + + for name, StratClass in STRATEGIES.items(): + strat = StratClass() + path = strat.find_path(maze, maze.start, maze.exit) + assert len(path) > 0, f"{name}: путь не найден!" + assert path[0].is_start + assert path[-1].is_exit + print(f" ✅ {name}: путь длиной {len(path)} — OK") + + os.remove(test_path) + print("\nВсе тесты пройдены!\n") + +if __name__ == '__main__': + smoke_test() + generate_maze_files() + run_benchmark() diff --git a/MashinDD/lab2/docs/data/chart_время-мс.png b/MashinDD/lab2/docs/data/chart_время-мс.png new file mode 100644 index 0000000..d20c76d Binary files /dev/null and b/MashinDD/lab2/docs/data/chart_время-мс.png differ diff --git a/MashinDD/lab2/docs/data/chart_длина-пути.png b/MashinDD/lab2/docs/data/chart_длина-пути.png new file mode 100644 index 0000000..11b5e33 Binary files /dev/null and b/MashinDD/lab2/docs/data/chart_длина-пути.png differ diff --git a/MashinDD/lab2/docs/data/chart_посещено-клеток.png b/MashinDD/lab2/docs/data/chart_посещено-клеток.png new file mode 100644 index 0000000..d2186e3 Binary files /dev/null and b/MashinDD/lab2/docs/data/chart_посещено-клеток.png differ diff --git a/MashinDD/lab2/docs/data/maze_builder.py b/MashinDD/lab2/docs/data/maze_builder.py new file mode 100644 index 0000000..78abf85 --- /dev/null +++ b/MashinDD/lab2/docs/data/maze_builder.py @@ -0,0 +1,44 @@ +from abc import ABC, abstractmethod +from maze_model import Cell, Maze + + +class MazeBuilder(ABC): + @abstractmethod + def build_from_file(self, filename) -> Maze: + pass + + +class TextFileMazeBuilder(MazeBuilder): + def build_from_file(self, filename) -> Maze: + with open(filename, 'r', encoding='utf-8') as f: + lines = f.read().splitlines() + + width = max(len(line) for line in lines) if lines else 0 + height = len(lines) + + cells = [] + start = None + exit_cell = None + + for y, line in enumerate(lines): + row = [] + line = line.ljust(width) + for x, char in enumerate(line): + is_wall = (char == '#') + is_start = (char == 'S') + is_exit = (char == 'E') + cell = Cell(x, y, is_wall=is_wall, + is_start=is_start, is_exit=is_exit) + if is_start: + start = cell + if is_exit: + exit_cell = cell + row.append(cell) + cells.append(row) + + if start is None: + raise ValueError("В файле лабиринта не найден старт (S)") + if exit_cell is None: + raise ValueError("В файле лабиринта не найден выход (E)") + + return Maze(width, height, cells, start, exit_cell) diff --git a/MashinDD/lab2/docs/data/maze_large.txt b/MashinDD/lab2/docs/data/maze_large.txt new file mode 100644 index 0000000..df1bb6a --- /dev/null +++ b/MashinDD/lab2/docs/data/maze_large.txt @@ -0,0 +1,100 @@ +#################################################################################################### +#S### # ## ## # # # ## ####### ## # # ## ## ## # # ### # # +# # # # # # # # # # # # # # # # # ## ### # ## ## ## # ## ## # +# # # # # # ## # # ## ## # # # # # ### # # # ### # # # # ## ## +# ## ## ### # # # # # ### # # # ## # # # ## # +# ## # ## #### # # # # # # ## ## #### ## # # # # +### # # # # # # # # ### #### # # # ## # # # # # # # # # +# # ## ## # ## ##### ## ###### # # ## # ## # # ## #### # +# ## ## ## ## ## ## # # # # # # ## # # # +## # # ## # # # # # # ## # # # # ## # # ### +## # # # # # # # # ## ## # # # # ### ## # # +## # # # # # ## # ## # ## # # #### ## # ## # # # ## ## # # +# # # # # # ## # # ## # ## # # # # ### # # # # # # ### # # +## # ## ## # # # # ### # ## ## # # ### ## # # +## ## # # ## ### # # # # # # # # ## # # # # # # +# ## # # ## # ### ## # # # ## # # # ## # # # #### # # # # +# # # # # # # ## ## ## # # # # ### # # # +# # # #### # # # # ## # ### # # #### # # # # # # +# # # # # # ## # # # # # # # ## # ### # ## +## # ### ## ## # # # # # # # # # # # # # # ### ## # # +## ## ### # ## # # ### ## # # # # ## # # # # # # # # +##### # # # #### # ## # # # # # # ### # ## # # # # # +## # # ### # # # # ## # # # # # # #### # # # ### # +# # ## ## # ### # # ## # ## ## ### # # # # # # # ### +## ## # # # # # # # # # # ## ## # # ## +# # # ### # # # # # ## # # # ### # # # # # ## ## ## # ## # +# # # # # ##### # ## # # # # # # # # # ## ## # # # ## +# # # # # # # ## # ## # # # # # # ## ### ## # # ##### # +# # # # # ## # # ## # # ## # ## # # # # ## # # # ## # +## ## # # # # # # ### # ## ### ## # ### # ## # # # ## # # ## # # +# # # # #### # ## #### # # # # # # # # # # ### # ## # # +# # ## # # # # # # # # # # ###### # ## # ## # # # #### #### # # +# # ##### # # # ### # # # # # # # # # ## ### # # +# # # # # # # ## # # ## # # ## # # # # # # # ## # # ### +## # ## # # # # #### # # ## # ## ## # ## # # ## # # +## # # # ## # # # # # # # # # # # # ###### # ## # # ## ### # #### # # +## # # # # # # # # # # # ## # # # # # # ## # # # ## # ## +## # # # ### # # # # # # # # # # # # # # ### +# # ### # # # # # ## ## ## # # ## # ### ### # # # +# # # # # ## # # ## ## # # # # # # ## ## ## # +# ### # # ### # # # # ### # # # # # # # ## # ## +# # ### ## ## ## ## # # ### # ## # # # # ## ## # # # # # # +# ## # # # ## # # # # ## # ### #### # ## ###### ### # +# # # # ### ### # # ## # # # ### ## # ## # # ## ## +# # # ### #### # # # # ### # # # ## ### ## # ## #### # # +# ### ## # # # # # # # # ### # # # # ## # ### ### ## # +# # # # # # # # # # ## ### ## ### # ## # # # ## # #### # ## # # +# # # # # # # # # # # ### # # # # # ## # # # # # # # +# ## # # # # ## # # # # ## ## ## # # ## # ## # # ## # ## # +# # # ## # # # # ### # # # # # # # ## # # # ## # ### ## # # # +## # ## # ## ### ## # # # # ## # # # # # # # +## ## # # ### # # # # # ## # # # # # ## # ## # # # # +# # # ## # ### # ## # # ## # # # # # # # # +# # # # # ## #### # # ### # ## # # ## # # ## # +# # # # ## # ### # ## ## # # # # ### # # # +# # # # # # # # # ## # ## ## ### ### # # ## # # # ## # +# # # # ## # # ### ##### # # # # ## # # # # # ## # # # +## # # # ## # # ## # ## ## # ## # ### # # # # # +# ## ## # ### # ## ### # # ## # # # # # # # # # # # ### +# ## # # # # # # # # # # # ## # # # # # # # # # # # ## # +# # # # ## # # # # # ## # # ## # # ## # # # ### ### # # # ## +# # # # ## # ## # # # # # # # ## # # ## # ### ## +### # # ## ### # ## # # #### # # # # ##### # ## #### # +# # # # # # # #### ## # ### ### # ## # ## # # ## # # # # # # ### +# #### # ## # # # # # # ## # # # # # # # # +# ## # # # # # # ## # ## ## # ### #### # # # # ## # +# # ## # ## # # # # ## ## # ## # ## # +# # # # # # # ## # # # # # # ### ## ### # ## # # ### +### # # # ##### # ## ## # # # ## # ## ## # # # # # # +# # # # # # ## ##### # ### # ## # # # ## # ### #### # # +# # ### # ## # # ### ## ## # ## # ### # ## ### # ### +# ## ## ## # # # # # # ### # ## # # ## # # # # +## ## ## # ## # ## # # # ## # ## # ## # ## # # # # +# # # # # # # # ## # # # ####### # ## ## ## ## +# # # # # # # # # ## # # # # # ## # # ### # ## +# # ## #### # # # # # ## ### # ### # ### # ### ## # # # +## # # ## # # # # # # # # ## # ##### # ## ##### #### ### +# # # # ## # ## # # ## # # ### ## ## # ###### +# # ## # # # # # # # # # ## ## # ## ## ## # ## # # +### #### # # ## # # # # # ## # # ## # # # #### # # ## # # +# ## ## # # ## # ## ## # # ## # # # # # #### # # +# ## # # # ## ### ## #### # # # # # # ## ### # # # ## +## # # # # # # # ## # ## ### # ## # ## # # # # +# # # # # # # # # ### # # # ## # # ## ## # #### # +# # ## # # # # # # # # # # ## ### # # # ## +## ## # ## # # # ## # # # # # #### # # ## ### # +## # ## ## # # # # ### # # ## # # # ## ## # # # # ## # +# ## # ## # # #### # # # # # # ## # # # # # # ### # +# ## # #### # # ## # # # # ### ## # ## ### # ## ## ## +# # # # # # ## # # # ## # #### # ##### # # # # # # # # +# # ## ## ### # ### ### # # #### # # # # ## # ## # # # # #### # # +# # # # ## # # ## # # ## # # ## # ## # # # ## ## # +# # ## # # # ## ## # ### ## # ## # # # # # # # ## # # # +# # ## # ## ## ## # # ## # # # # # ## # # # # ### # # +# # # ## # # # # # # # # # # # # ## # # # ## # # # +## # ## # # # # ## # # ## # # # # # # ## # # # # # # # # +# # ## # ## # ### # # ### # ## # # # ## # ### # ## # # +# # # ## # # ## # # # ## # # #### ## # # # ### # ## +# #### ## ### ### # # ### # # ## # # # ### # ####### # ## # #E# +#################################################################################################### \ No newline at end of file diff --git a/MashinDD/lab2/docs/data/maze_medium.txt b/MashinDD/lab2/docs/data/maze_medium.txt new file mode 100644 index 0000000..8bed293 --- /dev/null +++ b/MashinDD/lab2/docs/data/maze_medium.txt @@ -0,0 +1,50 @@ +################################################## +#S### # ## ## # # # ## ## ##### +# ## # # ## ## ## # # ### # # +# # # # # # # # # # # # # # # +# # # ## ### # ## ## ## # ## ## +## # # # # # # # # ## ## # # +# # # # ### # # ### # # # ## +# ## # ## ## ### # # # # # +## ### # # # ## # # # # +# # # # ## #### # # # # # # +## ## ## ## # ## # # # +# # ## # # # # # # # # ### ## +#### # # # ## # # # # # # +# # # # # ## ## # ## ###### +# ## ##### # # ## # ## # # # +# ## #### # ## # ## ## +## ## # # # # # +## ## # # # # # # # ## +# # # # ## # # +## # ## # # ### # # # # # # # +# # ## # # # # # +# ### ## # # # # # # # ### +# # ## # ## # # #### ## # ## # # ## +# ## ## # # # # # # ## # # +# # ## # ## # # # # ### # +# # # # # # ### # # # ## ## ## +# # # # ### # ## ## # # # +# ### ## # ## # # +# ## ### # # # # # # # +# ## # # # # ## # # # # ## +### # # # # ## # # # ## # # +## # ### # # # # # # +# # # # # ## ## ## # # +# # # ### # # # # ### +### # # # # ## ## # # +# # ### # # # # # # ## +# # # # ## # # # +# # # # ## # ### # ## # # # +### # # # # # # # # # # +# # # # # ### ## # # ## ### # +# # ## # ### ## # # # # ## # # +# # # # # # # ### # +## # # #### # ## # # # # # +# # ### # ## # # # # # # +# # # ### # # # # ## # # +## # # # # #### # # # ### # +## ## ## # ### # # ## # +# # ## ## ### # # # # # # ### # +# ## # # # # # E# +################################################## \ No newline at end of file diff --git a/MashinDD/lab2/docs/data/maze_model.py b/MashinDD/lab2/docs/data/maze_model.py new file mode 100644 index 0000000..664ad01 --- /dev/null +++ b/MashinDD/lab2/docs/data/maze_model.py @@ -0,0 +1,62 @@ +class Cell: + def __init__(self, x, y, is_wall=False, is_start=False, is_exit=False): + self.x = x + self.y = y + self.is_wall = is_wall + self.is_start = is_start + self.is_exit = is_exit + + def is_passable(self): + return not self.is_wall + + def __repr__(self): + if self.is_wall: + return '#' + if self.is_start: + return 'S' + if self.is_exit: + return 'E' + return ' ' + + +class Maze: + def __init__(self, width, height, cells, start, exit_cell): + self.width = width + self.height = height + self._cells = cells + self.start = start + self.exit = exit_cell + + def get_cell(self, x, y): + if 0 <= x < self.width and 0 <= y < self.height: + 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: + neighbor = self.get_cell(cell.x + dx, cell.y + dy) + if neighbor is not None and neighbor.is_passable(): + neighbors.append(neighbor) + return neighbors + + def render(self, path=None, player_pos=None): + path_set = set((c.x, c.y) for c in path) if path else set() + + for row in self._cells: + line = '' + for cell in row: + if player_pos and cell.x == player_pos.x and cell.y == player_pos.y: + line += 'P' + elif cell.is_wall: + line += '#' + elif cell.is_start: + line += 'S' + elif cell.is_exit: + line += 'E' + elif (cell.x, cell.y) in path_set: + line += '.' + else: + line += ' ' + print(line) diff --git a/MashinDD/lab2/docs/data/maze_no_exit.txt b/MashinDD/lab2/docs/data/maze_no_exit.txt new file mode 100644 index 0000000..c93724b --- /dev/null +++ b/MashinDD/lab2/docs/data/maze_no_exit.txt @@ -0,0 +1,20 @@ +#################### +#S# # # ## # +# # # ## # +# # # # # +# # # # # +# ### +## # # # # +# # # # # +## # # # # # +# # # # +# # # ## # +# # # ## +# # # # +# # # # ## # +# # # +## # # ## +# ### ## +# # ## ### +# # # #E# +#################### \ No newline at end of file diff --git a/MashinDD/lab2/docs/data/maze_open.txt b/MashinDD/lab2/docs/data/maze_open.txt new file mode 100644 index 0000000..335d47e --- /dev/null +++ b/MashinDD/lab2/docs/data/maze_open.txt @@ -0,0 +1,50 @@ +################################################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +################################################## \ No newline at end of file diff --git a/MashinDD/lab2/docs/data/maze_small.txt b/MashinDD/lab2/docs/data/maze_small.txt new file mode 100644 index 0000000..952175d --- /dev/null +++ b/MashinDD/lab2/docs/data/maze_small.txt @@ -0,0 +1,10 @@ +########## +#S# ## +# # # # +# # # +# ## # +# # +# # # # # +# # +# E# +########## \ No newline at end of file diff --git a/MashinDD/lab2/docs/data/maze_solver.py b/MashinDD/lab2/docs/data/maze_solver.py new file mode 100644 index 0000000..5347cf9 --- /dev/null +++ b/MashinDD/lab2/docs/data/maze_solver.py @@ -0,0 +1,121 @@ +import time +from abc import ABC, abstractmethod + +class SearchStats: + def __init__(self, time_ms, visited_cells, path_length, path): + self.time_ms = time_ms + self.visited_cells = visited_cells + self.path_length = path_length + self.path = path + + def __repr__(self): + return (f"SearchStats(time={self.time_ms:.3f}ms, " + f"visited={self.visited_cells}, " + f"path_len={self.path_length})") + +class Observer(ABC): + + @abstractmethod + def update(self, event, data=None): + pass + + +class ConsoleView(Observer): + + def update(self, event, data=None): + if event == 'maze_loaded': + print(f"\n[ConsoleView] Лабиринт загружен: " + f"{data['width']}×{data['height']}") + + elif event == 'path_found': + stats = data['stats'] + strategy_name = data['strategy'] + if stats.path_length > 0: + print(f"\n[ConsoleView] [{strategy_name}] Путь найден! " + f"Длина: {stats.path_length}, " + f"Посещено клеток: {stats.visited_cells}, " + f"Время: {stats.time_ms:.3f} мс") + else: + print(f"\n[ConsoleView] [{strategy_name}] Путь не найден. " + f"Посещено клеток: {stats.visited_cells}") + + elif event == 'move': + print(f"[ConsoleView] Игрок переместился в " + f"({data['x']}, {data['y']})") + +class MazeSolver: + def __init__(self, maze, strategy=None): + 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, data=None): + for obs in self._observers: + obs.update(event, data) + + def solve(self): + if self.strategy is None: + raise RuntimeError("Стратегия не задана. Используйте set_strategy().") + + start = time.perf_counter() + path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) + end = time.perf_counter() + + stats = SearchStats( + time_ms=(end - start) * 1000, + visited_cells=getattr(self.strategy, 'visited_count', 0), + path_length=len(path), + path=path + ) + + self._notify('path_found', { + 'stats': stats, + 'strategy': type(self.strategy).__name__ + }) + + return stats + +class Command(ABC): + @abstractmethod + def execute(self): + pass + + @abstractmethod + def undo(self): + pass + + +class Player: + def __init__(self, start_cell): + self.current_cell = start_cell + + def move_to(self, cell): + self.current_cell = cell + + +class MoveCommand(Command): + def __init__(self, player, target_cell, observers=None): + self.player = player + self.target_cell = target_cell + self.previous_cell = None + self._observers = observers or [] + + def execute(self): + self.previous_cell = self.player.current_cell + self.player.move_to(self.target_cell) + for obs in self._observers: + obs.update('move', {'x': self.target_cell.x, + 'y': self.target_cell.y}) + + def undo(self): + if self.previous_cell is not None: + self.player.move_to(self.previous_cell) + for obs in self._observers: + obs.update('move', {'x': self.previous_cell.x, + 'y': self.previous_cell.y}) diff --git a/MashinDD/lab2/docs/data/maze_strategies.py b/MashinDD/lab2/docs/data/maze_strategies.py new file mode 100644 index 0000000..a71c794 --- /dev/null +++ b/MashinDD/lab2/docs/data/maze_strategies.py @@ -0,0 +1,100 @@ +from abc import ABC, abstractmethod +from collections import deque +import heapq + + +class PathFindingStrategy(ABC): + @abstractmethod + def find_path(self, maze, start, exit_cell): + pass + + +def _reconstruct_path(came_from, start, exit_cell): + path = [] + current = exit_cell + while current is not None: + path.append(current) + current = came_from.get((current.x, current.y)) + path.reverse() + if path and path[0].x == start.x and path[0].y == start.y: + return path + return [] + +class BFSStrategy(PathFindingStrategy): + def find_path(self, maze, start, exit_cell): + queue = deque([start]) + came_from = {(start.x, start.y): None} + self.visited_count = 0 + + while queue: + current = queue.popleft() + self.visited_count += 1 + + if current.x == exit_cell.x and current.y == exit_cell.y: + return _reconstruct_path(came_from, start, exit_cell) + + for neighbor in maze.get_neighbors(current): + key = (neighbor.x, neighbor.y) + if key not in came_from: + came_from[key] = current + queue.append(neighbor) + + self.visited_count = len(came_from) + return [] # путь не найден + +class DFSStrategy(PathFindingStrategy): + + def find_path(self, maze, start, exit_cell): + stack = [start] + came_from = {(start.x, start.y): None} + self.visited_count = 0 + + while stack: + current = stack.pop() + self.visited_count += 1 + + if current.x == exit_cell.x and current.y == exit_cell.y: + return _reconstruct_path(came_from, start, exit_cell) + + for neighbor in maze.get_neighbors(current): + key = (neighbor.x, neighbor.y) + if key not in came_from: + came_from[key] = current + stack.append(neighbor) + + self.visited_count = len(came_from) + return [] + +class AStarStrategy(PathFindingStrategy): + + def _heuristic(self, cell, goal): + return abs(cell.x - goal.x) + abs(cell.y - goal.y) + + def find_path(self, maze, start, exit_cell): + # (f_score, счётчик для разрыва связей, клетка) + counter = 0 + open_set = [(0, counter, start)] + came_from = {(start.x, start.y): None} + g_score = {(start.x, start.y): 0} + self.visited_count = 0 + + while open_set: + _, _, current = heapq.heappop(open_set) + self.visited_count += 1 + + if current.x == exit_cell.x and current.y == exit_cell.y: + return _reconstruct_path(came_from, start, exit_cell) + + for neighbor in maze.get_neighbors(current): + key = (neighbor.x, neighbor.y) + tentative_g = g_score[(current.x, current.y)] + 1 + + if key not in g_score or tentative_g < g_score[key]: + g_score[key] = tentative_g + f = tentative_g + self._heuristic(neighbor, exit_cell) + counter += 1 + heapq.heappush(open_set, (f, counter, neighbor)) + came_from[key] = current + + self.visited_count = len(came_from) + return [] diff --git a/MashinDD/lab2/docs/data/plot_results.py b/MashinDD/lab2/docs/data/plot_results.py new file mode 100644 index 0000000..ac1d330 --- /dev/null +++ b/MashinDD/lab2/docs/data/plot_results.py @@ -0,0 +1,103 @@ +import csv +import os + +try: + import matplotlib.pyplot as plt + import matplotlib.patches as mpatches + HAS_MPL = True +except ImportError: + HAS_MPL = False + print("⚠️ matplotlib не установлен: pip install matplotlib\n") + +CSV_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'results.csv') +OUT_DIR = os.path.dirname(os.path.abspath(__file__)) + +COLORS = {'BFS': '#4E9AF1', 'DFS': '#F4845F', 'A*': '#6BCB77'} +STRATEGIES = ['BFS', 'DFS', 'A*'] +METRICS = [ + ('время_мс', 'Среднее время (мс)'), + ('посещено_клеток', 'Посещено клеток'), + ('длина_пути', 'Длина пути (шагов)'), +] + + +def load_csv(path): + data = {} + with open(path, newline='', encoding='utf-8') as f: + reader = csv.DictReader(f) + for row in reader: + key = (row['лабиринт'], row['стратегия']) + data[key] = { + 'время_мс': float(row['время_мс']), + 'посещено_клеток': float(row['посещено_клеток']), + 'длина_пути': float(row['длина_пути']), + } + return data + + +def get_mazes(data): + seen = [] + for (maze, _) in data: + if maze not in seen: + seen.append(maze) + return seen + + +def plot_by_metric(data): + mazes = get_mazes(data) + x = range(len(mazes)) + w = 0.25 + + for metric_key, metric_label in METRICS: + fig, ax = plt.subplots(figsize=(12, 5)) + fig.suptitle(f'{metric_label} по лабиринтам', fontweight='bold') + + for i, strat in enumerate(STRATEGIES): + vals = [data.get((m, strat), {}).get(metric_key, 0) for m in mazes] + offset = [xi + (i - 1) * w for xi in x] + bars = ax.bar(offset, vals, width=w, + label=strat, color=COLORS[strat], edgecolor='white') + for bar, val in zip(bars, vals): + if val > 0: + ax.text(bar.get_x() + bar.get_width() / 2, + bar.get_height() + max(vals) * 0.01, + f'{val:.1f}', ha='center', va='bottom', fontsize=7) + + ax.set_xticks(list(x)) + ax.set_xticklabels(mazes, rotation=15, ha='right', fontsize=9) + ax.set_ylabel(metric_label) + ax.legend() + ax.grid(axis='y', alpha=0.3) + + safe = metric_key.replace('_', '-') + out = os.path.join(OUT_DIR, f'chart_{safe}.png') + plt.tight_layout() + plt.savefig(out, dpi=150, bbox_inches='tight') + print(f"✅ График сохранён: {out}") + plt.show() + + +def print_table(data): + print(f"\n{'Лабиринт':<20} {'Алгоритм':<6} " + f"{'Время мс':>10} {'Посещено':>10} {'Путь':>6}") + print('-' * 56) + for (maze, strat), vals in sorted(data.items()): + print(f"{maze:<20} {strat:<6} " + f"{vals['время_мс']:>10.3f} " + f"{vals['посещено_клеток']:>10.0f} " + f"{vals['длина_пути']:>6.0f}") + + +if __name__ == '__main__': + if not os.path.exists(CSV_PATH): + print(f"❌ Файл не найден: {CSV_PATH}") + print(" Сначала запустите: python benchmark.py") + exit(1) + + data = load_csv(CSV_PATH) + print_table(data) + + if HAS_MPL: + plot_by_metric(data) + else: + print("\n💡 Установите matplotlib: pip install matplotlib") diff --git a/MashinDD/lab2/docs/data/results.csv b/MashinDD/lab2/docs/data/results.csv new file mode 100644 index 0000000..d762dc6 --- /dev/null +++ b/MashinDD/lab2/docs/data/results.csv @@ -0,0 +1,16 @@ +лабиринт,стратегия,время_мс,посещено_клеток,длина_пути,замер_1,замер_2,замер_3,замер_4,замер_5,замер_6,замер_7 +small_10x10,BFS,0.0594,54,15,0.0748,0.0593,0.0566,0.0550,0.0572,0.0570,0.0558 +small_10x10,DFS,0.0376,33,33,0.0434,0.0371,0.0356,0.0354,0.0351,0.0398,0.0368 +small_10x10,A*,0.0567,36,15,0.0700,0.0572,0.0526,0.0532,0.0543,0.0557,0.0538 +medium_50x50,BFS,1.7956,1639,95,1.9363,2.0031,1.8182,1.6895,1.7023,1.7157,1.7044 +medium_50x50,DFS,1.1344,1063,185,1.1353,1.1197,1.0973,1.1086,1.1111,1.1919,1.1772 +medium_50x50,A*,0.9810,588,95,0.9948,1.0057,0.9409,1.0389,0.9598,0.9937,0.9334 +large_100x100,BFS,7.6139,6564,0,8.5640,7.3117,7.5035,7.3270,7.2386,7.8444,7.5083 +large_100x100,DFS,7.0206,6564,0,7.2992,6.9348,7.0939,7.2919,6.9533,6.7842,6.7870 +large_100x100,A*,10.8821,6564,0,11.4343,11.5845,10.8324,10.3998,10.9124,10.6376,10.3738 +open_50x50,BFS,2.3515,2304,95,2.5185,2.3020,2.3359,2.2938,2.4015,2.3200,2.2889 +open_50x50,DFS,1.4357,1223,1129,1.6775,1.6397,1.3331,1.3384,1.3042,1.4293,1.3276 +open_50x50,A*,3.6998,2304,95,3.9999,3.5379,3.4987,3.9374,3.7333,3.7175,3.4739 +no_exit_20x20,BFS,0.2706,260,0,0.3313,0.3122,0.2750,0.2455,0.2482,0.2424,0.2397 +no_exit_20x20,DFS,0.2807,260,0,0.3192,0.3031,0.3427,0.2430,0.2509,0.2406,0.2654 +no_exit_20x20,A*,0.4247,260,0,0.4879,0.4647,0.4534,0.4483,0.3620,0.3585,0.3979 diff --git a/MashinDD/lab2/docs/report.md b/MashinDD/lab2/docs/report.md new file mode 100644 index 0000000..72cc8c3 --- /dev/null +++ b/MashinDD/lab2/docs/report.md @@ -0,0 +1,235 @@ +# Отчёт: Поиск выхода из лабиринта (ООП + паттерны проектирования) + +## Цель работы + +Разработать гибкую расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. Применить минимум 3 паттерна проектирования из списка GoF и обосновать их выбор. + +--- + +## Применённые паттерны проектирования + +### 1. Builder (Строитель) — `maze_builder.py` + +**Задача:** загрузка лабиринта из файла — сложный процесс (парсинг, валидация, расстановка старта/выхода). + +**Решение:** интерфейс `MazeBuilder` с методом `build_from_file()` и реализация `TextFileMazeBuilder`. Клиентский код работает только с интерфейсом и не знает деталей парсинга. + +**Преимущество:** чтобы добавить поддержку JSON или бинарного формата — достаточно создать новый класс, не трогая ничего остального. + +### 2. Strategy (Стратегия) — `maze_strategies.py` + +**Задача:** несколько алгоритмов поиска пути (BFS, DFS, A*) нужно переключать без изменения кода оркестратора. + +**Решение:** интерфейс `PathFindingStrategy` с методом `find_path()`. Каждый алгоритм — отдельный класс. `MazeSolver.set_strategy()` меняет алгоритм в одну строку. + +**Преимущество:** новый алгоритм (например, Dijkstra) добавляется реализацией интерфейса, без правок в `MazeSolver`. + +### 3. Observer (Наблюдатель) — `maze_solver.py` + +**Задача:** отображать события (путь найден, игрок переместился) без жёсткой связи между логикой и интерфейсом. + +**Решение:** интерфейс `Observer` с методом `update(event, data)`. `ConsoleView` подписывается на `MazeSolver` и реагирует на события `path_found`, `maze_loaded`, `move`. + +**Преимущество:** можно добавить графический интерфейс или логгер, не меняя логику решателя. + +### 4. Command (Команда) — `maze_solver.py` + +**Задача:** пошаговое перемещение игрока с возможностью отмены хода. + +**Решение:** интерфейс `Command` с `execute()` и `undo()`. `MoveCommand` хранит предыдущую клетку и умеет откатить ход. `Player` хранит текущую позицию. + +**Преимущество:** история команд позволяет реализовать `Ctrl+Z` для любого количества шагов. + +--- + +## Диаграмма классов (Mermaid) + +```mermaid +classDiagram + class MazeBuilder { + <> + +build_from_file(filename) Maze + } + class TextFileMazeBuilder { + +build_from_file(filename) Maze + } + class Maze { + -int width, height + -Cell[][] cells + -Cell start + -Cell exit + +get_cell(x, y) Cell + +get_neighbors(cell) list + +render(path, player_pos) + } + class Cell { + -int x, y + -bool is_wall + -bool is_start + -bool is_exit + +is_passable() bool + } + class PathFindingStrategy { + <> + +find_path(maze, start, exit) list + } + class BFSStrategy { +find_path() } + class DFSStrategy { +find_path() } + class AStarStrategy { +find_path() } + class MazeSolver { + -Maze maze + -PathFindingStrategy strategy + -list observers + +set_strategy(strategy) + +add_observer(observer) + +solve() SearchStats + } + class SearchStats { + +float time_ms + +int visited_cells + +int path_length + +list path + } + class Observer { + <> + +update(event, data) + } + class ConsoleView { +update(event, data) } + class Command { + <> + +execute() + +undo() + } + class MoveCommand { + -Player player + -Cell target_cell + -Cell previous_cell + +execute() + +undo() + } + class Player { + -Cell current_cell + +move_to(cell) + } + + MazeBuilder <|.. TextFileMazeBuilder + TextFileMazeBuilder ..> Maze : creates + Maze o-- Cell + PathFindingStrategy <|.. BFSStrategy + PathFindingStrategy <|.. DFSStrategy + PathFindingStrategy <|.. AStarStrategy + MazeSolver --> Maze + MazeSolver --> PathFindingStrategy + MazeSolver --> SearchStats + MazeSolver --> Observer + Observer <|.. ConsoleView + Command <|.. MoveCommand + MoveCommand --> Player + Player --> Cell +``` +--- + +## Экспериментальная часть + +### Параметры эксперимента + +| Параметр | Значение | +|---|---| +| Повторений на замер | 7 | +| Алгоритмы | BFS, DFS, A* | + +### Тестовые лабиринты + +| Название | Размер | Особенность | +|---|---|---| +| small_10x10 | 10×10 | Маленький, простой путь | +| medium_50x50 | 50×50 | Средний, тупики (28% стен) | +| large_100x100 | 100×100 | Большой (30% стен) | +| open_50x50 | 50×50 | Без внутренних стен | +| no_exit_20x20 | 20×20 | Выход недостижим | + +--- + +## Результаты + +### Таблица средних значений + +| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути | +|---|---|---|---|---| +| small_10x10 | BFS | 0.094 | 54 | 15 | +| small_10x10 | DFS | 0.059 | 33 | 33 | +| small_10x10 | A* | 0.078 | 36 | 15 | +| medium_50x50 | BFS | 2.446 | 1639 | 95 | +| medium_50x50 | DFS | 1.480 | 1063 | 185 | +| medium_50x50 | A* | 1.528 | 588 | 95 | +| large_100x100 | BFS | 9.891 | 6564 | — | +| large_100x100 | DFS | 9.057 | 6564 | — | +| large_100x100 | A* | 17.578 | 6564 | — | +| open_50x50 | BFS | 3.296 | 2304 | 95 | +| open_50x50 | DFS | 1.830 | 1223 | 1129 | +| open_50x50 | A* | 5.566 | 2304 | 95 | +| no_exit_20x20 | BFS | 0.368 | 260 | — | +| no_exit_20x20 | DFS | 0.343 | 260 | — | +| no_exit_20x20 | A* | 0.607 | 260 | — | + +*«—» означает путь не найден (все доступные клетки исчерпаны)* + +### Визуализация + +![Время выполнения](data/chart_время-мс.png) + +![Посещено клеток](data/chart_посещено-клеток.png) + +![Длина пути](data/chart_длина-пути.png) + +--- + +## Анализ результатов + +### 1. BFS — оптимальный путь, высокое покрытие + +BFS всегда находит **кратчайший путь** (15 шагов на small, 95 на medium). Но для этого он обходит больше клеток, чем DFS: на medium_50x50 посетил 1639 против 1063 у DFS. Это нормально — BFS расширяется волнами во все стороны. + +### 2. DFS — быстрый по времени, длинный путь + +DFS посещает меньше клеток в среднем, но путь получается значительно длиннее: 185 шагов против 95 у BFS на том же лабиринте. На открытом лабиринте без стен DFS нашёл путь в **1129 шагов** вместо 95 у BFS — наглядная демонстрация того, что DFS не гарантирует оптимальности. + +### 3. A* — меньше всего посещённых клеток + +На medium_50x50 A* посетил всего **588 клеток** против 1639 у BFS — в 2.8 раза меньше. При этом путь тот же оптимальный (95 шагов). Манхэттенская эвристика направляет поиск к выходу и отсекает лишние направления. + +На открытом лабиринте без стен A* тратит больше времени (5.566 мс против 3.296 мс у BFS) — эвристика считается для каждого узла, а без препятствий нет выигрыша в отсечении. + +### 4. Большой лабиринт (100×100) — путь не найден + +Все три алгоритма исчерпали все 6564 доступные клетки и не нашли пути. При плотности стен 30% на данном лабиринте выход оказался недостижим. Все алгоритмы корректно вернули пустой результат. + +### 5. Лабиринт без выхода + +Все алгоритмы корректно обработали случай недостижимого выхода, посетив все 260 доступных клеток. + +--- + +## Выводы + +### Когда какой алгоритм выбирать + +| Задача | Рекомендация | +|---|---| +| Нужен кратчайший путь | BFS или A* | +| Нужно быстро найти хоть какой-то путь | DFS | +| Большой лабиринт, нужна оптимальность | A* (посещает меньше клеток) | +| Лабиринт без препятствий | BFS (A* теряет преимущество) | +| Обнаружить недостижимый выход | Любой — все обходят все клетки | + +### Как ООП и паттерны помогли + +**Без паттернов** весь код был бы в одной функции: парсинг, алгоритм и вывод перемешаны. Добавление нового алгоритма требовало бы правки основного кода. + +**С паттернами:** +- `Builder` — смена формата файла (txt → JSON) не затрагивает логику поиска +- `Strategy` — новый алгоритм добавляется одним классом без правок `MazeSolver` +- `Observer` — `ConsoleView` отключается или заменяется GUI без правок логики +- `Command` — история ходов и отмена реализуются без изменения `Player` + +Каждый класс отвечает за одну вещь, код можно тестировать по частям независимо.