Merge pull request '[2] maze' (#260) from SobolevNS/2026-rff_mp:02 into develop
Reviewed-on: #260
This commit is contained in:
commit
78eaf79bc4
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