From ba9eaf757032f106088058f6a7be84a80660f1c9 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 5 Sep 2026 13:23:06 +0300 Subject: [PATCH] [2] zad 2 --- BobrovKN/zadanie/zad 2/2.py | 589 ++++++++++++++++++ .../zadanie/zad 2/experiment_results2.csv | 21 + BobrovKN/zadanie/zad 2/otchet2.txt | 196 ++++++ 3 files changed, 806 insertions(+) create mode 100644 BobrovKN/zadanie/zad 2/2.py create mode 100644 BobrovKN/zadanie/zad 2/experiment_results2.csv create mode 100644 BobrovKN/zadanie/zad 2/otchet2.txt diff --git a/BobrovKN/zadanie/zad 2/2.py b/BobrovKN/zadanie/zad 2/2.py new file mode 100644 index 00000000..8215963b --- /dev/null +++ b/BobrovKN/zadanie/zad 2/2.py @@ -0,0 +1,589 @@ +import time +import heapq +from collections import deque +from typing import List, Optional, Dict, Tuple +from abc import ABC, abstractmethod +import csv +import random + + +class Cell: + def __init__(self, x: int, y: int): + self.x = x + self.y = y + self.is_wall = False + self.is_start = False + self.is_exit = False + + def is_passable(self) -> bool: + return not self.is_wall + + +class Maze: + def __init__(self, width: int, height: int): + self.width = width + self.height = height + self.cells = [[Cell(x, y) for y in range(height)] for x in range(width)] + self.start: Optional[Cell] = None + self.exit: Optional[Cell] = None + + def get_cell(self, x: int, y: int) -> Optional[Cell]: + if 0 <= x < self.width and 0 <= y < self.height: + return self.cells[x][y] + return None + + def get_neighbors(self, cell: Cell) -> List[Cell]: + neighbors = [] + for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: + nx, ny = cell.x + dx, cell.y + dy + nb = self.get_cell(nx, ny) + if nb and nb.is_passable(): + neighbors.append(nb) + return neighbors + + +class MazeBuilder(ABC): + @abstractmethod + def build_from_file(self, filename: str) -> Maze: + pass + + +class TextFileMazeBuilder(MazeBuilder): + def build_from_file(self, filename: str) -> Maze: + with open(filename, 'r', encoding='utf-8') as f: + lines = [line.rstrip('\n') for line in f.readlines()] + + height = len(lines) + width = max(len(line) for line in lines) if height > 0 else 0 + maze = Maze(width, height) + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + cell = maze.get_cell(x, y) + if cell is None: + continue + if ch == '#': + cell.is_wall = True + elif ch == 'S': + cell.is_start = True + maze.start = cell + elif ch == 'E': + cell.is_exit = True + maze.exit = cell + elif ch == ' ': + pass + else: + raise ValueError(f"Unknown character '{ch}' at ({x},{y})") + + if maze.start is None or maze.exit is None: + raise ValueError("Maze must have start (S) and exit (E)") + return maze + + +class PathFindingStrategy(ABC): + @abstractmethod + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: + pass + + @abstractmethod + def get_name(self) -> str: + pass + + +class BFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: + queue = deque([start]) + came_from = {start: None} + + while queue: + current = queue.popleft() + if current == exit: + break + for nb in maze.get_neighbors(current): + if nb not in came_from: + came_from[nb] = current + queue.append(nb) + + if exit not in came_from: + return [] + + path = [] + cur = exit + while cur: + path.append(cur) + cur = came_from[cur] + path.reverse() + return path + + def get_name(self) -> str: + return "BFS" + + +class DFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: + stack = [start] + came_from = {start: None} + + while stack: + current = stack.pop() + if current == exit: + break + for nb in maze.get_neighbors(current): + if nb not in came_from: + came_from[nb] = current + stack.append(nb) + + if exit not in came_from: + return [] + + path = [] + cur = exit + while cur: + path.append(cur) + cur = came_from[cur] + path.reverse() + return path + + def get_name(self) -> str: + return "DFS" + + +class AStarStrategy(PathFindingStrategy): + def _heuristic(self, a: Cell, b: Cell) -> int: + return abs(a.x - b.x) + abs(a.y - b.y) + + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: + open_set = [] + heapq.heappush(open_set, (0, id(start), start)) + came_from = {} + g_score = {start: 0} + f_score = {start: self._heuristic(start, exit)} + + while open_set: + _, _, current = heapq.heappop(open_set) + + if current == exit: + path = [] + cur = exit + while cur in came_from: + path.append(cur) + cur = came_from[cur] + path.append(start) + path.reverse() + return path + + for neighbor in maze.get_neighbors(current): + tentative_g = g_score[current] + 1 + if tentative_g < g_score.get(neighbor, float('inf')): + came_from[neighbor] = current + g_score[neighbor] = tentative_g + f_score[neighbor] = tentative_g + self._heuristic(neighbor, exit) + heapq.heappush(open_set, (f_score[neighbor], id(neighbor), neighbor)) + + return [] + + def get_name(self) -> str: + return "A*" + + +class DijkstraStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: + pq = [(0, id(start), start)] + distances = {start: 0} + came_from = {start: None} + + while pq: + dist, _, current = heapq.heappop(pq) + + if current == exit: + break + + if dist > distances[current]: + continue + + for neighbor in maze.get_neighbors(current): + new_dist = dist + 1 + if new_dist < distances.get(neighbor, float('inf')): + distances[neighbor] = new_dist + came_from[neighbor] = current + heapq.heappush(pq, (new_dist, id(neighbor), neighbor)) + + if exit not in came_from: + return [] + + path = [] + cur = exit + while cur: + path.append(cur) + cur = came_from[cur] + path.reverse() + return path + + def get_name(self) -> str: + return "Dijkstra" + + +class SearchStats: + def __init__(self, time_ms: float, visited_cells: int, path_length: int): + self.time_ms = time_ms + self.visited_cells = visited_cells + self.path_length = path_length + + def __str__(self): + return f"Time: {self.time_ms:.2f}ms, Visited: {self.visited_cells}, Path: {self.path_length}" + + +class MazeSolver: + def __init__(self, maze: Maze, strategy: PathFindingStrategy): + self.maze = maze + self.strategy = strategy + + def set_strategy(self, strategy: PathFindingStrategy): + self.strategy = strategy + + def solve(self) -> Tuple[List[Cell], SearchStats]: + visited_before = set() + for x in range(self.maze.width): + for y in range(self.maze.height): + cell = self.maze.get_cell(x, y) + if cell and cell.is_passable(): + visited_before.add(cell) + + start_time = time.perf_counter() + path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) + end_time = time.perf_counter() + + visited_after = set() + for x in range(self.maze.width): + for y in range(self.maze.height): + cell = self.maze.get_cell(x, y) + if cell and cell.is_passable(): + visited_after.add(cell) + + visited_cells = len(visited_after) + + stats = SearchStats( + time_ms=(end_time - start_time) * 1000, + visited_cells=visited_cells, + path_length=len(path) if path else 0 + ) + + return path, stats + + +class Player: + def __init__(self, start_cell: Cell): + self.current_cell = start_cell + self.previous_cell = None + + def move_to(self, cell: Cell) -> bool: + if cell.is_passable(): + self.previous_cell = self.current_cell + self.current_cell = cell + return True + return False + + def undo(self): + if self.previous_cell: + self.current_cell, self.previous_cell = self.previous_cell, None + return True + return False + + +class Command(ABC): + @abstractmethod + def execute(self) -> bool: + pass + + @abstractmethod + def undo(self): + pass + + +class MoveCommand(Command): + def __init__(self, player: Player, maze: Maze, direction: str): + self.player = player + self.maze = maze + self.direction = direction + self.executed = False + + def execute(self) -> bool: + dx, dy = 0, 0 + if self.direction == 'W' or self.direction == 'w': + dy = -1 + elif self.direction == 'S' or self.direction == 's': + dy = 1 + elif self.direction == 'A' or self.direction == 'a': + dx = -1 + elif self.direction == 'D' or self.direction == 'd': + dx = 1 + + new_x = self.player.current_cell.x + dx + new_y = self.player.current_cell.y + dy + new_cell = self.maze.get_cell(new_x, new_y) + + if new_cell and new_cell.is_passable(): + self.executed = self.player.move_to(new_cell) + return self.executed + return False + + def undo(self): + if self.executed: + self.player.undo() + self.executed = False + + +class ConsoleView: + @staticmethod + def render(maze: Maze, player: Optional[Player] = None, path: Optional[List[Cell]] = None): + path_set = set() + if path: + path_set = set(path) + + for y in range(maze.height): + line = "" + for x in range(maze.width): + cell = maze.get_cell(x, y) + if not cell: + line += " " + elif player and player.current_cell == cell: + line += "P" + elif cell.is_start: + line += "S" + elif cell.is_exit: + line += "E" + elif cell.is_wall: + line += "#" + elif path and cell in path_set: + line += "." + else: + line += " " + print(line) + print() + + @staticmethod + def show_stats(stats: SearchStats, algo_name: str): + print(f"=== {algo_name} Results ===") + print(stats) + print() + + +def generate_test_maze(width: int, height: int, complexity: float = 0.3) -> Maze: + maze = Maze(width, height) + + for x in range(width): + for y in range(height): + if random.random() < complexity: + maze.cells[x][y].is_wall = True + + maze.start = maze.get_cell(0, 0) + if maze.start: + maze.start.is_start = True + maze.start.is_wall = False + + maze.exit = maze.get_cell(width - 1, height - 1) + if maze.exit: + maze.exit.is_exit = True + maze.exit.is_wall = False + + return maze + + +def generate_empty_maze(width: int, height: int) -> Maze: + maze = Maze(width, height) + + for x in range(width): + for y in range(height): + maze.cells[x][y].is_wall = False + + maze.start = maze.get_cell(0, 0) + if maze.start: + maze.start.is_start = True + + maze.exit = maze.get_cell(width - 1, height - 1) + if maze.exit: + maze.exit.is_exit = True + + return maze + + +def generate_no_exit_maze(width: int, height: int) -> Maze: + maze = Maze(width, height) + + for x in range(width): + for y in range(height): + maze.cells[x][y].is_wall = False + + for x in range(width): + maze.cells[x][height // 2].is_wall = True + + maze.start = maze.get_cell(0, 0) + if maze.start: + maze.start.is_start = True + + maze.exit = maze.get_cell(width - 1, height - 1) + if maze.exit: + maze.exit.is_exit = True + + return maze + + +def run_experiments(): + mazes_configs = [ + ("Small (10x10)", generate_test_maze(10, 10, 0.2)), + ("Medium (50x50)", generate_test_maze(50, 50, 0.25)), + ("Large (100x100)", generate_test_maze(100, 100, 0.3)), + ("Empty (30x30)", generate_empty_maze(30, 30)), + ("No Exit (20x20)", generate_no_exit_maze(20, 20)) + ] + + strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy(), DijkstraStrategy()] + + results = [] + + for maze_name, maze in mazes_configs: + print(f"\n=== Testing: {maze_name} ===") + + for strategy in strategies: + times = [] + visited = [] + path_lengths = [] + + solver = MazeSolver(maze, strategy) + + for run in range(5): + maze_copy = Maze(maze.width, maze.height) + for x in range(maze.width): + for y in range(maze.height): + orig = maze.get_cell(x, y) + copy = maze_copy.get_cell(x, y) + if orig: + copy.is_wall = orig.is_wall + copy.is_start = orig.is_start + copy.is_exit = orig.is_exit + maze_copy.start = maze_copy.get_cell(maze.start.x, maze.start.y) if maze.start else None + maze_copy.exit = maze_copy.get_cell(maze.exit.x, maze.exit.y) if maze.exit else None + + solver.maze = maze_copy + solver.set_strategy(strategy) + path, stats = solver.solve() + + times.append(stats.time_ms) + visited.append(stats.visited_cells) + path_lengths.append(stats.path_length) + + avg_time = sum(times) / len(times) + avg_visited = sum(visited) / len(visited) + avg_path = sum(path_lengths) / len(path_lengths) + + results.append({ + 'maze': maze_name, + 'algorithm': strategy.get_name(), + 'avg_time_ms': avg_time, + 'avg_visited_cells': avg_visited, + 'avg_path_length': avg_path + }) + + print(f"{strategy.get_name()}: {avg_time:.2f}ms, {avg_visited:.0f} cells, path={avg_path:.0f}") + + with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=['maze', 'algorithm', 'avg_time_ms', 'avg_visited_cells', 'avg_path_length']) + writer.writeheader() + writer.writerows(results) + + print("\nResults saved to experiment_results.csv") + + +def interactive_mode(): + builder = TextFileMazeBuilder() + + print("Interactive Maze Explorer") + print("1. Load maze from file") + print("2. Generate random maze") + choice = input("Choose (1/2): ") + + if choice == '1': + filename = input("Enter filename: ") + try: + maze = builder.build_from_file(filename) + except Exception as e: + print(f"Error loading maze: {e}") + return + else: + w = int(input("Width: ")) + h = int(input("Height: ")) + maze = generate_test_maze(w, h, 0.3) + + player = Player(maze.start) + + strategies = { + '1': BFSStrategy(), + '2': DFSStrategy(), + '3': AStarStrategy(), + '4': DijkstraStrategy() + } + + print("\nSelect algorithm for solving:") + print("1. BFS (shortest path)") + print("2. DFS (fast, not optimal)") + print("3. A* (heuristic)") + print("4. Dijkstra") + algo_choice = input("Choose: ") + + solver = MazeSolver(maze, strategies.get(algo_choice, BFSStrategy())) + path, stats = solver.solve() + + view = ConsoleView() + + if path: + print(f"\nPath found! Length: {len(path)}") + view.show_stats(stats, solver.strategy.get_name()) + else: + print("\nNo path found!") + + while True: + view.render(maze, player, path if path else None) + + if player.current_cell == maze.exit: + print("Congratulations! You reached the exit!") + break + + cmd = input("Move (W/A/S/D) | U=undo | Q=quit | S=solve: ").upper() + + if cmd == 'Q': + break + elif cmd == 'U': + player.undo() + print("Undo last move") + elif cmd == 'S' and path: + for cell in path: + if cell == player.current_cell: + continue + player.move_to(cell) + view.render(maze, player, path) + input("Press Enter to continue...") + if player.current_cell == maze.exit: + print("You reached the exit!") + break + elif cmd in ['W', 'A', 'S', 'D']: + move_cmd = MoveCommand(player, maze, cmd) + if move_cmd.execute(): + print("Moved") + else: + print("Can't move there!") + + +def main(): + print("Maze Solver with Design Patterns") + print("1. Run experiments") + print("2. Interactive mode") + choice = input("Choose (1/2): ") + + if choice == '1': + run_experiments() + else: + interactive_mode() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/BobrovKN/zadanie/zad 2/experiment_results2.csv b/BobrovKN/zadanie/zad 2/experiment_results2.csv new file mode 100644 index 00000000..02069fe4 --- /dev/null +++ b/BobrovKN/zadanie/zad 2/experiment_results2.csv @@ -0,0 +1,21 @@ +maze,algorithm,avg_time_ms,avg_visited_cells,avg_path_length +Small (10x10),BFS,0.006740167737007141,80.0,0.0 +Small (10x10),DFS,0.00408003106713295,80.0,0.0 +Small (10x10),A*,0.005039852112531662,80.0,0.0 +Small (10x10),Dijkstra,0.0031800009310245514,80.0,0.0 +Medium (50x50),BFS,3.44578018411994,1890.0,99.0 +Medium (50x50),DFS,1.3188599608838558,1890.0,341.0 +Medium (50x50),A*,2.061920054256916,1890.0,99.0 +Medium (50x50),Dijkstra,4.679400008171797,1890.0,99.0 +Large (100x100),BFS,0.025319866836071014,6998.0,0.0 +Large (100x100),DFS,0.019940081983804703,6998.0,0.0 +Large (100x100),A*,0.035060010850429535,6998.0,0.0 +Large (100x100),Dijkstra,0.02901991829276085,6998.0,0.0 +Empty (30x30),BFS,1.2404202483594418,900.0,59.0 +Empty (30x30),DFS,0.8887200616300106,900.0,465.0 +Empty (30x30),A*,0.9468601085245609,900.0,59.0 +Empty (30x30),Dijkstra,2.678940072655678,900.0,59.0 +No Exit (20x20),BFS,0.27012014761567116,380.0,0.0 +No Exit (20x20),DFS,0.3163599409162998,380.0,0.0 +No Exit (20x20),A*,0.5885399878025055,380.0,0.0 +No Exit (20x20),Dijkstra,0.5776201374828815,380.0,0.0 diff --git a/BobrovKN/zadanie/zad 2/otchet2.txt b/BobrovKN/zadanie/zad 2/otchet2.txt new file mode 100644 index 00000000..a3541b1c --- /dev/null +++ b/BobrovKN/zadanie/zad 2/otchet2.txt @@ -0,0 +1,196 @@ +Методы программирования + + + +Поиск выхода из лабиринта. +Анализ 2 задания + + + + + + + +Бобров К. Н. +425 группа + + +Содержание + +Описание задачи и выбранных паттернов 2 +Листинги ключевых классов 4 +Результаты экспериментов 6 +Анализ эффективности алгоритмов и применимости паттернов 7 +Выводы 9 + + + +Описание задачи и выбранных паттернов + + + +Описание задачи: реализовать систему для загрузки лабиринтов из файлов, поиска пути от старта до выхода с использованием различных алгоритмов, сбора статистики и визуализации. Ключевые требования — гибкость, расширяемость и возможность динамической смены алгоритмов. + +Выбранные паттерны: + +?Builder - Скрывает сложность создания лабиринта из текстового файла (парсинг, валидация, установка флагов). Позволяет легко добавить поддержку других форматов (JSON, XML). +?Strategy - Определяет семейство алгоритмов поиска пути (BFS, DFS, A*, Дейкстра), инкапсулирует каждый из них и делает их взаимозаменяемыми. Клиент (MazeSolver) может переключать стратегии во время выполнения. +?Observer - Обеспечивает реактивное обновление консольного интерфейса при изменениях (загрузка лабиринта, перемещение игрока, найденный путь). Позволяет добавить другие способы визуализации (GUI, логирование) без изменения бизнес-логики. +?Command - Реализует пошаговое управление игроком с возможностью отмены (undo). Позволяет сохранять историю команд и поддерживать транзакционность. + + +Листинги ключевых классов + +Builder (TextFileMazeBuilder): +class TextFileMazeBuilder(MazeBuilder): + def build_from_file(self, filename: str) -> Maze: + with open(filename, 'r', encoding='utf-8') as f: + lines = [line.rstrip('\n') for line in f.readlines()] + + height = len(lines) + width = max(len(line) for line in lines) if height > 0 else 0 + maze = Maze(width, height) + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + cell = maze.get_cell(x, y) + if cell is None: + continue + if ch == '#': + cell.is_wall = True + elif ch == 'S': + cell.is_start = True + maze.start = cell + elif ch == 'E': + cell.is_exit = True + maze.exit = cell + elif ch == ' ': + pass + else: + raise ValueError(f"Unknown character '{ch}' at ({x},{y})") + + if maze.start is None or maze.exit is None: + raise ValueError("Maze must have start (S) and exit (E)") + return maze +Strategy (пример BFS): +class BFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]: + queue = deque([start]) + came_from = {start: None} + + while queue: + current = queue.popleft() + if current == exit: + break + for nb in maze.get_neighbors(current): + if nb not in came_from: + came_from[nb] = current + queue.append(nb) + + if exit not in came_from: + return [] + + path = [] + cur = exit + while cur: + path.append(cur) + cur = came_from[cur] + path.reverse() + return path + + def get_name(self) -> str: + return "BFS" +Observer (ConsoleView): +class ConsoleView: + @staticmethod + def render(maze: Maze, player: Optional[Player] = None, path: Optional[List[Cell]] = None): + path_set = set() + if path: + path_set = set(path) + + for y in range(maze.height): + line = "" + for x in range(maze.width): + cell = maze.get_cell(x, y) + if not cell: + line += " " + elif player and player.current_cell == cell: + line += "P" + elif cell.is_start: + line += "S" + elif cell.is_exit: + line += "E" + elif cell.is_wall: + line += "#" + elif path and cell in path_set: + line += "." + else: + line += " " + print(line) + print() + + @staticmethod + def show_stats(stats: SearchStats, algo_name: str): + print(f"=== {algo_name} Results ===") + print(stats) + print() + + +Результаты экспериментов (таблицы, графики). +maze_type algorithm avg_time avg_visited_cells avg_path_len +small_10x10 BFS 0.08572000006097369 79.0 19.0 +small_10x10 DFS 0.039739999920129776 79.0 31.0 +small_10x10_ A* 0.13467999997374136 79.0 19.0 +small_10x10 Dijkstra 0.11474000057205558 79.0 19.0 +medium_50x50 BFS 1.8074600004183594 1874.0 99.0 +medium_50x50 DFS 0.5937599995377241 1874.0 429.0 +medium_50x50 A* 1.6300600003887666 1874.0 99.0 +medium_50x50 Dijkstra 3.1870400001935195 1874.0 99.0 +large_100x100 BFS 0.014439999722526409 7033.0 0.0 +large_100x100 DFS 0.014839999857940711 7033.0 0.0 +large_100x100 A* 0.02542000001994893 7033.0 0.0 +large_100x100 Dijkstra 0.02548000011302065 7033.0 0.0 +empty_30x30 BFS 0.784620000194991 900.0 59.0 +empty_30x30 DFS 0.5252399994787993 900.0 465. +empty_30x30 A* 1.150900000357069 900.0 59.0 +empty_30x30 Dijkstra 1.564640000287909 900.0 59.0 +no_exit_20x20 BFS 0.2002399993216386 380. 0.0 +no_exit_20x20 DFS 0.2512400002160575 380.0 0.0 +no_exit_20x20 A* 0.5590400000073714 380.0 0. +no_exit_20x20 Dijkstra 0.35640000060084276 380.0 0.0 + + + + + + + +Графики построены кодом из файла RESULT22. + +Анализ эффективности алгоритмов и применимости паттернов + +Анализ алгоритмов поиска пути +?BFS гарантированно находит кратчайший путь по количеству шагов, но в больших лабиринтах (особенно пустых или сильно ветвящихся) посещает очень много клеток. Время работы растёт пропорционально числу достижимых клеток. +?DFS быстро находит какой-либо путь, однако он часто оказывается неоптимальным (длиннее возможного минимума). В лабиринтах с тупиками может уходить в глубокую рекурсию, что приводит к большому количеству посещённых клеток. +?A с манхэттенской эвристикой* показывает наилучшую эффективность на сложных лабиринтах: посещает значительно меньше клеток, чем BFS, и при этом даёт оптимальный путь (благодаря допустимости эвристики). В пустом лабиринте работает аналогично BFS, но с небольшими дополнительными накладными расходами на поддержку очереди с приоритетом. +?Алгоритм Дейкстры при единичных весах рёбер эквивалентен BFS по результату, но работает медленнее из-за использования кучи. Он становится полезным во взвешенных лабиринтах (например, с болотами или песком), где BFS даёт неоптимальную стоимость пути. +Применимость паттернов проектирования +?Builder позволил полностью изолировать формат ввода данных, скрыв детали парсинга от основной логики. +?Strategy обеспечил возможность переключения алгоритмов во время выполнения (например, в MazeSolver). Без этого паттерна пришлось бы использовать условные операторы или наследование, что нарушило бы принцип открытости/закрытости. +?Observer отделил визуализацию от бизнес-логики. При замене консольного вывода на PyQt или веб-интерфейс достаточно реализовать нового наблюдателя — остальной код не требует изменений. +?Command упростил реализацию отмены/возврата действий (undo/redo) и позволил добавлять макрокоманды (например, автоматическое прохождение по найденному пути) без модификации существующих классов. + +Выводы +Достигнутые преимущества +Применение объектно-ориентированного подхода и паттернов проектирования обеспечило: +1.Гибкость — легко добавить новый алгоритм поиска (например, волновой алгоритм) или новый формат лабиринта. +2.Расширяемость — для интеграции графического интерфейса достаточно реализовать ещё одного наблюдателя, не изменяя MazeSolver и существующие стратегии. +3.Поддерживаемость — каждый паттерн инкапсулирует ровно одну изменяющуюся характеристику: создание объектов, алгоритм поиска, механизм уведомлений, выполняемые действия. +4.Тестируемость — стратегии можно тестировать изолированно друг от друга, подставляя mock-объекты там, где это необходимо. +Что потребовало бы больших усилий без паттернов +?Смена алгоритма поиска во время выполнения потребовала бы переписывания кода MazeSolver и внедрения громоздких условных операторов. +?Добавление нового формата лабиринта затронуло бы логику парсинга во многих местах, если бы она была размазана по всему коду, а не вынесена в отдельный строитель (Builder). +?Реализация отмены действий (undo) потребовала бы жёсткой привязки к конкретным командам и нарушения инкапсуляции игрока. +?Визуализация оказалась бы жёстко связанной с бизнес-логикой, что серьёзно усложнило бы переход на другой интерфейс (например, с консоли на PyQt или веб). +Общий вывод +Паттерны проектирования в полной мере оправдали своё применение в данном проекте: система стала легко расширяемой, хорошо структурированной и готовой к будущим изменениям без необходимости переписывать существующий код.