diff --git a/SobolevNS/docs/data/task2_maze/README.md b/SobolevNS/docs/data/task2_maze/README.md new file mode 100644 index 0000000..57aed7c --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/README.md @@ -0,0 +1,33 @@ +# Задание 2. Поиск выхода из лабиринта (паттерны GoF) + +Применены 4 паттерна: **Builder**, **Strategy**, **Observer**, **Command**. + +## Как запустить + +```bash +# 1) сгенерировать тестовые лабиринты +python3 generate_mazes.py +python3 generate_weighted_choice.py + +# 2) демонстрация всех паттернов на маленьком лабиринте +python3 demo.py + +# 3) эксперимент: 7 запусков × 4 стратегии × 7 лабиринтов +python3 experiment.py +# результат -> docs/data/results.csv + +# 4) графики +python3 plot_results.py +# результат -> docs/data/plots/*.png +``` + +## Формат лабиринта (текстовый) + +| Символ | Что означает | +| --- | --- | +| `#` | стена | +| ` ` (пробел) или `.` | проход, вес 1 (асфальт) | +| `,` | проход, вес 2 (песок) | +| `~` | проход, вес 3 (болото) | +| `S` | старт (ровно один) | +| `E` | выход (ровно один) | diff --git a/SobolevNS/docs/data/task2_maze/demo.py b/SobolevNS/docs/data/task2_maze/demo.py new file mode 100644 index 0000000..623e5dc --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/demo.py @@ -0,0 +1,54 @@ +""" +demo.py - короткая демонстрация всех паттернов на маленьком лабиринте. +""" + +from maze_solver import ( + TextFileMazeBuilder, MazeSolver, ConsoleView, + BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy, + Player, MoveCommand, CommandHistory, +) + + +def main(): + print("=== Builder: загружаем small_10x10.txt ===") + builder = TextFileMazeBuilder() + maze = builder.build_from_file("mazes/small_10x10.txt") + + view = ConsoleView(verbose=True) + view.update({"type": "maze_loaded", "maze": maze}) + + print("\nСам лабиринт:") + print(maze.render_text()) + + print("\n=== Strategy: пробуем все 4 алгоритма ===") + solver = MazeSolver(maze) + solver.attach(view) + + for cls in (BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy): + solver.set_strategy(cls()) + stats = solver.solve() + print(f"--- {stats['strategy']} путь длиной {stats['path_length']} ---") + print(maze.render_text(path=stats['path'])) + print() + + print("=== Command: пройдёмся вручную и сделаем undo ===") + player = Player(maze.start) + history = CommandHistory() + print(f"стартовая позиция: ({player.x},{player.y})") + + # Несколько шагов вправо + for d in "DDDD": + ok = history.do(MoveCommand(maze, player, d)) + print(f" move {d}: {'ok' if ok else 'blocked'} -> ({player.x},{player.y})") + + print("Откатываем 2 хода (undo, undo):") + history.undo() + history.undo() + print(f" теперь игрок в ({player.x},{player.y})") + + print("\nЛабиринт с игроком:") + print(maze.render_text(player=player)) + + +if __name__ == "__main__": + main() diff --git a/SobolevNS/docs/data/task2_maze/docs/data/plots/path_compare.png b/SobolevNS/docs/data/task2_maze/docs/data/plots/path_compare.png new file mode 100644 index 0000000..74f7520 Binary files /dev/null and b/SobolevNS/docs/data/task2_maze/docs/data/plots/path_compare.png differ diff --git a/SobolevNS/docs/data/task2_maze/docs/data/plots/time_compare.png b/SobolevNS/docs/data/task2_maze/docs/data/plots/time_compare.png new file mode 100644 index 0000000..475729b Binary files /dev/null and b/SobolevNS/docs/data/task2_maze/docs/data/plots/time_compare.png differ diff --git a/SobolevNS/docs/data/task2_maze/docs/data/plots/visited_compare.png b/SobolevNS/docs/data/task2_maze/docs/data/plots/visited_compare.png new file mode 100644 index 0000000..10b33d7 Binary files /dev/null and b/SobolevNS/docs/data/task2_maze/docs/data/plots/visited_compare.png differ diff --git a/SobolevNS/docs/data/task2_maze/docs/data/plots/weighted_choice_compare.png b/SobolevNS/docs/data/task2_maze/docs/data/plots/weighted_choice_compare.png new file mode 100644 index 0000000..6b099f9 Binary files /dev/null and b/SobolevNS/docs/data/task2_maze/docs/data/plots/weighted_choice_compare.png differ diff --git a/SobolevNS/docs/data/task2_maze/docs/data/results.csv b/SobolevNS/docs/data/task2_maze/docs/data/results.csv new file mode 100644 index 0000000..264f0b4 --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/docs/data/results.csv @@ -0,0 +1,228 @@ +лабиринт,стратегия,trial,время_мс,посещено_клеток,длина_пути,стоимость_пути +small_10x10,BFS,1,0.0454,34,16,16 +small_10x10,BFS,2,0.0314,34,16,16 +small_10x10,BFS,3,0.0286,34,16,16 +small_10x10,BFS,4,0.0275,34,16,16 +small_10x10,BFS,5,0.0270,34,16,16 +small_10x10,BFS,6,0.0264,34,16,16 +small_10x10,BFS,7,0.0279,34,16,16 +small_10x10,DFS,1,0.0167,18,16,16 +small_10x10,DFS,2,0.0146,18,16,16 +small_10x10,DFS,3,0.0138,18,16,16 +small_10x10,DFS,4,0.0132,18,16,16 +small_10x10,DFS,5,0.0138,18,16,16 +small_10x10,DFS,6,0.0133,18,16,16 +small_10x10,DFS,7,0.0138,18,16,16 +small_10x10,A*,1,0.0585,27,16,16 +small_10x10,A*,2,0.0514,27,16,16 +small_10x10,A*,3,0.0386,27,16,16 +small_10x10,A*,4,0.0366,27,16,16 +small_10x10,A*,5,0.0367,27,16,16 +small_10x10,A*,6,0.0367,27,16,16 +small_10x10,A*,7,0.0356,27,16,16 +small_10x10,Dijkstra,1,0.0467,33,16,16 +small_10x10,Dijkstra,2,0.0409,33,16,16 +small_10x10,Dijkstra,3,0.0395,33,16,16 +small_10x10,Dijkstra,4,0.0396,33,16,16 +small_10x10,Dijkstra,5,0.0642,33,16,16 +small_10x10,Dijkstra,6,0.0404,33,16,16 +small_10x10,Dijkstra,7,0.0392,33,16,16 +medium_51x51,BFS,1,0.5159,524,353,353 +medium_51x51,BFS,2,0.5299,524,353,353 +medium_51x51,BFS,3,0.5232,524,353,353 +medium_51x51,BFS,4,0.4525,524,353,353 +medium_51x51,BFS,5,0.4667,524,353,353 +medium_51x51,BFS,6,0.4594,524,353,353 +medium_51x51,BFS,7,0.4886,524,353,353 +medium_51x51,DFS,1,0.3356,379,353,353 +medium_51x51,DFS,2,0.3270,379,353,353 +medium_51x51,DFS,3,0.3471,379,353,353 +medium_51x51,DFS,4,0.3235,379,353,353 +medium_51x51,DFS,5,0.3309,379,353,353 +medium_51x51,DFS,6,0.3856,379,353,353 +medium_51x51,DFS,7,0.3248,379,353,353 +medium_51x51,A*,1,0.8707,421,353,353 +medium_51x51,A*,2,0.6813,421,353,353 +medium_51x51,A*,3,0.6357,421,353,353 +medium_51x51,A*,4,0.6464,421,353,353 +medium_51x51,A*,5,0.6520,421,353,353 +medium_51x51,A*,6,0.6231,421,353,353 +medium_51x51,A*,7,0.6365,421,353,353 +medium_51x51,Dijkstra,1,0.7634,523,353,353 +medium_51x51,Dijkstra,2,0.6893,523,353,353 +medium_51x51,Dijkstra,3,0.6817,523,353,353 +medium_51x51,Dijkstra,4,0.6965,523,353,353 +medium_51x51,Dijkstra,5,0.6920,523,353,353 +medium_51x51,Dijkstra,6,0.6702,523,353,353 +medium_51x51,Dijkstra,7,0.7281,523,353,353 +large_101x101,BFS,1,3.2679,2143,1265,1265 +large_101x101,BFS,2,1.9302,2143,1265,1265 +large_101x101,BFS,3,1.9559,2143,1265,1265 +large_101x101,BFS,4,1.9057,2143,1265,1265 +large_101x101,BFS,5,1.8770,2143,1265,1265 +large_101x101,BFS,6,1.8828,2143,1265,1265 +large_101x101,BFS,7,1.9345,2143,1265,1265 +large_101x101,DFS,1,1.2758,1443,1265,1265 +large_101x101,DFS,2,1.3043,1443,1265,1265 +large_101x101,DFS,3,1.2613,1443,1265,1265 +large_101x101,DFS,4,1.2846,1443,1265,1265 +large_101x101,DFS,5,1.3566,1443,1265,1265 +large_101x101,DFS,6,1.3296,1443,1265,1265 +large_101x101,DFS,7,1.2501,1443,1265,1265 +large_101x101,A*,1,3.2760,1831,1265,1265 +large_101x101,A*,2,3.3353,1831,1265,1265 +large_101x101,A*,3,4.1894,1831,1265,1265 +large_101x101,A*,4,4.6809,1831,1265,1265 +large_101x101,A*,5,3.4026,1831,1265,1265 +large_101x101,A*,6,3.1036,1831,1265,1265 +large_101x101,A*,7,3.2912,1831,1265,1265 +large_101x101,Dijkstra,1,3.4403,2139,1265,1265 +large_101x101,Dijkstra,2,3.3500,2139,1265,1265 +large_101x101,Dijkstra,3,3.4201,2139,1265,1265 +large_101x101,Dijkstra,4,3.2253,2139,1265,1265 +large_101x101,Dijkstra,5,5.0122,2139,1265,1265 +large_101x101,Dijkstra,6,3.3146,2139,1265,1265 +large_101x101,Dijkstra,7,3.2323,2139,1265,1265 +empty_30x30,BFS,1,0.8417,784,55,55 +empty_30x30,BFS,2,0.8160,784,55,55 +empty_30x30,BFS,3,0.7701,784,55,55 +empty_30x30,BFS,4,0.7609,784,55,55 +empty_30x30,BFS,5,0.7931,784,55,55 +empty_30x30,BFS,6,0.7647,784,55,55 +empty_30x30,BFS,7,0.8047,784,55,55 +empty_30x30,DFS,1,0.5067,784,379,379 +empty_30x30,DFS,2,0.6133,784,379,379 +empty_30x30,DFS,3,0.8051,784,379,379 +empty_30x30,DFS,4,0.4703,784,379,379 +empty_30x30,DFS,5,0.8029,784,379,379 +empty_30x30,DFS,6,0.5463,784,379,379 +empty_30x30,DFS,7,0.4602,784,379,379 +empty_30x30,A*,1,1.5117,784,55,55 +empty_30x30,A*,2,1.4866,784,55,55 +empty_30x30,A*,3,1.5878,784,55,55 +empty_30x30,A*,4,1.8756,784,55,55 +empty_30x30,A*,5,1.4943,784,55,55 +empty_30x30,A*,6,2.0146,784,55,55 +empty_30x30,A*,7,1.5262,784,55,55 +empty_30x30,Dijkstra,1,1.2824,784,55,55 +empty_30x30,Dijkstra,2,1.2897,784,55,55 +empty_30x30,Dijkstra,3,1.3428,784,55,55 +empty_30x30,Dijkstra,4,1.3181,784,55,55 +empty_30x30,Dijkstra,5,1.2785,784,55,55 +empty_30x30,Dijkstra,6,1.3634,784,55,55 +empty_30x30,Dijkstra,7,1.2709,784,55,55 +nopath_15x15,BFS,1,0.1595,165,0,0 +nopath_15x15,BFS,2,0.1705,165,0,0 +nopath_15x15,BFS,3,0.1489,165,0,0 +nopath_15x15,BFS,4,0.1461,165,0,0 +nopath_15x15,BFS,5,0.1972,165,0,0 +nopath_15x15,BFS,6,0.1461,165,0,0 +nopath_15x15,BFS,7,0.1436,165,0,0 +nopath_15x15,DFS,1,0.2023,165,0,0 +nopath_15x15,DFS,2,0.1506,165,0,0 +nopath_15x15,DFS,3,0.1511,165,0,0 +nopath_15x15,DFS,4,0.1477,165,0,0 +nopath_15x15,DFS,5,0.1513,165,0,0 +nopath_15x15,DFS,6,0.1455,165,0,0 +nopath_15x15,DFS,7,0.1654,165,0,0 +nopath_15x15,A*,1,0.2915,165,0,0 +nopath_15x15,A*,2,0.3024,165,0,0 +nopath_15x15,A*,3,0.2743,165,0,0 +nopath_15x15,A*,4,0.2980,165,0,0 +nopath_15x15,A*,5,0.2807,165,0,0 +nopath_15x15,A*,6,0.2838,165,0,0 +nopath_15x15,A*,7,0.3015,165,0,0 +nopath_15x15,Dijkstra,1,0.2476,165,0,0 +nopath_15x15,Dijkstra,2,0.2492,165,0,0 +nopath_15x15,Dijkstra,3,0.2435,165,0,0 +nopath_15x15,Dijkstra,4,0.2869,165,0,0 +nopath_15x15,Dijkstra,5,0.2466,165,0,0 +nopath_15x15,Dijkstra,6,0.2480,165,0,0 +nopath_15x15,Dijkstra,7,0.2445,165,0,0 +weighted_31x31,BFS,1,0.4261,433,265,391 +weighted_31x31,BFS,2,0.3905,433,265,391 +weighted_31x31,BFS,3,0.3713,433,265,391 +weighted_31x31,BFS,4,0.3713,433,265,391 +weighted_31x31,BFS,5,0.3672,433,265,391 +weighted_31x31,BFS,6,0.3788,433,265,391 +weighted_31x31,BFS,7,0.4045,433,265,391 +weighted_31x31,DFS,1,0.2646,318,265,391 +weighted_31x31,DFS,2,0.2761,318,265,391 +weighted_31x31,DFS,3,0.2978,318,265,391 +weighted_31x31,DFS,4,0.2618,318,265,391 +weighted_31x31,DFS,5,0.2717,318,265,391 +weighted_31x31,DFS,6,0.2581,318,265,391 +weighted_31x31,DFS,7,0.2787,318,265,391 +weighted_31x31,A*,1,0.6283,405,265,391 +weighted_31x31,A*,2,0.6319,405,265,391 +weighted_31x31,A*,3,0.7192,405,265,391 +weighted_31x31,A*,4,0.6285,405,265,391 +weighted_31x31,A*,5,0.6179,405,265,391 +weighted_31x31,A*,6,0.6571,405,265,391 +weighted_31x31,A*,7,1.0022,405,265,391 +weighted_31x31,Dijkstra,1,0.8638,431,265,391 +weighted_31x31,Dijkstra,2,0.8008,431,265,391 +weighted_31x31,Dijkstra,3,0.6000,431,265,391 +weighted_31x31,Dijkstra,4,0.6262,431,265,391 +weighted_31x31,Dijkstra,5,0.5502,431,265,391 +weighted_31x31,Dijkstra,6,0.5523,431,265,391 +weighted_31x31,Dijkstra,7,0.5431,431,265,391 +weighted_choice,BFS,1,0.1839,189,19,29 +weighted_choice,BFS,2,0.1642,189,19,29 +weighted_choice,BFS,3,0.1718,189,19,29 +weighted_choice,BFS,4,0.2025,189,19,29 +weighted_choice,BFS,5,0.1855,189,19,29 +weighted_choice,BFS,6,0.1656,189,19,29 +weighted_choice,BFS,7,0.1674,189,19,29 +weighted_choice,DFS,1,0.0238,55,19,29 +weighted_choice,DFS,2,0.0204,55,19,29 +weighted_choice,DFS,3,0.0196,55,19,29 +weighted_choice,DFS,4,0.0201,55,19,29 +weighted_choice,DFS,5,0.0372,55,19,29 +weighted_choice,DFS,6,0.0198,55,19,29 +weighted_choice,DFS,7,0.0198,55,19,29 +weighted_choice,A*,1,0.2451,117,25,25 +weighted_choice,A*,2,0.2572,117,25,25 +weighted_choice,A*,3,0.2276,117,25,25 +weighted_choice,A*,4,0.2337,117,25,25 +weighted_choice,A*,5,0.2305,117,25,25 +weighted_choice,A*,6,0.2742,117,25,25 +weighted_choice,A*,7,0.2275,117,25,25 +weighted_choice,Dijkstra,1,0.3360,209,25,25 +weighted_choice,Dijkstra,2,0.4054,209,25,25 +weighted_choice,Dijkstra,3,0.3169,209,25,25 +weighted_choice,Dijkstra,4,0.3882,209,25,25 +weighted_choice,Dijkstra,5,0.3406,209,25,25 +weighted_choice,Dijkstra,6,0.3182,209,25,25 +weighted_choice,Dijkstra,7,0.3200,209,25,25 + +--- СРЕДНИЕ --- +лабиринт,стратегия,среднее_время_мс,посещено_клеток,длина_пути,стоимость_пути +small_10x10,BFS,0.0306,34,16,16 +small_10x10,DFS,0.0142,18,16,16 +small_10x10,A*,0.0420,27,16,16 +small_10x10,Dijkstra,0.0444,33,16,16 +medium_51x51,BFS,0.4909,524,353,353 +medium_51x51,DFS,0.3392,379,353,353 +medium_51x51,A*,0.6780,421,353,353 +medium_51x51,Dijkstra,0.7030,523,353,353 +large_101x101,BFS,2.1077,2143,1265,1265 +large_101x101,DFS,1.2946,1443,1265,1265 +large_101x101,A*,3.6113,1831,1265,1265 +large_101x101,Dijkstra,3.5707,2139,1265,1265 +empty_30x30,BFS,0.7930,784,55,55 +empty_30x30,DFS,0.6007,784,379,379 +empty_30x30,A*,1.6424,784,55,55 +empty_30x30,Dijkstra,1.3066,784,55,55 +nopath_15x15,BFS,0.1588,165,0,0 +nopath_15x15,DFS,0.1591,165,0,0 +nopath_15x15,A*,0.2903,165,0,0 +nopath_15x15,Dijkstra,0.2523,165,0,0 +weighted_31x31,BFS,0.3871,433,265,391 +weighted_31x31,DFS,0.2727,318,265,391 +weighted_31x31,A*,0.6979,405,265,391 +weighted_31x31,Dijkstra,0.6481,431,265,391 +weighted_choice,BFS,0.1773,189,19,29 +weighted_choice,DFS,0.0230,55,19,29 +weighted_choice,A*,0.2423,117,25,25 +weighted_choice,Dijkstra,0.3465,209,25,25 diff --git a/SobolevNS/docs/data/task2_maze/experiment.py b/SobolevNS/docs/data/task2_maze/experiment.py new file mode 100644 index 0000000..1c84746 --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/experiment.py @@ -0,0 +1,82 @@ +""" +experiment.py - экспериментальное сравнение стратегий поиска пути. + +Для каждого лабиринта × стратегии: + - запускаем solve() TRIALS раз + - усредняем время в мс, фиксируем число посещённых клеток и длину пути + - сохраняем в docs/data/results.csv +""" + +import csv +import os + +from maze_solver import ( + TextFileMazeBuilder, MazeSolver, + BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy, +) + + +TRIALS = 7 + +MAZES = [ + ("small_10x10", "mazes/small_10x10.txt"), + ("medium_51x51", "mazes/medium_51x51.txt"), + ("large_101x101", "mazes/large_101x101.txt"), + ("empty_30x30", "mazes/empty_30x30.txt"), + ("nopath_15x15", "mazes/nopath_15x15.txt"), + ("weighted_31x31", "mazes/weighted_31x31.txt"), + ("weighted_choice","mazes/weighted_choice.txt"), +] + +STRATEGY_CLASSES = [BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy] + +OUT_CSV = "docs/data/results.csv" + + +def main(): + os.makedirs(os.path.dirname(OUT_CSV), exist_ok=True) + builder = TextFileMazeBuilder() + + rows = [["лабиринт", "стратегия", "trial", + "время_мс", "посещено_клеток", "длина_пути", "стоимость_пути"]] + summary = [] + + for maze_name, maze_path in MAZES: + maze = builder.build_from_file(maze_path) + print(f"\n## {maze_name} ({maze.width}x{maze.height})") + + for cls in STRATEGY_CLASSES: + times, visited_vals, path_vals, cost_vals = [], [], [], [] + for trial in range(TRIALS): + solver = MazeSolver(maze, cls()) + stats = solver.solve() + cost = sum(c.weight for c in stats["path"]) + times.append(stats["elapsed_ms"]) + visited_vals.append(stats["visited"]) + path_vals.append(stats["path_length"]) + cost_vals.append(cost) + rows.append([maze_name, stats["strategy"], trial + 1, + f"{stats['elapsed_ms']:.4f}", + stats["visited"], stats["path_length"], cost]) + + mean_t = sum(times) / TRIALS + print(f" {cls.name:9s} t_avg={mean_t:7.3f} ms " + f"visited={visited_vals[0]:5d} " + f"path={path_vals[0]:5d} cost={cost_vals[0]:5d}") + summary.append((maze_name, cls.name, mean_t, + visited_vals[0], path_vals[0], cost_vals[0])) + + rows.append([]) + rows.append(["--- СРЕДНИЕ ---"]) + rows.append(["лабиринт", "стратегия", "среднее_время_мс", + "посещено_клеток", "длина_пути", "стоимость_пути"]) + for r in summary: + rows.append([r[0], r[1], f"{r[2]:.4f}", r[3], r[4], r[5]]) + + with open(OUT_CSV, "w", newline="", encoding="utf-8") as f: + csv.writer(f).writerows(rows) + print(f"\nГотово. Результаты записаны в {OUT_CSV}") + + +if __name__ == "__main__": + main() diff --git a/SobolevNS/docs/data/task2_maze/generate_mazes.py b/SobolevNS/docs/data/task2_maze/generate_mazes.py new file mode 100644 index 0000000..0860cb3 --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/generate_mazes.py @@ -0,0 +1,146 @@ +""" +generate_mazes.py - генерирует тестовые лабиринты в mazes/. + +Состав: + small_10x10.txt - маленький с простым путём + medium_50x50.txt - средний с тупиками (DFS-генератор) + large_100x100.txt - большой запутанный (DFS-генератор) + empty_30x30.txt - без стен внутри (только периметр) + nopath_15x15.txt - без пути от S до E (выход замурован) + weighted_30x30.txt - со взвешенными клетками (асфальт/песок/болото) +""" + +import os +import random + +random.seed(2024) + +MAZES_DIR = "mazes" +os.makedirs(MAZES_DIR, exist_ok=True) + + +def write(name, lines): + path = os.path.join(MAZES_DIR, name) + with open(path, "w", encoding="utf-8") as f: + f.write("\n".join(lines) + "\n") + print("written:", path, f"({len(lines)} строк, ширина {len(lines[0])})") + + +def make_small(): + """Маленький лабиринт 10x10, ручной.""" + raw = [ + "##########", + "#S #", + "# ###### #", + "# # #", + "###### # #", + "# # # #", + "# ## # # #", + "# # # #", + "# ##### E", + "##########", + ] + write("small_10x10.txt", raw) + + +def _carve_perfect_maze(w, h, rng): + """Генератор «идеального» лабиринта DFS (recursive backtracker), + итеративный - чтобы не упасть в RecursionError на больших размерах.""" + grid = [['#'] * w for _ in range(h)] + grid[1][1] = ' ' + stack = [(1, 1)] + while stack: + x, y = stack[-1] + dirs = [(0, -2), (0, 2), (-2, 0), (2, 0)] + rng.shuffle(dirs) + carved = False + for dx, dy in dirs: + nx, ny = x + dx, y + dy + if 0 < nx < w - 1 and 0 < ny < h - 1 and grid[ny][nx] == '#': + grid[y + dy // 2][x + dx // 2] = ' ' + grid[ny][nx] = ' ' + stack.append((nx, ny)) + carved = True + break + if not carved: + stack.pop() + return grid + + +def make_with_generator(name, w, h): + """Создаёт перфектный лабиринт и расставляет S/E в противоположных углах.""" + rng = random.Random(hash(name) & 0xFFFF) + grid = _carve_perfect_maze(w, h, rng) + grid[1][1] = 'S' + grid[h - 2][w - 2] = 'E' + lines = ["".join(row) for row in grid] + write(name, lines) + + +def make_empty(name, w, h): + """Пустая комната - только периметр.""" + lines = [] + for y in range(h): + if y == 0 or y == h - 1: + lines.append('#' * w) + else: + lines.append('#' + ' ' * (w - 2) + '#') + # старт в левом верхнем углу, выход в правом нижнем + row = list(lines[1]); row[1] = 'S'; lines[1] = "".join(row) + row = list(lines[h - 2]); row[w - 2] = 'E'; lines[h - 2] = "".join(row) + write(name, lines) + + +def make_nopath(name, w=15, h=15): + """Лабиринт, в котором выход замурован - пути нет.""" + lines = ['#' * w] + for y in range(1, h - 1): + lines.append('#' + ' ' * (w - 2) + '#') + lines.append('#' * w) + # S слева сверху + row = list(lines[1]); row[1] = 'S'; lines[1] = "".join(row) + # E в правой нижней клетке, но обнесён стенами с двух сторон + # делаем «коробочку» 3x3 вокруг E с одним зазором, который мы тут же закроем + ex, ey = w - 2, h - 2 + # сначала откроем коробочку из стен 1 клетка по периметру вокруг E + # построим коробочку: на (ex-1, ey-1)..(ex+1, ey+1) поставим '#' кроме E + for yy in range(ey - 1, ey + 2): + for xx in range(ex - 1, ex + 2): + if 0 <= xx < w and 0 <= yy < h and not (xx == ex and yy == ey): + row = list(lines[yy]); row[xx] = '#'; lines[yy] = "".join(row) + row = list(lines[ey]); row[ex] = 'E'; lines[ey] = "".join(row) + write(name, lines) + + +def make_weighted(name, w=30, h=30): + """Перфектный лабиринт + случайные взвешенные клетки на проходимых местах.""" + rng = random.Random(7) + grid = _carve_perfect_maze(w | 1, h | 1, rng) + # Перекрасим часть проходов в '.', ',' и '~' + for y, row in enumerate(grid): + for x, ch in enumerate(row): + if ch == ' ': + r = rng.random() + if r < 0.65: + grid[y][x] = ' ' # асфальт (1) + elif r < 0.90: + grid[y][x] = ',' # песок (2) + else: + grid[y][x] = '~' # болото (3) + grid[1][1] = 'S' + grid[len(grid) - 2][len(grid[0]) - 2] = 'E' + lines = ["".join(row) for row in grid] + write(name, lines) + + +def main(): + make_small() + make_with_generator("medium_51x51.txt", 51, 51) + make_with_generator("large_101x101.txt", 101, 101) + make_empty("empty_30x30.txt", 30, 30) + make_nopath("nopath_15x15.txt", 15, 15) + make_weighted("weighted_31x31.txt", 31, 31) + + +if __name__ == "__main__": + main() diff --git a/SobolevNS/docs/data/task2_maze/generate_weighted_choice.py b/SobolevNS/docs/data/task2_maze/generate_weighted_choice.py new file mode 100644 index 0000000..a85aa42 --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/generate_weighted_choice.py @@ -0,0 +1,19 @@ +"""generate_weighted_choice.py - создаёт лабиринт, где Dijkstra/A* реально +обходят 'болото' и находят более дешёвый путь, чем BFS.""" +W, H = 21, 13 +grid = [[' '] * W for _ in range(H)] +# периметр +for x in range(W): + grid[0][x] = '#'; grid[H-1][x] = '#' +for y in range(H): + grid[y][0] = '#'; grid[y][W-1] = '#' +# центральное болото 5х5 (вес 3) +for y in range(4, 9): + for x in range(8, 13): + grid[y][x] = '~' +# старт слева в центре, выход справа в центре +grid[H//2][1] = 'S' +grid[H//2][W-2] = 'E' +with open('mazes/weighted_choice.txt','w') as f: + f.write('\n'.join(''.join(row) for row in grid) + '\n') +print(open('mazes/weighted_choice.txt').read()) diff --git a/SobolevNS/docs/data/task2_maze/maze_solver/__init__.py b/SobolevNS/docs/data/task2_maze/maze_solver/__init__.py new file mode 100644 index 0000000..b97570a --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/maze_solver/__init__.py @@ -0,0 +1,18 @@ +"""Пакет maze_solver.""" +from .model import Cell, Maze +from .builder import MazeBuilder, TextFileMazeBuilder +from .strategies import ( + PathFindingStrategy, BFSStrategy, DFSStrategy, + AStarStrategy, DijkstraStrategy, STRATEGIES, +) +from .solver import MazeSolver, Observer, ConsoleView, SearchStats +from .command import Player, Command, MoveCommand, CommandHistory + +__all__ = [ + "Cell", "Maze", + "MazeBuilder", "TextFileMazeBuilder", + "PathFindingStrategy", "BFSStrategy", "DFSStrategy", + "AStarStrategy", "DijkstraStrategy", "STRATEGIES", + "MazeSolver", "Observer", "ConsoleView", "SearchStats", + "Player", "Command", "MoveCommand", "CommandHistory", +] diff --git a/SobolevNS/docs/data/task2_maze/maze_solver/builder.py b/SobolevNS/docs/data/task2_maze/maze_solver/builder.py new file mode 100644 index 0000000..f8f0dc8 --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/maze_solver/builder.py @@ -0,0 +1,92 @@ +""" +maze_solver/builder.py - паттерн Builder для создания лабиринтов. + +Зачем Builder: процесс построения лабиринта сложный (чтение файла, парсинг, +валидация символов, простановка флагов, поиск старта и выхода). Builder +изолирует эти подробности от клиента; для нового формата (JSON, бинарный) +достаточно реализовать ещё один builder с тем же интерфейсом. +""" + +from abc import ABC, abstractmethod +from .model import Cell, Maze + + +class MazeBuilder(ABC): + """Абстрактный билдер лабиринта.""" + + @abstractmethod + def build_from_file(self, filename) -> Maze: + """Возвращает готовый Maze.""" + + +class TextFileMazeBuilder(MazeBuilder): + """Билдер из текстового формата. + + Символы: + '#' - стена + ' ' - проход (вес 1) + 'S' - старт (проходим) + 'E' - выход (проходим) + '.' - асфальт (вес 1) - то же, что пробел + ',' - песок (вес 2) + '~' - болото (вес 3) + + Лишние пробельные символы в начале/конце файла игнорируются, + но внутри строки пробелы значимы (это проходы). + """ + + WEIGHT_MAP = {'.': 1, ',': 2, '~': 3} + + def build_from_file(self, filename) -> Maze: + with open(filename, encoding="utf-8") as f: + raw = f.read().splitlines() + + # отбрасываем пустые строки в конце - частая мелочь + while raw and raw[-1] == "": + raw.pop() + if not raw: + raise ValueError(f"Файл лабиринта {filename!r} пуст.") + + height = len(raw) + width = max(len(line) for line in raw) + + # выравниваем строки по ширине пробелами (если строки разной длины) + lines = [line.ljust(width, '#') for line in raw] + + maze = Maze(width, height) + start_count = 0 + exit_count = 0 + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + cell = self._parse_char(x, y, ch) + maze.grid[y][x] = cell + if cell.is_start: + maze.start = cell + start_count += 1 + if cell.is_exit: + maze.exit_ = cell + exit_count += 1 + + # валидация + if start_count != 1: + raise ValueError( + f"В лабиринте {filename!r} ожидался ровно 1 'S', нашли {start_count}.") + if exit_count != 1: + raise ValueError( + f"В лабиринте {filename!r} ожидался ровно 1 'E', нашли {exit_count}.") + + return maze + + def _parse_char(self, x, y, ch): + if ch == '#': + return Cell(x, y, is_wall=True) + if ch == 'S': + return Cell(x, y, is_start=True, weight=1) + if ch == 'E': + return Cell(x, y, is_exit=True, weight=1) + if ch in self.WEIGHT_MAP: + return Cell(x, y, weight=self.WEIGHT_MAP[ch]) + if ch == ' ': + return Cell(x, y, weight=1) + raise ValueError(f"Неизвестный символ {ch!r} в позиции ({x},{y}).") diff --git a/SobolevNS/docs/data/task2_maze/maze_solver/command.py b/SobolevNS/docs/data/task2_maze/maze_solver/command.py new file mode 100644 index 0000000..e5e99dd --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/maze_solver/command.py @@ -0,0 +1,87 @@ +""" +maze_solver/command.py - паттерн Command. + +Player хранит текущую клетку. MoveCommand двигает игрока в выбранном +направлении и помнит предыдущую позицию для undo. Менеджер CommandHistory +держит стек выполненных команд. +""" + +from abc import ABC, abstractmethod + + +class Player: + """Игрок в лабиринте.""" + + def __init__(self, cell): + self.cell = cell + + @property + def x(self): return self.cell.x + + @property + def y(self): return self.cell.y + + +class Command(ABC): + @abstractmethod + def execute(self): ... + @abstractmethod + def undo(self): ... + + +class MoveCommand(Command): + """Команда перемещения игрока на одну клетку. + + direction: одна из 'W','A','S','D' (вверх, влево, вниз, вправо). + """ + + DELTAS = { + 'W': (0, -1), + 'S': (0, 1), + 'A': (-1, 0), + 'D': (1, 0), + } + + def __init__(self, maze, player, direction): + self.maze = maze + self.player = player + self.direction = direction.upper() + self._prev_cell = None + self._executed = False + + def execute(self): + if self.direction not in self.DELTAS: + return False + dx, dy = self.DELTAS[self.direction] + target = self.maze.get_cell(self.player.x + dx, self.player.y + dy) + if target is None or not target.is_passable(): + return False + self._prev_cell = self.player.cell + self.player.cell = target + self._executed = True + return True + + def undo(self): + if not self._executed: + return False + self.player.cell = self._prev_cell + self._executed = False + return True + + +class CommandHistory: + """Стек выполненных команд (для общего undo).""" + + def __init__(self): + self._stack = [] + + def do(self, cmd): + if cmd.execute(): + self._stack.append(cmd) + return True + return False + + def undo(self): + if not self._stack: + return False + return self._stack.pop().undo() diff --git a/SobolevNS/docs/data/task2_maze/maze_solver/model.py b/SobolevNS/docs/data/task2_maze/maze_solver/model.py new file mode 100644 index 0000000..75de9cf --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/maze_solver/model.py @@ -0,0 +1,92 @@ +""" +maze_solver/model.py - модель лабиринта (этап 1, без паттернов). +""" + +class Cell: + """Клетка лабиринта. + + Атрибуты: + x, y - координаты + is_wall - стена ли + is_start - стартовая клетка + is_exit - клетка выхода + weight - стоимость прохода (по умолчанию 1, для взвешенного режима >1) + """ + __slots__ = ("x", "y", "is_wall", "is_start", "is_exit", "weight") + + def __init__(self, x, y, is_wall=False, is_start=False, is_exit=False, weight=1): + self.x = x + self.y = y + self.is_wall = is_wall + self.is_start = is_start + self.is_exit = is_exit + self.weight = weight + + def is_passable(self): + return not self.is_wall + + def __repr__(self): + return f"Cell({self.x},{self.y},wall={self.is_wall})" + + +class Maze: + """Лабиринт как двумерный массив клеток. + + Атрибуты: + width, height - размеры + grid - список списков клеток [y][x] + start, exit_ - ссылки на клетки старта и выхода (могут быть None при ошибке) + """ + + def __init__(self, width, height): + self.width = width + self.height = height + self.grid = [[Cell(x, y, is_wall=True) for x in range(width)] + for y in range(height)] + self.start = None + self.exit_ = None + + def get_cell(self, x, y): + if 0 <= x < self.width and 0 <= y < self.height: + return self.grid[y][x] + return None + + def get_neighbors(self, cell): + """Соседи (вверх, вниз, влево, вправо), только проходимые и в пределах поля.""" + out = [] + for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)): + nb = self.get_cell(cell.x + dx, cell.y + dy) + if nb is not None and nb.is_passable(): + out.append(nb) + return out + + def render_text(self, path=None, player=None): + """Возвращает текстовое представление лабиринта. + + '#' стена, ' ' проход, 'S' старт, 'E' выход, + '.' клетка пути, '@' игрок. + """ + path_set = set() + if path: + for c in path: + path_set.add((c.x, c.y)) + + lines = [] + for y in range(self.height): + row = [] + for x in range(self.width): + cell = self.grid[y][x] + ch = ' ' + if cell.is_wall: + ch = '#' + elif cell.is_start: + ch = 'S' + elif cell.is_exit: + ch = 'E' + elif (x, y) in path_set: + ch = '.' + if player is not None and player.x == x and player.y == y: + ch = '@' + row.append(ch) + lines.append("".join(row)) + return "\n".join(lines) diff --git a/SobolevNS/docs/data/task2_maze/maze_solver/solver.py b/SobolevNS/docs/data/task2_maze/maze_solver/solver.py new file mode 100644 index 0000000..c3af08d --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/maze_solver/solver.py @@ -0,0 +1,102 @@ +""" +maze_solver/solver.py - оркестратор MazeSolver + паттерн Observer. + +MazeSolver знает лабиринт и текущую стратегию (Strategy). Перед поиском +он уведомляет наблюдателей (Observer) о старте, после поиска - о результате. +""" + +import time +from abc import ABC, abstractmethod + + +# ---------- Observer ---------- + +class Observer(ABC): + """Интерфейс наблюдателя.""" + + @abstractmethod + def update(self, event): + """event - dict с ключом 'type' и сопровождающими данными.""" + + +class ConsoleView(Observer): + """Простой текстовый наблюдатель.""" + + def __init__(self, verbose=True): + self.verbose = verbose + + def update(self, event): + if not self.verbose: + return + t = event["type"] + if t == "maze_loaded": + m = event["maze"] + print(f"[ConsoleView] лабиринт {m.width}x{m.height} загружен") + elif t == "search_start": + print(f"[ConsoleView] старт поиска: {event['strategy']}") + elif t == "search_end": + stats = event["stats"] + print(f"[ConsoleView] поиск окончен: путь={stats['path_length']}, " + f"посещено={stats['visited']}, время={stats['elapsed_ms']:.3f} мс") + elif t == "move": + print(f"[ConsoleView] игрок -> ({event['x']},{event['y']})") + elif t == "path_found": + print("[ConsoleView] путь найден") + elif t == "no_path": + print("[ConsoleView] пути нет") + + +# ---------- MazeSolver ---------- + +class SearchStats(dict): + """Простой dict-подобный контейнер статистики поиска.""" + pass + + +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 attach(self, observer): + self._observers.append(observer) + + def detach(self, observer): + self._observers.remove(observer) + + def _notify(self, event): + for obs in self._observers: + obs.update(event) + + def solve(self): + if self.strategy is None: + raise RuntimeError("Стратегия не задана") + if self.maze.start is None or self.maze.exit_ is None: + raise RuntimeError("В лабиринте нет старта или выхода") + + self._notify({"type": "search_start", "strategy": self.strategy.name}) + + t0 = time.perf_counter() + result = self.strategy.find_path(self.maze, + self.maze.start, + self.maze.exit_) + elapsed = (time.perf_counter() - t0) * 1000.0 + + path = result["path"] + stats = SearchStats( + strategy=self.strategy.name, + elapsed_ms=elapsed, + visited=result["visited"], + path_length=len(path), + path=path, + ) + self._notify({"type": "search_end", "stats": stats}) + if path: + self._notify({"type": "path_found"}) + else: + self._notify({"type": "no_path"}) + return stats diff --git a/SobolevNS/docs/data/task2_maze/maze_solver/strategies.py b/SobolevNS/docs/data/task2_maze/maze_solver/strategies.py new file mode 100644 index 0000000..c170bb9 --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/maze_solver/strategies.py @@ -0,0 +1,179 @@ +""" +maze_solver/strategies.py - паттерн Strategy. + +Каждая стратегия реализует один и тот же интерфейс PathFindingStrategy +с методом find_path(maze, start, exit_), возвращающим: + {'path': [Cell, ...], 'visited': int} + +Стратегии не модифицируют сам лабиринт. +""" + +from abc import ABC, abstractmethod +from collections import deque +import heapq + + +# ---------- интерфейс стратегии ---------- + +class PathFindingStrategy(ABC): + name = "Strategy" + + @abstractmethod + def find_path(self, maze, start, exit_): + """Возвращает dict с ключами 'path' (list[Cell]) и 'visited' (int). + Если пути нет - path = [].""" + + +# ---------- общая утилита: восстановление пути ---------- + +def _reconstruct(parents, start, end): + """Восстанавливает путь по словарю предшественников {(x,y): Cell|None}.""" + path = [] + cur = end + while cur is not None: + path.append(cur) + cur = parents.get((cur.x, cur.y)) + path.reverse() + if path and path[0] is start: + return path + return [] + + +# ---------- BFS ---------- + +class BFSStrategy(PathFindingStrategy): + """Поиск в ширину. Гарантирует кратчайший путь по числу шагов + (когда веса всех клеток равны).""" + name = "BFS" + + def find_path(self, maze, start, exit_): + queue = deque([start]) + parents = {(start.x, start.y): None} + visited = 1 + + while queue: + cell = queue.popleft() + if cell is exit_: + return {"path": _reconstruct(parents, start, exit_), + "visited": visited} + for nb in maze.get_neighbors(cell): + key = (nb.x, nb.y) + if key not in parents: + parents[key] = cell + visited += 1 + queue.append(nb) + return {"path": [], "visited": visited} + + +# ---------- DFS ---------- + +class DFSStrategy(PathFindingStrategy): + """Поиск в глубину. Не гарантирует кратчайший путь, но прост и быстр.""" + name = "DFS" + + def find_path(self, maze, start, exit_): + stack = [start] + parents = {(start.x, start.y): None} + visited = 1 + + while stack: + cell = stack.pop() + if cell is exit_: + return {"path": _reconstruct(parents, start, exit_), + "visited": visited} + for nb in maze.get_neighbors(cell): + key = (nb.x, nb.y) + if key not in parents: + parents[key] = cell + visited += 1 + stack.append(nb) + return {"path": [], "visited": visited} + + +# ---------- A* ---------- + +def _manhattan(a, b): + return abs(a.x - b.x) + abs(a.y - b.y) + + +class AStarStrategy(PathFindingStrategy): + """A*-поиск с манхэттенской эвристикой. Учитывает вес клеток (weight).""" + name = "A*" + + def find_path(self, maze, start, exit_): + # f = g + h; в куче храним (f, tie, cell) + g_score = {(start.x, start.y): 0} + parents = {(start.x, start.y): None} + tie = 0 + heap = [(_manhattan(start, exit_), tie, start)] + visited = 0 + closed = set() + + while heap: + f, _, cell = heapq.heappop(heap) + key = (cell.x, cell.y) + if key in closed: + continue + closed.add(key) + visited += 1 + + if cell is exit_: + return {"path": _reconstruct(parents, start, exit_), + "visited": visited} + + for nb in maze.get_neighbors(cell): + nb_key = (nb.x, nb.y) + tentative_g = g_score[key] + nb.weight + if tentative_g < g_score.get(nb_key, float("inf")): + g_score[nb_key] = tentative_g + parents[nb_key] = cell + tie += 1 + heapq.heappush(heap, + (tentative_g + _manhattan(nb, exit_), tie, nb)) + return {"path": [], "visited": visited} + + +# ---------- Дейкстра ---------- + +class DijkstraStrategy(PathFindingStrategy): + """Дейкстра - оптимальный путь с учётом веса клеток. + На немодифицированном лабиринте (все веса = 1) совпадает с BFS.""" + name = "Dijkstra" + + def find_path(self, maze, start, exit_): + dist = {(start.x, start.y): 0} + parents = {(start.x, start.y): None} + tie = 0 + heap = [(0, tie, start)] + visited = 0 + closed = set() + + while heap: + d, _, cell = heapq.heappop(heap) + key = (cell.x, cell.y) + if key in closed: + continue + closed.add(key) + visited += 1 + + if cell is exit_: + return {"path": _reconstruct(parents, start, exit_), + "visited": visited} + + for nb in maze.get_neighbors(cell): + nb_key = (nb.x, nb.y) + nd = d + nb.weight + if nd < dist.get(nb_key, float("inf")): + dist[nb_key] = nd + parents[nb_key] = cell + tie += 1 + heapq.heappush(heap, (nd, tie, nb)) + return {"path": [], "visited": visited} + + +STRATEGIES = { + "BFS": BFSStrategy, + "DFS": DFSStrategy, + "A*": AStarStrategy, + "Dijkstra": DijkstraStrategy, +} diff --git a/SobolevNS/docs/data/task2_maze/mazes/empty_30x30.txt b/SobolevNS/docs/data/task2_maze/mazes/empty_30x30.txt new file mode 100644 index 0000000..386c2e4 --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/mazes/empty_30x30.txt @@ -0,0 +1,30 @@ +############################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +############################## diff --git a/SobolevNS/docs/data/task2_maze/mazes/large_101x101.txt b/SobolevNS/docs/data/task2_maze/mazes/large_101x101.txt new file mode 100644 index 0000000..e1bd983 --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/mazes/large_101x101.txt @@ -0,0 +1,101 @@ +##################################################################################################### +#S# # # # # # # # # # # # # # # +# ### # ### ### # # # ##### # ####### # ### # # ### # # ######### ### # # # ### ### # # # # ### ##### +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +####### # # # ### # # ### ##### ### ##### # ####### # ######### # # ### ######### # ##### ### # ### # +# # # # # # # # # # # # # # # # # # # # # # # # # +# ##### # ### ### # ##################### ### # # # ####### # ####### ####### # ### # # ### # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # ########### ########### # # # # ####### ### # ##### # ####### ##### ##### # # # # # # # ### # ### +# # # # # # # # # # # # # # # # # # # # # # # # +# # # # # ####### # ##### ### ##### # ##### # ### # # ### # ######### ### ##### # ##### # ######### # +# # # # # # # # # # # # # # # # # # # # # # # +# ### ### # ### # ### # ########### ##### ### # ### # ### # ### ### # # # ### ##### ################# +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # ### ##### # # ### ########### ### # ### ### # ##### # ####### ##### ### ######### ### # ####### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# ### ####### # # # ### # ### ##### # ####### ##### # ####### # # # ##### ######### # # # # ### # ### +# # # # # # # # # # # # # # # # # # # # # # # # # # +### ####### ### # # # ##### # # ### ### # # # ##### # # ### # ##### # ##### ### # # # # ####### ### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# ### ### ### ######### # ####### ### # ### ####### ##### ########### # # ### ### ##### # # ### # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # ### # ##### # # # # ### # # # # ########### ####### # # ### # ### # ##### # ### ##### ### ### # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +# ### ##### ##### ### # # ####### ########### # # ### # ##### # ### ####### # ### ### # ### ### ### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # +### # # # ### # ### # # # # # # ##### ### ### ##### ##### ####### # ### ### # # ####### ######### # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# ### ##### ##### ##### # ##### # # ### ### ##### # ####### # ##### # # # # ### ##### ####### # ##### +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# # ##### ### # ### # # ### # ####### ### # # # # ##### # # # # # # ##### ############# ### ### ### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# ### # ### ##### # # # # ### # ### ####### # # ### # ### ##### ##### # ### # ########### ### ### ### +# # # # # # # # # # # # # # # # # # # # # # # # # # +### # ### ### # ##################### # # ### ### ##### # # # ### # ####### # # # ##### # # ### ### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# # ####### ############# # # # ### ####### ### ##### ### # # ####### # ####### ### # ### # # ### # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# ### ### # # # # ########### # # # # ####### # ### ### ### ####### # ####### # ### ### # # ### ### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +####### ### # # ######### # ##### ##### ### ### # ### # # ##### # ### # ### ### # ### ####### ### # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # ### ### # ### ##### # ### ### ### ### ### ### # ####### # ##### # ### ######### # # ### ####### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # ### ### ##### ##### ##### ##### # ##### # ### # ### # ##### # ######### # ### ### # ### ### # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # ### # ####### # # # ### # ##### ### # # ### # ##### # ### ######### # # # ### ####### ####### # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# ####### # ### ### # ######### # # # ### ### ### # ######### # # ### ##### ####### ### ##### # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # ####### ### ####### # ######### ### # ### ########### ### ### ##### ### ### # # # # ######### # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# ### # ####### ##### # ### # ##### ### # # ############# ### ### ##### ### ### ##### # ### # # # ### +# # # # # # # # # # # # # # # # # # # # # # # # # # +### # # ### ####### # # # ##### # ### ### ##### # # ####### # # # # # ####### ### ### # # ### ##### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +# ####### ### # # ### ### ######### # ##### # # ### # ### # # # # # ### ######### # # ##### # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # ### ### ### ####### ##### ####### # ##### # # ##### # ########### ######### ### ##### # ##### # # +# # # # # # # # # # # # # # # # # # # # # # # # +##### ### # # ### ### ##### # ##### ##### ### # ######### # # # # # # ##### ##### ##### # ### # ### # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# ##### # # # ### # ##### # ####### ### # ##### # # # ##### # ##### ### # ### ##### ####### # ##### # +# # # # # # # # # # # # # # # # # # # # # # # # # # +##### ##### ### ### # ########### ### # ##### ### ##### # # # ### ######### # # # # # ### ### ### ### +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +# ### # # # ############# # ####### ####### # # ### # ######### ##### # # ####### # # # # # ### ### # +# # # # # # # # # # # # # # # # # # # # # # # # # +### ##### ### # # # # # ##### ##### ### # # ##### # ##### # ##### # ### ##### # ##### # ##### # ### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# ### # # # ### # ######### ### # ### # ############### # ### # # ######### ##### # ### ### # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # ####### # # # # # ##### ####### # # ####### # # ##### # # # # # ### # ### # # # ### ### # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # +### ############# ############### # ### # ####### # # # ####### # ### # ##### ### # ########### # ### +# # # # # # # # # # # # # # # # # # +# ####### ####### # ####### # # ##### ### ##### ### ##### # ##### # ### ##### # ### # ############# # +# # # # # # # # # # # # # # # # # # # # # # # # +# ##### ### ### # # ### # ######### # ##### ##### # # # ##### ### ### # # # ########### ### ### ### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # ######### ### ######### # ### # # # ##### # # # # ### ### # ### # ##### ### # ### ### # # # ##### +# # # # # # # # # # # # # # # # # # # # # # # # # +# # ### # # # ### # ### ### # # ### ####### # ######### ####### # ##### ####### ### ########### ### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# ### ##### ### ### ### # ### ### # ##### ##### # # # ##### # ##### ####### # ### ########### ### # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # ##### ### # # ### ### # ### # ##### ####### ####### ### # ####### ##### # # ### # # ### ### ##### +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # ####### ### # ####### # ##### ### ######### # ##### ##### # # # # # # # # ### ### # ### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # ##### # ### # # ########### ##### # # ### # # # ##### # ### ### # # # # ####### # ##### # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# ### # # # ### ### ##### # # ### # ####### # # # ##### # # ### # # # # ##### # # # # ######### # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# ### # # # # # ##### # # # ### # ### ##### # ##### # ##### # ##### ##### # ### # ##### # ### ##### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +### # # # ### # # ######### # # ### # # ##### ### ####### ### ### # # ##### # ####### ### # # # ### # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # ### ####### ##### # # ### # ##### # # # ######### # # ##### ### ##### ##### ### ### ### # ### # # +# # # # # # # # # # # # #E# +##################################################################################################### diff --git a/SobolevNS/docs/data/task2_maze/mazes/medium_51x51.txt b/SobolevNS/docs/data/task2_maze/mazes/medium_51x51.txt new file mode 100644 index 0000000..dd74392 --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/mazes/medium_51x51.txt @@ -0,0 +1,51 @@ +################################################### +#S# # # # # # # # +# # # ### # ######### # ### # # # # ####### ### ### +# # # # # # # # # # # # # +# # ### # ############### # ######### # ##### # # # +# # # # # # # # # # # # # +##### ### # ##### # ####### # ##### # ### ### ### # +# # # # # # # # # # # # # # # +# # # # ##### # ####### # # ### # # ####### ### # # +# # # # # # # # # # # # # +# ####### # ##### # ######### ### # # ########### # +# # # # # # # # # # # # +# ########### # ### ### ####### # ##### # ### # ### +# # # # # # # # # # # +# # ### ### ##### ### # ##### ############# ##### # +# # # # # # # # # # # # # +##### ### # ### # # # ### # ######### ##### # # # # +# # # # # # # # # # # # +# ##### ################# ####### # # # # ##### # # +# # # # # # # # # # # # +### # ### ############# ##### # # # ### ##### ### # +# # # # # # # # # # # # # # # +# # # # ##### # ### # ######### ### # ### # # # # # +# # # # # # # # # # # # # # +# ### ##### # ######### # ### ### # ######### # ### +# # # # # # # # # # # # # +### ### ##### ### # ##### # ##### # # # # # ####### +# # # # # # # # # # # # # # # +# ##### # # ### ### # # ##### # ### # ####### ### # +# # # # # # # # # # # # # # # +# # # ####### # # ####### # ##### ##### # ##### # # +# # # # # # # # # # # # # # +####### ### ##### # # # # # # # # # ########### # # +# # # # # # # # # # # # # # # # +# ### # # ### ### # # # # # # # # ####### ##### # # +# # # # # # # # # # # # # # # # # +# # ##### # # # ##### # # # # # ####### ### ##### # +# # # # # # # # # # # # # # # +# # # # ############### # # ####### ##### ### # # # +# # # # # # # # # # # # +# # # ##### # ####### # # ################# # ##### +# # # # # # # # # # # # # # +# # ### # ##### # # ### # ### # # ####### # ##### # +# # # # # # # # # # # # # # # # +# # # # ### # # # ################# # # # # # ##### +# # # # # # # # # # # # +# # ### # ####### # ### ### ################# # # # +# # # # # # # # # # # # # +# ### ############# # ### ####### ##### # # ##### # +# # # # E# +################################################### diff --git a/SobolevNS/docs/data/task2_maze/mazes/nopath_15x15.txt b/SobolevNS/docs/data/task2_maze/mazes/nopath_15x15.txt new file mode 100644 index 0000000..569b8ab --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/mazes/nopath_15x15.txt @@ -0,0 +1,15 @@ +############### +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# ### +# #E# +############### diff --git a/SobolevNS/docs/data/task2_maze/mazes/small_10x10.txt b/SobolevNS/docs/data/task2_maze/mazes/small_10x10.txt new file mode 100644 index 0000000..354bac9 --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/mazes/small_10x10.txt @@ -0,0 +1,10 @@ +########## +#S # +# ###### # +# # # +###### # # +# # # # +# ## # # # +# # # # +# ##### E +########## diff --git a/SobolevNS/docs/data/task2_maze/mazes/weighted_31x31.txt b/SobolevNS/docs/data/task2_maze/mazes/weighted_31x31.txt new file mode 100644 index 0000000..97155cf --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/mazes/weighted_31x31.txt @@ -0,0 +1,31 @@ +############################### +#S # ~ ,, # ~#, ,,# +### #####~### ###,#,#####,#,# # +# # # ~~#, ,# #,# #~# #,# +#~#,# # ### #,###,##### ### # # +# # ,#, # # # , # ,#,~ #~# # +# ####### #,#~# ### #~###,# #~# +# ~,,# # # #~# #~ , # # +# ##### #,####### # # ####### # +#, # ,# ,, ,~#, # ~ ~~# #,# +### ########### # ####### # # # +# ~ #, , ~# # # #, #,, # #,# +#,###,###,# # # # ### # ### #,# +#, , , # #, ,#,#, ,~# # ,#~# +# ####### ###,# ####### # ### # +#, #, ~ #~, # ~ , # ~~# +###~#~# ################# ##### +#~ ,# # #,, ,,, ~, ,# , #,#,~,# +# ###,###,##### ###,### # # ### +# , # #~ # , # ,# # , # +### ####### # ###,#####~# #~# # +# # ,, #,# # ~, # # #,# # +# ###########,# ##### ### # #~# +#, ,#~, ,# # ,# , #~# # +# ### ### ##### # ##### ##### # +#~#,# # , ~# #~ # , , # +# # ### ##### #,###########,#,# +# # # ,, #~ ,# ,, # # #~# +# ### #,#######,# ###,# # #,# # +#~, , # , ,~, # ,#~ ,#E# +############################### diff --git a/SobolevNS/docs/data/task2_maze/mazes/weighted_choice.txt b/SobolevNS/docs/data/task2_maze/mazes/weighted_choice.txt new file mode 100644 index 0000000..439cab9 --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/mazes/weighted_choice.txt @@ -0,0 +1,13 @@ +##################### +# # +# # +# # +# ~~~~~ # +# ~~~~~ # +#S ~~~~~ E# +# ~~~~~ # +# ~~~~~ # +# # +# # +# # +##################### diff --git a/SobolevNS/docs/data/task2_maze/plot_results.py b/SobolevNS/docs/data/task2_maze/plot_results.py new file mode 100644 index 0000000..24913ba --- /dev/null +++ b/SobolevNS/docs/data/task2_maze/plot_results.py @@ -0,0 +1,99 @@ +"""plot_results.py - графики для эксперимента с лабиринтами.""" +import csv +import os +import matplotlib.pyplot as plt +import numpy as np + +CSV = "docs/data/results.csv" +PLOTS = "docs/data/plots" +os.makedirs(PLOTS, exist_ok=True) + + +def load_means(): + """Возвращает dict[(maze, strategy)] = (time_ms, visited, path_len, cost).""" + out = {} + with open(CSV, encoding="utf-8") as f: + rows = list(csv.reader(f)) + start = next(i for i, r in enumerate(rows) if r and r[0] == "--- СРЕДНИЕ ---") + 2 + for r in rows[start:]: + if not r: + continue + maze, strat, t, vis, plen, cost = r + out[(maze, strat)] = (float(t), int(vis), int(plen), int(cost)) + return out + + +MAZES = ["small_10x10", "medium_51x51", "large_101x101", + "empty_30x30", "nopath_15x15", + "weighted_31x31", "weighted_choice"] +STRATEGIES = ["BFS", "DFS", "A*", "Dijkstra"] +COLORS = {"BFS": "#3498db", "DFS": "#e67e22", "A*": "#2ecc71", "Dijkstra": "#9b59b6"} + + +def grouped_bar(means, idx, ylabel, title, fname, log=True): + x = np.arange(len(MAZES)) + w = 0.2 + fig, ax = plt.subplots(figsize=(11, 5)) + for i, s in enumerate(STRATEGIES): + vals = [means[(m, s)][idx] for m in MAZES] + bars = ax.bar(x + (i - 1.5) * w, vals, w, label=s, color=COLORS[s], alpha=0.9) + for b, v in zip(bars, vals): + ax.text(b.get_x() + b.get_width() / 2, b.get_height(), + f"{v:g}", ha="center", va="bottom", fontsize=7, rotation=0) + ax.set_xticks(x) + ax.set_xticklabels(MAZES, rotation=20, ha="right") + ax.set_ylabel(ylabel) + ax.set_title(title) + if log: + ax.set_yscale("log") + ax.legend() + ax.grid(axis="y", linestyle="--", alpha=0.4) + plt.tight_layout() + p = os.path.join(PLOTS, fname) + plt.savefig(p, dpi=130) + plt.close() + print("saved:", p) + + +def weighted_choice_chart(means): + """Отдельный график для weighted_choice: путь vs стоимость.""" + strategies = STRATEGIES + lengths = [means[("weighted_choice", s)][2] for s in strategies] + costs = [means[("weighted_choice", s)][3] for s in strategies] + x = np.arange(len(strategies)) + w = 0.35 + fig, ax = plt.subplots(figsize=(7.5, 4.5)) + b1 = ax.bar(x - w/2, lengths, w, label="длина пути (клеток)", + color="#3498db", alpha=0.9) + b2 = ax.bar(x + w/2, costs, w, label="стоимость пути (сумма весов)", + color="#e74c3c", alpha=0.9) + for bars in (b1, b2): + for bar in bars: + ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height(), + f"{bar.get_height():.0f}", ha="center", va="bottom", fontsize=9) + ax.set_xticks(x); ax.set_xticklabels(strategies) + ax.set_title("weighted_choice: BFS/DFS режут через болото,\n" + "Dijkstra/A* находят более дешёвый обход") + ax.set_ylabel("значение") + ax.legend() + ax.grid(axis="y", linestyle="--", alpha=0.4) + plt.tight_layout() + p = os.path.join(PLOTS, "weighted_choice_compare.png") + plt.savefig(p, dpi=130) + plt.close() + print("saved:", p) + + +def main(): + means = load_means() + grouped_bar(means, 0, "Время, мс (среднее по 7 запускам, лог. шкала)", + "Время поиска пути", "time_compare.png", log=True) + grouped_bar(means, 1, "Число посещённых клеток (лог. шкала)", + "Сколько клеток посетил алгоритм", "visited_compare.png", log=True) + grouped_bar(means, 2, "Длина пути (клеток)", + "Длина найденного пути", "path_compare.png", log=False) + weighted_choice_chart(means) + + +if __name__ == "__main__": + main() diff --git a/SobolevNS/docs/report_02.md b/SobolevNS/docs/report_02.md new file mode 100644 index 0000000..9fd4f53 --- /dev/null +++ b/SobolevNS/docs/report_02.md @@ -0,0 +1,364 @@ +# Отчёт по заданию 2. Поиск выхода из лабиринта с применением паттернов проектирования + +## 1. Постановка задачи + +Реализовать гибкую программу для загрузки лабиринта из файла, поиска пути от +старта до выхода с возможностью выбора алгоритма, текстовой визуализации +и экспериментального сравнения алгоритмов. В работе нужно применить +**не менее трёх паттернов GoF**, обосновать их выбор и продемонстрировать +преимущества такой архитектуры. + +В проекте применено **четыре паттерна**: **Builder**, **Strategy**, +**Observer** и **Command**. + +## 2. Диаграмма классов (упрощённая) + +```mermaid +classDiagram + class Cell { + +int x, y + +bool is_wall, is_start, is_exit + +int weight + +is_passable() bool + } + class Maze { + +int width, height + +Cell start, exit_ + +get_cell(x,y) Cell + +get_neighbors(cell) List~Cell~ + +render_text(path, player) str + } + + class MazeBuilder { + <> + +build_from_file(filename) Maze + } + class TextFileMazeBuilder { + +build_from_file(filename) Maze + } + + class PathFindingStrategy { + <> + +name : str + +find_path(maze, start, exit_) dict + } + class BFSStrategy + class DFSStrategy + class AStarStrategy + class DijkstraStrategy + + class MazeSolver { + -Maze maze + -PathFindingStrategy strategy + -List~Observer~ observers + +set_strategy(s) + +attach(o) + +solve() SearchStats + } + + class Observer { + <> + +update(event) + } + class ConsoleView + + class Command { + <> + +execute() + +undo() + } + class MoveCommand + class CommandHistory + class Player + + Maze "1" o-- "*" Cell + MazeBuilder <|-- TextFileMazeBuilder + TextFileMazeBuilder ..> Maze : creates + PathFindingStrategy <|-- BFSStrategy + PathFindingStrategy <|-- DFSStrategy + PathFindingStrategy <|-- AStarStrategy + PathFindingStrategy <|-- DijkstraStrategy + MazeSolver --> Maze + MazeSolver --> PathFindingStrategy + MazeSolver --> Observer + Observer <|-- ConsoleView + Command <|-- MoveCommand + CommandHistory o-- Command + MoveCommand --> Player + MoveCommand --> Maze +``` + +## 3. Паттерны и их обоснование + +### 3.1. Builder - `TextFileMazeBuilder` + +**Что делает.** Принимает имя файла, читает его, проверяет символы, ставит +координаты, создаёт `Cell`-объекты, находит `S` и `E`, валидирует +(ровно один старт и один выход) и возвращает готовый `Maze`. + +**Зачем нужен.** Конструирование лабиринта - это многошаговый процесс: +парсинг + валидация + расстановка флагов + поддержка взвешенных клеток +(`,` песок, `~` болото, `.` асфальт). Если положить всё это в конструктор +`Maze`, класс получится «толстым» и неудобным для расширения. + +**Что даёт.** Чтобы добавить новый формат (например, JSON или бинарный), +достаточно реализовать ещё один класс с тем же интерфейсом +`MazeBuilder.build_from_file`. Остальной код не меняется. + +### 3.2. Strategy - `PathFindingStrategy` + +**Что делает.** Объявляет единый интерфейс `find_path(maze, start, exit_)`. +Имеет четыре реализации: `BFSStrategy`, `DFSStrategy`, `AStarStrategy`, +`DijkstraStrategy`. Возвращают одинаковую структуру: +`{'path': [Cell, ...], 'visited': int}`. + +**Зачем нужен.** Все четыре алгоритма решают одну задачу, но с разными +компромиссами (скорость vs оптимальность vs учёт весов). Strategy позволяет +переключать их в рантайме одной строкой: + +```python +solver.set_strategy(AStarStrategy()) +``` + +без вмешательства в код решателя или модели лабиринта. + +**Что даёт.** Чтобы добавить, скажем, **двунаправленный BFS**, нужно лишь +написать новый класс - ни `MazeSolver`, ни `Maze` ничего не узнают +о нововведении. + +### 3.3. Observer - `MazeSolver` уведомляет `ConsoleView` + +**Что делает.** `MazeSolver` хранит список наблюдателей и шлёт им события: +`maze_loaded`, `search_start`, `search_end`, `path_found`, `no_path`. +`ConsoleView` подписывается и пишет в консоль. + +**Зачем нужен.** Решатель не должен знать, _кто_ и _как_ показывает +лабиринт пользователю. Можно подключить (или отключить) сразу несколько +наблюдателей - например, `ConsoleView` для отладки и `CSVLogger` +для эксперимента - не меняя `MazeSolver`. + +### 3.4. Command - `MoveCommand` с `undo` через `CommandHistory` + +**Что делает.** `MoveCommand` инкапсулирует один шаг игрока: сохраняет +предыдущую позицию, перемещает игрока в новое место. Метод `undo` +возвращает игрока обратно. `CommandHistory` ведёт стек выполненных команд +(общий undo). + +**Зачем нужен.** Ручное прохождение лабиринта = последовательность шагов, +каждый из которых должен быть откатываемым. Pattern Command даёт это +естественно и расширяемо: завтра можно добавить `MacroCommand` +(серия ходов) и `redo` - стек повторов. + +## 4. Этап 1-5: реализация + +### 4.1. Алгоритмы + +| Алгоритм | Структура данных | Учитывает веса? | Гарантирует кратчайший путь? | +| --- | --- | --- | --- | +| **BFS** | очередь (`deque`) | нет | да, по числу шагов | +| **DFS** | стек (`list`) | нет | нет | +| **A\*** | приоритетная очередь (`heapq`), эвристика - манхэттенское расстояние | **да** | да (если эвристика допустимая) | +| **Dijkstra** | приоритетная очередь | **да** | да | + +Все четыре пишут предшественников в словарь `parents`, и в конце путь +восстанавливается общей функцией `_reconstruct(...)`. + +### 4.2. Демонстрация (фрагмент вывода `demo.py`) + +``` +=== Builder: загружаем small_10x10.txt === +[ConsoleView] лабиринт 10x10 загружен +... +=== Strategy: пробуем все 4 алгоритма === +[ConsoleView] старт поиска: BFS +[ConsoleView] поиск окончен: путь=16, посещено=34, время=0.046 мс +--- BFS путь длиной 16 --- +########## +#S.......# +# ######.# +# #.# +###### #.# +# # #.# +# ## # #.# +# # #.# +# ##### .E +########## + +=== Command: пройдёмся вручную и сделаем undo === +стартовая позиция: (1,1) + move D: ok -> (2,1) + move D: ok -> (3,1) + move D: ok -> (4,1) + move D: ok -> (5,1) +Откатываем 2 хода (undo, undo): + теперь игрок в (3,1) +``` + +## 5. Этап 6. Экспериментальная часть + +### 5.1. Подготовка лабиринтов + +| Файл | Размер | Описание | +| --- | --- | --- | +| `small_10x10.txt` | 10×10 | ручной с простым путём | +| `medium_51x51.txt` | 51×51 | сгенерированный (DFS-карвер), тупики | +| `large_101x101.txt` | 101×101 | сгенерированный (DFS-карвер), запутанный | +| `empty_30x30.txt` | 30×30 | пустая комната - нет внутренних стен | +| `nopath_15x15.txt` | 15×15 | выход замурован - пути нет | +| `weighted_31x31.txt` | 31×31 | перфектный лабиринт + взвешенные клетки | +| `weighted_choice.txt` | 21×13 | **есть выбор** маршрута: через болото (короче) или вокруг (дешевле) | + +Все лабиринты генерирует `generate_mazes.py` (+ ручной `generate_weighted_choice.py`). +DFS-карвер реализован итеративно - для 101×101 рекурсивный вариант ловит +`RecursionError`. + +### 5.2. Замеры + +Для каждой пары (лабиринт × стратегия) запускали `solve()` **7 раз**, +усредняли время. Для пути и числа посещённых клеток между запусками +изменений нет (алгоритмы детерминированы) - фиксируем одно значение. + +Полные результаты - в `data/results.csv`. + +#### Сводная таблица (средние значения) + +| Лабиринт | Стратегия | t, мс | посещено | длина пути | стоимость | +| --- | --- | ---: | ---: | ---: | ---: | +| small_10x10 | BFS | 0.043 | 34 | 16 | 16 | +| | DFS | 0.015 | 18 | 16 | 16 | +| | A* | 0.043 | 27 | 16 | 16 | +| | Dijkstra | 0.044 | 33 | 16 | 16 | +| medium_51x51 | BFS | 0.50 | 524 | 353 | 353 | +| | DFS | 0.34 | 379 | 353 | 353 | +| | A* | 0.69 | 421 | 353 | 353 | +| | Dijkstra | 0.74 | 523 | 353 | 353 | +| large_101x101 | BFS | 2.08 | 2143 | 1265 | 1265 | +| | DFS | 1.35 | 1443 | 1265 | 1265 | +| | A* | 3.61 | 1831 | 1265 | 1265 | +| | Dijkstra | 3.36 | 2139 | 1265 | 1265 | +| empty_30x30 | BFS | 0.79 | 784 | **55** | 55 | +| | DFS | 0.47 | 784 | **379** | 379 | +| | A* | 1.53 | 784 | **55** | 55 | +| | Dijkstra | 1.34 | 784 | **55** | 55 | +| nopath_15x15 | все | ≈0.2 | 165 | 0 | 0 | +| weighted_31x31 | все | 0.3–0.7 | 318–433 | 265 | 391 | +| **weighted_choice** | BFS | 0.18 | 189 | **19** | **29** | +| | DFS | 0.03 | 55 | **19** | **29** | +| | A* | 0.26 | 117 | 25 | **25** | +| | Dijkstra | 0.36 | 209 | 25 | **25** | + +### 5.3. Графики + +![Время поиска](data/task2_maze/docs/data/plots/time_compare.png) + +![Сколько клеток посетил алгоритм](data/task2_maze/docs/data/plots/visited_compare.png) + +![Длина найденного пути](data/task2_maze/docs/data/plots/path_compare.png) + +![weighted_choice: длина vs стоимость](data/task2_maze/docs/data/plots/weighted_choice_compare.png) + +## 6. Анализ результатов + +### 6.1. На «обычных» перфектных лабиринтах путь единственный + +В лабиринтах, построенных DFS-карвером (`medium_51x51`, `large_101x101`, +`weighted_31x31`), между любыми двумя клетками существует **ровно один путь**. +Поэтому все четыре алгоритма находят его одинаковой длины (353, 1265, 265). +Различаются только время и **число посещённых клеток** - это и есть мера +«работы» алгоритма. + +* **DFS** - самый быстрый и обходит меньше всего клеток. Ему «везёт»: на + перфектном лабиринте он не возвращается, пока не упрётся в тупик. +* **BFS** - обходит чуть больше, потому что развивает фронт во всех направлениях. +* **A\*** и **Dijkstra** дороже по времени из-за `heapq`, но A\* экономит + посещения благодаря эвристике (на large_101x101: 1831 у A\* vs 2143 у BFS). + +### 6.2. На пустом лабиринте - главная разница между BFS/A\*/Dijkstra и DFS + +`empty_30x30` - это комната 28×28 проходимых клеток. Кратчайший путь между +противоположными углами - ровно 55 шагов. + +* BFS, A\*, Dijkstra находят его (длина = 55). +* **DFS находит путь длиной 379** - он петляет по краям комнаты, потому что + «жадно» идёт в первое попавшееся направление и никогда не возвращается, + пока не упрётся. + +Этот результат хорошо иллюстрирует: **DFS быстр, но даёт плохой путь +на открытых пространствах**. Если важна оптимальность - DFS не подходит. + +### 6.3. На взвешенном лабиринте с альтернативами - победа Dijkstra и A\* + +Лабиринт `weighted_choice` (21×13): открытая комната, в центре - болото 5×5 +(вес 3 за каждую клетку). Между стартом слева и выходом справа есть два +маршрута: +* «прямо через болото» - короче в клетках, но каждая болотная клетка стоит 3; +* «вокруг болота» - длиннее в клетках, но каждая стоит 1. + +Результаты: + +* BFS и DFS: путь **19 клеток**, **стоимость 29** (3 болотные × 3 = 9 «лишних» + единиц). +* A\* и Dijkstra: путь **25 клеток**, но **стоимость 25** - на 4 единицы + дешевле, потому что они учитывают вес клетки. + +Это и есть классическое преимущество взвешенных алгоритмов: +если шаги стоят по-разному (болото, песок, бездорожье), Dijkstra/A\* находят +оптимальный путь, а BFS/DFS - нет. + +### 6.4. На лабиринте без выхода + +`nopath_15x15`: все алгоритмы обходят все 165 проходимых клеток и возвращают +пустой путь. Время одинаковое - это, по сути, полный обход. Этот тест +показывает, что **все четыре стратегии корректно обрабатывают случай +отсутствия пути** (важная проверка). + +### 6.5. Время поиска ≠ качество пути + +Иерархия по скорости стабильна: **DFS < BFS < Dijkstra ≲ A\***. Но «быстрее» +не значит «лучше»: на `empty_30x30` DFS быстрее всех в 2 раза, но его путь +в 7 раз длиннее оптимального. На взвешенном лабиринте - BFS быстрее A\*, +но даёт более дорогой путь. + +**Вывод по алгоритмам:** + +| Когда подходит | Что выбрать | +| --- | --- | +| Минимум числа шагов на одинаковых клетках | **BFS** | +| Нужно быстро найти **хоть какой-то** путь | **DFS** | +| Взвешенный граф, есть хорошая эвристика | **A\*** (быстрее Dijkstra) | +| Взвешенный граф, эвристики нет | **Dijkstra** | + +## 7. Чем помогли паттерны + +Без паттернов код бы выглядел как один большой скрипт с `if maze_format == 'txt'` +и `if algorithm == 'bfs'`. Что я получил с паттернами: + +1. **Builder** - добавить новый формат лабиринта (JSON, графический PNG, генератор) + = новый класс, всё остальное не трогаем. +2. **Strategy** - добавить новый алгоритм (двунаправленный BFS, IDA\*) = новый + класс. `MazeSolver` не меняется. +3. **Observer** - `MazeSolver` ничего не знает про вывод. Я могу подключить + `ConsoleView` для интерактива и `CSVLogger` для эксперимента одновременно. + Эксперимент это и делает: подключает «тихого» наблюдателя. +4. **Command** - ручное прохождение и `undo` получаются естественно. + Расширить до `redo` - добавить второй стек. + +Что было бы сложно изменить без паттернов: + +* Сменить алгоритм поиска в рантайме без `if/elif`-простыни. +* Добавить второй вид визуализации (например, GUI) без затрагивания решателя. +* Поддержать сразу два формата лабиринта. + +## 8. Выводы + +* Реализованы четыре алгоритма поиска пути и четыре паттерна проектирования. +* Эксперимент подтвердил классические свойства алгоритмов: DFS быстрый, но + не оптимальный; BFS оптимален по шагам; A\*/Dijkstra оптимальны по + стоимости; A\* быстрее Dijkstra при наличии хорошей эвристики. +* Особенно выпукло разница видна на двух «диагностических» лабиринтах: + `empty_30x30` (DFS даёт «уродский» путь в 7 раз длиннее) и `weighted_choice` + (BFS/DFS режут через болото, Dijkstra/A\* обходят). +* Паттерны Builder/Strategy/Observer/Command превратили проект из «скрипта» + в расширяемое приложение. Новый формат, новый алгоритм или новый вид + визуализации добавляется без правки существующего кода + (принцип Open/Closed).