[2] maze #260
33
SobolevNS/docs/data/task2_maze/README.md
Normal file
33
SobolevNS/docs/data/task2_maze/README.md
Normal file
|
|
@ -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` | выход (ровно один) |
|
||||
54
SobolevNS/docs/data/task2_maze/demo.py
Normal file
54
SobolevNS/docs/data/task2_maze/demo.py
Normal file
|
|
@ -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()
|
||||
BIN
SobolevNS/docs/data/task2_maze/docs/data/plots/path_compare.png
Normal file
BIN
SobolevNS/docs/data/task2_maze/docs/data/plots/path_compare.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
BIN
SobolevNS/docs/data/task2_maze/docs/data/plots/time_compare.png
Normal file
BIN
SobolevNS/docs/data/task2_maze/docs/data/plots/time_compare.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
228
SobolevNS/docs/data/task2_maze/docs/data/results.csv
Normal file
228
SobolevNS/docs/data/task2_maze/docs/data/results.csv
Normal file
|
|
@ -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
|
||||
|
Can't render this file because it has a wrong number of fields in line 199.
|
82
SobolevNS/docs/data/task2_maze/experiment.py
Normal file
82
SobolevNS/docs/data/task2_maze/experiment.py
Normal file
|
|
@ -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()
|
||||
146
SobolevNS/docs/data/task2_maze/generate_mazes.py
Normal file
146
SobolevNS/docs/data/task2_maze/generate_mazes.py
Normal file
|
|
@ -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()
|
||||
19
SobolevNS/docs/data/task2_maze/generate_weighted_choice.py
Normal file
19
SobolevNS/docs/data/task2_maze/generate_weighted_choice.py
Normal file
|
|
@ -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())
|
||||
18
SobolevNS/docs/data/task2_maze/maze_solver/__init__.py
Normal file
18
SobolevNS/docs/data/task2_maze/maze_solver/__init__.py
Normal file
|
|
@ -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",
|
||||
]
|
||||
92
SobolevNS/docs/data/task2_maze/maze_solver/builder.py
Normal file
92
SobolevNS/docs/data/task2_maze/maze_solver/builder.py
Normal file
|
|
@ -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}).")
|
||||
87
SobolevNS/docs/data/task2_maze/maze_solver/command.py
Normal file
87
SobolevNS/docs/data/task2_maze/maze_solver/command.py
Normal file
|
|
@ -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()
|
||||
92
SobolevNS/docs/data/task2_maze/maze_solver/model.py
Normal file
92
SobolevNS/docs/data/task2_maze/maze_solver/model.py
Normal file
|
|
@ -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)
|
||||
102
SobolevNS/docs/data/task2_maze/maze_solver/solver.py
Normal file
102
SobolevNS/docs/data/task2_maze/maze_solver/solver.py
Normal file
|
|
@ -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
|
||||
179
SobolevNS/docs/data/task2_maze/maze_solver/strategies.py
Normal file
179
SobolevNS/docs/data/task2_maze/maze_solver/strategies.py
Normal file
|
|
@ -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,
|
||||
}
|
||||
30
SobolevNS/docs/data/task2_maze/mazes/empty_30x30.txt
Normal file
30
SobolevNS/docs/data/task2_maze/mazes/empty_30x30.txt
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
##############################
|
||||
#S #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# E#
|
||||
##############################
|
||||
101
SobolevNS/docs/data/task2_maze/mazes/large_101x101.txt
Normal file
101
SobolevNS/docs/data/task2_maze/mazes/large_101x101.txt
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
#####################################################################################################
|
||||
#S# # # # # # # # # # # # # # #
|
||||
# ### # ### ### # # # ##### # ####### # ### # # ### # # ######### ### # # # ### ### # # # # ### #####
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
####### # # # ### # # ### ##### ### ##### # ####### # ######### # # ### ######### # ##### ### # ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ##### # ### ### # ##################### ### # # # ####### # ####### ####### # ### # # ### # # # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # ########### ########### # # # # ####### ### # ##### # ####### ##### ##### # # # # # # # ### # ###
|
||||
# # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # # # ####### # ##### ### ##### # ##### # ### # # ### # ######### ### ##### # ##### # ######### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### ### # ### # ### # ########### ##### ### # ### # ### # ### ### # # # ### ##### #################
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # ### ##### # # ### ########### ### # ### ### # ##### # ####### ##### ### ######### ### # ####### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### ####### # # # ### # ### ##### # ####### ##### # ####### # # # ##### ######### # # # # ### # ###
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
### ####### ### # # # ##### # # ### ### # # # ##### # # ### # ##### # ##### ### # # # # ####### ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### ### ### ######### # ####### ### # ### ####### ##### ########### # # ### ### ##### # # ### # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # ### # ##### # # # # ### # # # # ########### ####### # # ### # ### # ##### # ### ##### ### ### # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### ##### ##### ### # # ####### ########### # # ### # ##### # ### ####### # ### ### # ### ### ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
### # # # ### # ### # # # # # # ##### ### ### ##### ##### ####### # ### ### # # ####### ######### # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### ##### ##### ##### # ##### # # ### ### ##### # ####### # ##### # # # # ### ##### ####### # #####
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # ##### ### # ### # # ### # ####### ### # # # # ##### # # # # # # ##### ############# ### ### ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### # ### ##### # # # # ### # ### ####### # # ### # ### ##### ##### # ### # ########### ### ### ###
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
### # ### ### # ##################### # # ### ### ##### # # # ### # ####### # # # ##### # # ### ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # ####### ############# # # # ### ####### ### ##### ### # # ####### # ####### ### # ### # # ### # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### ### # # # # ########### # # # # ####### # ### ### ### ####### # ####### # ### ### # # ### ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
####### ### # # ######### # ##### ##### ### ### # ### # # ##### # ### # ### ### # ### ####### ### # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # ### ### # ### ##### # ### ### ### ### ### ### # ####### # ##### # ### ######### # # ### ####### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # ### ### ##### ##### ##### ##### # ##### # ### # ### # ##### # ######### # ### ### # ### ### # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # ### # ####### # # # ### # ##### ### # # ### # ##### # ### ######### # # # ### ####### ####### # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ####### # ### ### # ######### # # # ### ### ### # ######### # # ### ##### ####### ### ##### # # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # ####### ### ####### # ######### ### # ### ########### ### ### ##### ### ### # # # # ######### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### # ####### ##### # ### # ##### ### # # ############# ### ### ##### ### ### ##### # ### # # # ###
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
### # # ### ####### # # # ##### # ### ### ##### # # ####### # # # # # ####### ### ### # # ### ##### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ####### ### # # ### ### ######### # ##### # # ### # ### # # # # # ### ######### # # ##### # # # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # ### ### ### ####### ##### ####### # ##### # # ##### # ########### ######### ### ##### # ##### # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # #
|
||||
##### ### # # ### ### ##### # ##### ##### ### # ######### # # # # # # ##### ##### ##### # ### # ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ##### # # # ### # ##### # ####### ### # ##### # # # ##### # ##### ### # ### ##### ####### # ##### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
##### ##### ### ### # ########### ### # ##### ### ##### # # # ### ######### # # # # # ### ### ### ###
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### # # # ############# # ####### ####### # # ### # ######### ##### # # ####### # # # # # ### ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
### ##### ### # # # # # ##### ##### ### # # ##### # ##### # ##### # ### ##### # ##### # ##### # ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### # # # ### # ######### ### # ### # ############### # ### # # ######### ##### # ### ### # # # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # ####### # # # # # ##### ####### # # ####### # # ##### # # # # # ### # ### # # # ### ### # # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
### ############# ############### # ### # ####### # # # ####### # ### # ##### ### # ########### # ###
|
||||
# # # # # # # # # # # # # # # # # #
|
||||
# ####### ####### # ####### # # ##### ### ##### ### ##### # ##### # ### ##### # ### # ############# #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ##### ### ### # # ### # ######### # ##### ##### # # # ##### ### ### # # # ########### ### ### ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # ######### ### ######### # ### # # # ##### # # # # ### ### # ### # ##### ### # ### ### # # # #####
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # ### # # # ### # ### ### # # ### ####### # ######### ####### # ##### ####### ### ########### ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### ##### ### ### ### # ### ### # ##### ##### # # # ##### # ##### ####### # ### ########### ### # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # ##### ### # # ### ### # ### # ##### ####### ####### ### # ####### ##### # # ### # # ### ### #####
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # # # # ####### ### # ####### # ##### ### ######### # ##### ##### # # # # # # # # ### ### # ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # # ##### # ### # # ########### ##### # # ### # # # ##### # ### ### # # # # ####### # ##### # # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### # # # ### ### ##### # # ### # ####### # # # ##### # # ### # # # # ##### # # # # ######### # # #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# ### # # # # # ##### # # # ### # ### ##### # ##### # ##### # ##### ##### # ### # ##### # ### ##### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
### # # # ### # # ######### # # ### # # ##### ### ####### ### ### # # ##### # ####### ### # # # ### #
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||
# # ### ####### ##### # # ### # ##### # # # ######### # # ##### ### ##### ##### ### ### ### # ### # #
|
||||
# # # # # # # # # # # # #E#
|
||||
#####################################################################################################
|
||||
51
SobolevNS/docs/data/task2_maze/mazes/medium_51x51.txt
Normal file
51
SobolevNS/docs/data/task2_maze/mazes/medium_51x51.txt
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
###################################################
|
||||
#S# # # # # # # #
|
||||
# # # ### # ######### # ### # # # # ####### ### ###
|
||||
# # # # # # # # # # # # #
|
||||
# # ### # ############### # ######### # ##### # # #
|
||||
# # # # # # # # # # # # #
|
||||
##### ### # ##### # ####### # ##### # ### ### ### #
|
||||
# # # # # # # # # # # # # # #
|
||||
# # # # ##### # ####### # # ### # # ####### ### # #
|
||||
# # # # # # # # # # # # #
|
||||
# ####### # ##### # ######### ### # # ########### #
|
||||
# # # # # # # # # # # #
|
||||
# ########### # ### ### ####### # ##### # ### # ###
|
||||
# # # # # # # # # # #
|
||||
# # ### ### ##### ### # ##### ############# ##### #
|
||||
# # # # # # # # # # # # #
|
||||
##### ### # ### # # # ### # ######### ##### # # # #
|
||||
# # # # # # # # # # # #
|
||||
# ##### ################# ####### # # # # ##### # #
|
||||
# # # # # # # # # # # #
|
||||
### # ### ############# ##### # # # ### ##### ### #
|
||||
# # # # # # # # # # # # # # #
|
||||
# # # # ##### # ### # ######### ### # ### # # # # #
|
||||
# # # # # # # # # # # # # #
|
||||
# ### ##### # ######### # ### ### # ######### # ###
|
||||
# # # # # # # # # # # # #
|
||||
### ### ##### ### # ##### # ##### # # # # # #######
|
||||
# # # # # # # # # # # # # # #
|
||||
# ##### # # ### ### # # ##### # ### # ####### ### #
|
||||
# # # # # # # # # # # # # # #
|
||||
# # # ####### # # ####### # ##### ##### # ##### # #
|
||||
# # # # # # # # # # # # # #
|
||||
####### ### ##### # # # # # # # # # ########### # #
|
||||
# # # # # # # # # # # # # # # #
|
||||
# ### # # ### ### # # # # # # # # ####### ##### # #
|
||||
# # # # # # # # # # # # # # # # #
|
||||
# # ##### # # # ##### # # # # # ####### ### ##### #
|
||||
# # # # # # # # # # # # # # #
|
||||
# # # # ############### # # ####### ##### ### # # #
|
||||
# # # # # # # # # # # #
|
||||
# # # ##### # ####### # # ################# # #####
|
||||
# # # # # # # # # # # # # #
|
||||
# # ### # ##### # # ### # ### # # ####### # ##### #
|
||||
# # # # # # # # # # # # # # # #
|
||||
# # # # ### # # # ################# # # # # # #####
|
||||
# # # # # # # # # # # #
|
||||
# # ### # ####### # ### ### ################# # # #
|
||||
# # # # # # # # # # # # #
|
||||
# ### ############# # ### ####### ##### # # ##### #
|
||||
# # # # E#
|
||||
###################################################
|
||||
15
SobolevNS/docs/data/task2_maze/mazes/nopath_15x15.txt
Normal file
15
SobolevNS/docs/data/task2_maze/mazes/nopath_15x15.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
###############
|
||||
#S #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# ###
|
||||
# #E#
|
||||
###############
|
||||
10
SobolevNS/docs/data/task2_maze/mazes/small_10x10.txt
Normal file
10
SobolevNS/docs/data/task2_maze/mazes/small_10x10.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
##########
|
||||
#S #
|
||||
# ###### #
|
||||
# # #
|
||||
###### # #
|
||||
# # # #
|
||||
# ## # # #
|
||||
# # # #
|
||||
# ##### E
|
||||
##########
|
||||
31
SobolevNS/docs/data/task2_maze/mazes/weighted_31x31.txt
Normal file
31
SobolevNS/docs/data/task2_maze/mazes/weighted_31x31.txt
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
###############################
|
||||
#S # ~ ,, # ~#, ,,#
|
||||
### #####~### ###,#,#####,#,# #
|
||||
# # # ~~#, ,# #,# #~# #,#
|
||||
#~#,# # ### #,###,##### ### # #
|
||||
# # ,#, # # # , # ,#,~ #~# #
|
||||
# ####### #,#~# ### #~###,# #~#
|
||||
# ~,,# # # #~# #~ , # #
|
||||
# ##### #,####### # # ####### #
|
||||
#, # ,# ,, ,~#, # ~ ~~# #,#
|
||||
### ########### # ####### # # #
|
||||
# ~ #, , ~# # # #, #,, # #,#
|
||||
#,###,###,# # # # ### # ### #,#
|
||||
#, , , # #, ,#,#, ,~# # ,#~#
|
||||
# ####### ###,# ####### # ### #
|
||||
#, #, ~ #~, # ~ , # ~~#
|
||||
###~#~# ################# #####
|
||||
#~ ,# # #,, ,,, ~, ,# , #,#,~,#
|
||||
# ###,###,##### ###,### # # ###
|
||||
# , # #~ # , # ,# # , #
|
||||
### ####### # ###,#####~# #~# #
|
||||
# # ,, #,# # ~, # # #,# #
|
||||
# ###########,# ##### ### # #~#
|
||||
#, ,#~, ,# # ,# , #~# #
|
||||
# ### ### ##### # ##### ##### #
|
||||
#~#,# # , ~# #~ # , , #
|
||||
# # ### ##### #,###########,#,#
|
||||
# # # ,, #~ ,# ,, # # #~#
|
||||
# ### #,#######,# ###,# # #,# #
|
||||
#~, , # , ,~, # ,#~ ,#E#
|
||||
###############################
|
||||
13
SobolevNS/docs/data/task2_maze/mazes/weighted_choice.txt
Normal file
13
SobolevNS/docs/data/task2_maze/mazes/weighted_choice.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#####################
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# ~~~~~ #
|
||||
# ~~~~~ #
|
||||
#S ~~~~~ E#
|
||||
# ~~~~~ #
|
||||
# ~~~~~ #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
#####################
|
||||
99
SobolevNS/docs/data/task2_maze/plot_results.py
Normal file
99
SobolevNS/docs/data/task2_maze/plot_results.py
Normal file
|
|
@ -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()
|
||||
364
SobolevNS/docs/report_02.md
Normal file
364
SobolevNS/docs/report_02.md
Normal file
|
|
@ -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 {
|
||||
<<abstract>>
|
||||
+build_from_file(filename) Maze
|
||||
}
|
||||
class TextFileMazeBuilder {
|
||||
+build_from_file(filename) Maze
|
||||
}
|
||||
|
||||
class PathFindingStrategy {
|
||||
<<abstract>>
|
||||
+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 {
|
||||
<<abstract>>
|
||||
+update(event)
|
||||
}
|
||||
class ConsoleView
|
||||
|
||||
class Command {
|
||||
<<abstract>>
|
||||
+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. Графики
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 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).
|
||||
Loading…
Reference in New Issue
Block a user