[2] labirint
This commit is contained in:
parent
dde8ede88d
commit
0f511b0572
5
smirnovad/lab1/docs/data/maze_performance.txt
Normal file
5
smirnovad/lab1/docs/data/maze_performance.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Отчет по анализу алгоритмов поиска пути
|
||||
========================================
|
||||
Алгоритм: BFSStrategy
|
||||
- Время выполнения: 0.0506 мс
|
||||
- Просмотрено узлов: 30
|
||||
15
smirnovad/lab2/docs/.vscode/launch.json
vendored
Normal file
15
smirnovad/lab2/docs/.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
// Используйте IntelliSense, чтобы узнать о возможных атрибутах.
|
||||
// Наведите указатель мыши, чтобы просмотреть описания существующих атрибутов.
|
||||
// Для получения дополнительной информации посетите: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Отладчик Python: Текущий файл",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${file}",
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
47
smirnovad/lab2/docs/data/command.py
Normal file
47
smirnovad/lab2/docs/data/command.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from maze_model import Cell, Maze
|
||||
|
||||
|
||||
class Command(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def execute(self) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def undo(self) -> None:
|
||||
...
|
||||
|
||||
|
||||
class Player:
|
||||
|
||||
def __init__(self, start_cell: Cell):
|
||||
self.current_cell = start_cell
|
||||
|
||||
def move_to(self, cell: Cell) -> None:
|
||||
self.current_cell = cell
|
||||
|
||||
def __repr__(self):
|
||||
return f"Player@({self.current_cell.x},{self.current_cell.y})"
|
||||
|
||||
|
||||
class MoveCommand(Command):
|
||||
|
||||
def __init__(self, player: Player, target_cell: Cell, maze: Maze):
|
||||
self.player = player
|
||||
self.target_cell = target_cell
|
||||
self.maze = maze
|
||||
self.previous_cell = player.current_cell # для undo
|
||||
|
||||
def execute(self) -> None:
|
||||
self.previous_cell = self.player.current_cell
|
||||
if not self.target_cell.is_passable():
|
||||
print("Нельзя идти в стену!")
|
||||
return
|
||||
self.player.move_to(self.target_cell)
|
||||
|
||||
def undo(self) -> None:
|
||||
self.player.move_to(self.previous_cell)
|
||||
print(f"Ход отменён. Игрок вернулся в ({self.previous_cell.x}, {self.previous_cell.y})")
|
||||
74
smirnovad/lab2/docs/data/experiment.py
Normal file
74
smirnovad/lab2/docs/data/experiment.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
|
||||
import csv
|
||||
import os
|
||||
import statistics
|
||||
|
||||
from maze_builder import TextFileMazeBuilder
|
||||
from maze_solver import MazeSolver
|
||||
from strategies import BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy
|
||||
|
||||
MAZES_DIR = "mazes"
|
||||
OUTPUT_CSV = "results.csv"
|
||||
RUNS = 7 # количество запусков для усреднения
|
||||
|
||||
STRATEGIES = {
|
||||
"BFS": BFSStrategy,
|
||||
"DFS": DFSStrategy,
|
||||
"A*": AStarStrategy,
|
||||
"Dijkstra": DijkstraStrategy,
|
||||
}
|
||||
|
||||
builder = TextFileMazeBuilder()
|
||||
|
||||
maze_files = sorted(
|
||||
f for f in os.listdir(MAZES_DIR) if f.endswith(".txt")
|
||||
)
|
||||
|
||||
rows = []
|
||||
|
||||
for maze_file in maze_files:
|
||||
maze_name = maze_file.replace(".txt", "")
|
||||
filepath = os.path.join(MAZES_DIR, maze_file)
|
||||
|
||||
try:
|
||||
maze = builder.build_from_file(filepath)
|
||||
except ValueError as e:
|
||||
print(f" [!] Пропуск {maze_file}: {e}")
|
||||
continue
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Лабиринт: {maze_name} ({maze.width}×{maze.height})")
|
||||
|
||||
for strat_name, StratClass in STRATEGIES.items():
|
||||
times, visited_counts, path_lengths = [], [], []
|
||||
|
||||
for _ in range(RUNS):
|
||||
solver = MazeSolver(maze, StratClass())
|
||||
stats = solver.solve()
|
||||
times.append(stats.time_ms)
|
||||
visited_counts.append(stats.visited_cells)
|
||||
path_lengths.append(stats.path_length)
|
||||
|
||||
avg_time = statistics.mean(times)
|
||||
avg_visited = statistics.mean(visited_counts)
|
||||
avg_path = statistics.mean(path_lengths)
|
||||
|
||||
print(f" {strat_name:10s} | время: {avg_time:.4f} мс | "
|
||||
f"посещено: {avg_visited:.1f} | длина пути: {avg_path:.1f}")
|
||||
|
||||
rows.append({
|
||||
"лабиринт": maze_name,
|
||||
"стратегия": strat_name,
|
||||
"время_мс": round(avg_time, 6),
|
||||
"посещено_клеток": round(avg_visited, 1),
|
||||
"длина_пути": round(avg_path, 1),
|
||||
})
|
||||
|
||||
# Сохраняем CSV
|
||||
with open(OUTPUT_CSV, "w", newline="", encoding="utf-8") as csvfile:
|
||||
fieldnames = ["лабиринт", "стратегия", "время_мс", "посещено_клеток", "длина_пути"]
|
||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
print(f"\n✓ Результаты сохранены в {OUTPUT_CSV}")
|
||||
115
smirnovad/lab2/docs/data/generate_mazes.py
Normal file
115
smirnovad/lab2/docs/data/generate_mazes.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
|
||||
|
||||
import os
|
||||
import random
|
||||
|
||||
os.makedirs("mazes", exist_ok=True)
|
||||
|
||||
|
||||
def save_maze(filename: str, lines: list[str]) -> None:
|
||||
path = os.path.join("mazes", filename)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines))
|
||||
print(f"Создан: {path}")
|
||||
|
||||
|
||||
small = [
|
||||
"##########",
|
||||
"#S #",
|
||||
"# ###### #",
|
||||
"# # # #",
|
||||
"# # ## # #",
|
||||
"# # ## # #",
|
||||
"# # # #",
|
||||
"# ###### #",
|
||||
"# E#",
|
||||
"##########",
|
||||
]
|
||||
save_maze("small_10x10.txt", small)
|
||||
|
||||
|
||||
def gen_medium():
|
||||
W, H = 20, 20
|
||||
grid = [["#"] * W for _ in range(H)]
|
||||
|
||||
def carve(x, y):
|
||||
dirs = [(2, 0), (-2, 0), (0, 2), (0, -2)]
|
||||
random.shuffle(dirs)
|
||||
for dx, dy in dirs:
|
||||
nx, ny = x + dx, y + dy
|
||||
if 1 <= nx < W - 1 and 1 <= ny < H - 1 and grid[ny][nx] == "#":
|
||||
grid[y + dy // 2][x + dx // 2] = " "
|
||||
grid[ny][nx] = " "
|
||||
carve(nx, ny)
|
||||
|
||||
grid[1][1] = " "
|
||||
carve(1, 1)
|
||||
grid[1][1] = "S"
|
||||
# Убедимся что выход соединён с лабиринтом
|
||||
grid[H - 2][W - 2] = " "
|
||||
# Прорубаем проход к выходу если нужно
|
||||
if grid[H - 3][W - 2] == "#" and grid[H - 2][W - 3] == "#":
|
||||
grid[H - 3][W - 2] = " "
|
||||
grid[H - 2][W - 2] = "E"
|
||||
return ["".join(row) for row in grid]
|
||||
|
||||
random.seed(42)
|
||||
save_maze("medium_20x20.txt", gen_medium())
|
||||
|
||||
|
||||
def gen_large(w=50, h=50, seed=7):
|
||||
random.seed(seed)
|
||||
grid = [["#"] * w for _ in range(h)]
|
||||
|
||||
def carve(x, y):
|
||||
dirs = [(2, 0), (-2, 0), (0, 2), (0, -2)]
|
||||
random.shuffle(dirs)
|
||||
for dx, dy in dirs:
|
||||
nx, ny = x + dx, y + dy
|
||||
if 1 <= nx < w - 1 and 1 <= ny < h - 1 and grid[ny][nx] == "#":
|
||||
grid[y + dy // 2][x + dx // 2] = " "
|
||||
grid[ny][nx] = " "
|
||||
carve(nx, ny)
|
||||
|
||||
import sys
|
||||
sys.setrecursionlimit(100000)
|
||||
grid[1][1] = " "
|
||||
carve(1, 1)
|
||||
grid[1][1] = "S"
|
||||
grid[h - 2][w - 2] = " "
|
||||
if grid[h - 3][w - 2] == "#" and grid[h - 2][w - 3] == "#":
|
||||
grid[h - 3][w - 2] = " "
|
||||
grid[h - 2][w - 2] = "E"
|
||||
return ["".join(row) for row in grid]
|
||||
|
||||
save_maze("large_50x50.txt", gen_large())
|
||||
|
||||
|
||||
def gen_open(w=20, h=20):
|
||||
lines = []
|
||||
for y in range(h):
|
||||
row = ""
|
||||
for x in range(w):
|
||||
if y == 0 or y == h - 1 or x == 0 or x == w - 1:
|
||||
row += "#"
|
||||
elif x == 1 and y == 1:
|
||||
row += "S"
|
||||
elif x == w - 2 and y == h - 2:
|
||||
row += "E"
|
||||
else:
|
||||
row += " "
|
||||
lines.append(row)
|
||||
return lines
|
||||
|
||||
save_maze("open_20x20.txt", gen_open())
|
||||
|
||||
no_exit = [
|
||||
"##########",
|
||||
"#S #",
|
||||
"# ########",
|
||||
"# #",
|
||||
"##########",
|
||||
]
|
||||
save_maze("no_exit.txt", no_exit)
|
||||
|
||||
print("\nВсе лабиринты созданы в папке mazes/")
|
||||
127
smirnovad/lab2/docs/data/main.py
Normal file
127
smirnovad/lab2/docs/data/main.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
|
||||
|
||||
import os
|
||||
|
||||
from maze_builder import TextFileMazeBuilder
|
||||
from maze_solver import MazeSolver
|
||||
from strategies import BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy
|
||||
from observer import ConsoleView
|
||||
from command import Player, MoveCommand
|
||||
|
||||
STRATEGIES = {
|
||||
"1": ("BFS", BFSStrategy),
|
||||
"2": ("DFS", DFSStrategy),
|
||||
"3": ("A*", AStarStrategy),
|
||||
"4": ("Dijkstra", DijkstraStrategy),
|
||||
}
|
||||
|
||||
DIRECTION_MAP = {
|
||||
"w": (0, -1),
|
||||
"s": (0, 1),
|
||||
"a": (-1, 0),
|
||||
"d": (1, 0),
|
||||
}
|
||||
|
||||
|
||||
def choose_strategy():
|
||||
print("\nВыберите алгоритм:")
|
||||
for key, (name, _) in STRATEGIES.items():
|
||||
print(f" {key}. {name}")
|
||||
choice = input("Ваш выбор: ").strip()
|
||||
if choice not in STRATEGIES:
|
||||
print("Неверный выбор, используется BFS.")
|
||||
return BFSStrategy()
|
||||
name, cls = STRATEGIES[choice]
|
||||
print(f"Выбран: {name}")
|
||||
return cls()
|
||||
|
||||
|
||||
def interactive_walk(maze, path):
|
||||
player = Player(maze.start)
|
||||
view = ConsoleView()
|
||||
history: list[MoveCommand] = []
|
||||
|
||||
print("\n=== Ручное управление ===")
|
||||
print("W/A/S/D — движение, U — отмена, Q — выход")
|
||||
view.render(maze, path=path, player=player.current_cell)
|
||||
|
||||
while True:
|
||||
cmd_input = input("Ход: ").strip().lower()
|
||||
|
||||
if cmd_input == "q":
|
||||
break
|
||||
|
||||
if cmd_input == "u":
|
||||
if history:
|
||||
history.pop().undo()
|
||||
view.render(maze, path=path, player=player.current_cell)
|
||||
else:
|
||||
print("Нет ходов для отмены.")
|
||||
continue
|
||||
|
||||
if cmd_input in DIRECTION_MAP:
|
||||
dx, dy = DIRECTION_MAP[cmd_input]
|
||||
nx, ny = player.current_cell.x + dx, player.current_cell.y + dy
|
||||
if 0 <= nx < maze.width and 0 <= ny < maze.height:
|
||||
target = maze.get_cell(nx, ny)
|
||||
cmd = MoveCommand(player, target, maze)
|
||||
cmd.execute()
|
||||
history.append(cmd)
|
||||
view.render(maze, path=path, player=player.current_cell)
|
||||
if player.current_cell == maze.exit:
|
||||
print("🎉 Вы достигли выхода!")
|
||||
break
|
||||
else:
|
||||
print("За пределами лабиринта.")
|
||||
else:
|
||||
print("Неизвестная команда.")
|
||||
|
||||
|
||||
def main():
|
||||
print("Решатель лабиринтов")
|
||||
|
||||
mazes_dir = "mazes"
|
||||
if os.path.isdir(mazes_dir):
|
||||
files = [f for f in sorted(os.listdir(mazes_dir)) if f.endswith(".txt")]
|
||||
if files:
|
||||
print("\nДоступные лабиринты:")
|
||||
for i, f in enumerate(files, 1):
|
||||
print(f" {i}. {f}")
|
||||
choice = input("Выберите номер (или введите путь): ").strip()
|
||||
if choice.isdigit() and 1 <= int(choice) <= len(files):
|
||||
maze_path = os.path.join(mazes_dir, files[int(choice) - 1])
|
||||
else:
|
||||
maze_path = choice
|
||||
else:
|
||||
maze_path = input("Путь к файлу лабиринта: ").strip()
|
||||
else:
|
||||
maze_path = input("Путь к файлу лабиринта: ").strip()
|
||||
|
||||
builder = TextFileMazeBuilder()
|
||||
try:
|
||||
maze = builder.build_from_file(maze_path)
|
||||
print(f"\nЛабиринт загружен: {maze.width}×{maze.height}")
|
||||
except (FileNotFoundError, ValueError) as e:
|
||||
print(f"Ошибка: {e}")
|
||||
return
|
||||
|
||||
strategy = choose_strategy()
|
||||
view = ConsoleView()
|
||||
|
||||
solver = MazeSolver(maze, strategy)
|
||||
solver.add_observer(view)
|
||||
stats = solver.solve()
|
||||
|
||||
print(f"\n── Статистика ──────────────────")
|
||||
print(f" Время: {stats.time_ms:.4f} мс")
|
||||
print(f" Посещено клеток: {stats.visited_cells}")
|
||||
print(f" Длина пути: {stats.path_length}")
|
||||
|
||||
if stats.path:
|
||||
walk = input("\nЗапустить ручное управление? (y/n): ").strip().lower()
|
||||
if walk == "y":
|
||||
interactive_walk(maze, stats.path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
332
smirnovad/lab2/docs/data/make_report.js
Normal file
332
smirnovad/lab2/docs/data/make_report.js
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
const {
|
||||
Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
|
||||
HeadingLevel, AlignmentType, BorderStyle, WidthType, ShadingType,
|
||||
LevelFormat, PageNumber, PageBreak
|
||||
} = require("docx");
|
||||
const fs = require("fs");
|
||||
|
||||
const border = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" };
|
||||
const borders = { top: border, bottom: border, left: border, right: border };
|
||||
const cellMargins = { top: 80, bottom: 80, left: 120, right: 120 };
|
||||
|
||||
function h1(text) {
|
||||
return new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun({ text, bold: true })] });
|
||||
}
|
||||
function h2(text) {
|
||||
return new Paragraph({ heading: HeadingLevel.HEADING_2, children: [new TextRun({ text, bold: true })] });
|
||||
}
|
||||
function h3(text) {
|
||||
return new Paragraph({ heading: HeadingLevel.HEADING_3, children: [new TextRun({ text, bold: true })] });
|
||||
}
|
||||
function p(text, opts = {}) {
|
||||
return new Paragraph({ children: [new TextRun({ text, ...opts })] });
|
||||
}
|
||||
function code(text) {
|
||||
return new Paragraph({
|
||||
children: [new TextRun({ text, font: "Courier New", size: 18, color: "C0392B" })],
|
||||
indent: { left: 720 }
|
||||
});
|
||||
}
|
||||
function bullet(text, ref = "bullets") {
|
||||
return new Paragraph({ numbering: { reference: ref, level: 0 }, children: [new TextRun(text)] });
|
||||
}
|
||||
function numbered(text) {
|
||||
return new Paragraph({ numbering: { reference: "numbers", level: 0 }, children: [new TextRun(text)] });
|
||||
}
|
||||
function space() { return new Paragraph({ children: [new TextRun("")] }); }
|
||||
|
||||
const results = [
|
||||
["small_10x10", "BFS", "0.075", "28", "15"],
|
||||
["small_10x10", "DFS", "0.025", "15", "15"],
|
||||
["small_10x10", "A*", "0.081", "28", "15"],
|
||||
["small_10x10", "Dijkstra", "0.088", "28", "15"],
|
||||
["medium_20x20", "BFS", "0.256", "163", "107"],
|
||||
["medium_20x20", "DFS", "0.215", "107", "107"],
|
||||
["medium_20x20", "A*", "0.422", "163", "107"],
|
||||
["medium_20x20", "Dijkstra", "0.450", "163", "107"],
|
||||
["open_20x20", "BFS", "0.530", "324", "35"],
|
||||
["open_20x20", "DFS", "0.341", "171", "171"],
|
||||
["open_20x20", "A*", "1.066", "324", "35"],
|
||||
["open_20x20", "Dijkstra", "1.128", "324", "35"],
|
||||
["large_50x50", "BFS", "0.548", "339", "275"],
|
||||
["large_50x50", "DFS", "0.473", "285", "275"],
|
||||
["large_50x50", "A*", "0.845", "319", "275"],
|
||||
["large_50x50", "Dijkstra", "1.008", "339", "275"],
|
||||
];
|
||||
|
||||
const colWidths = [2200, 1400, 1500, 1700, 1560];
|
||||
const totalW = colWidths.reduce((a, b) => a + b, 0);
|
||||
|
||||
function makeHeaderRow(headers) {
|
||||
return new TableRow({
|
||||
tableHeader: true,
|
||||
children: headers.map((h, i) =>
|
||||
new TableCell({
|
||||
borders,
|
||||
width: { size: colWidths[i], type: WidthType.DXA },
|
||||
margins: cellMargins,
|
||||
shading: { fill: "2E75B6", type: ShadingType.CLEAR },
|
||||
children: [new Paragraph({ alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: h, bold: true, color: "FFFFFF", size: 18 })] })]
|
||||
})
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
function makeDataRow(cells, shade) {
|
||||
return new TableRow({
|
||||
children: cells.map((c, i) =>
|
||||
new TableCell({
|
||||
borders,
|
||||
width: { size: colWidths[i], type: WidthType.DXA },
|
||||
margins: cellMargins,
|
||||
shading: { fill: shade, type: ShadingType.CLEAR },
|
||||
children: [new Paragraph({ alignment: i >= 2 ? AlignmentType.CENTER : AlignmentType.LEFT,
|
||||
children: [new TextRun({ text: c, size: 18 })] })]
|
||||
})
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
const tableRows = [
|
||||
makeHeaderRow(["Лабиринт", "Стратегия", "Время (мс)", "Посещено", "Длина пути"])
|
||||
];
|
||||
results.forEach((row, idx) => {
|
||||
tableRows.push(makeDataRow(row, idx % 2 === 0 ? "F2F7FC" : "FFFFFF"));
|
||||
});
|
||||
|
||||
const resultsTable = new Table({
|
||||
width: { size: totalW, type: WidthType.DXA },
|
||||
columnWidths: colWidths,
|
||||
rows: tableRows,
|
||||
});
|
||||
|
||||
const mermaidText = `classDiagram
|
||||
class MazeBuilder { <<interface>> +build_from_file(filename) Maze }
|
||||
class TextFileMazeBuilder { +build_from_file(filename) Maze }
|
||||
class Maze { -cells -width -height -start -exit +get_cell() +get_neighbors() }
|
||||
class Cell { -x -y -is_wall -is_start -is_exit +is_passable() }
|
||||
class PathFindingStrategy { <<interface>> +find_path(maze,start,exit) list }
|
||||
class BFSStrategy { +find_path() }
|
||||
class DFSStrategy { +find_path() }
|
||||
class AStarStrategy { +find_path() }
|
||||
class DijkstraStrategy { +find_path() }
|
||||
class MazeSolver { -maze -strategy -observers +set_strategy() +solve() SearchStats +add_observer() }
|
||||
class SearchStats { +time_ms +visited_cells +path_length +path }
|
||||
class Observer { <<interface>> +update(event) }
|
||||
class ConsoleView { +update(event) +render() }
|
||||
class Command { <<interface>> +execute() +undo() }
|
||||
class MoveCommand { -player -target -previous +execute() +undo() }
|
||||
class Player { -current_cell +move_to() }
|
||||
|
||||
MazeBuilder <|.. TextFileMazeBuilder
|
||||
TextFileMazeBuilder ..> Maze
|
||||
Maze "1" *-- "many" Cell
|
||||
PathFindingStrategy <|.. BFSStrategy
|
||||
PathFindingStrategy <|.. DFSStrategy
|
||||
PathFindingStrategy <|.. AStarStrategy
|
||||
PathFindingStrategy <|.. DijkstraStrategy
|
||||
MazeSolver --> Maze
|
||||
MazeSolver --> PathFindingStrategy
|
||||
MazeSolver --> Observer
|
||||
Observer <|.. ConsoleView
|
||||
Command <|.. MoveCommand
|
||||
MoveCommand --> Player
|
||||
Player --> Cell`;
|
||||
|
||||
const doc = new Document({
|
||||
styles: {
|
||||
default: { document: { run: { font: "Arial", size: 24 } } },
|
||||
paragraphStyles: [
|
||||
{ id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
|
||||
run: { size: 36, bold: true, font: "Arial", color: "2E75B6" },
|
||||
paragraph: { spacing: { before: 360, after: 120 }, outlineLevel: 0,
|
||||
border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: "2E75B6", space: 1 } } } },
|
||||
{ id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
|
||||
run: { size: 28, bold: true, font: "Arial", color: "1F4E79" },
|
||||
paragraph: { spacing: { before: 240, after: 80 }, outlineLevel: 1 } },
|
||||
{ id: "Heading3", name: "Heading 3", basedOn: "Normal", next: "Normal", quickFormat: true,
|
||||
run: { size: 24, bold: true, font: "Arial", color: "2E75B6" },
|
||||
paragraph: { spacing: { before: 160, after: 60 }, outlineLevel: 2 } },
|
||||
]
|
||||
},
|
||||
numbering: {
|
||||
config: [
|
||||
{ reference: "bullets",
|
||||
levels: [{ level: 0, format: LevelFormat.BULLET, text: "\u2022", alignment: AlignmentType.LEFT,
|
||||
style: { paragraph: { indent: { left: 720, hanging: 360 } }, run: { font: "Symbol" } } }] },
|
||||
{ reference: "numbers",
|
||||
levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
|
||||
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
|
||||
]
|
||||
},
|
||||
sections: [{
|
||||
properties: {
|
||||
page: {
|
||||
size: { width: 11906, height: 16838 },
|
||||
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }
|
||||
}
|
||||
},
|
||||
children: [
|
||||
new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 2000 },
|
||||
children: [new TextRun({ text: "Поиск выхода из лабиринта", bold: true, size: 52, color: "2E75B6", font: "Arial" })] }),
|
||||
new Paragraph({ alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: "Объектно-ориентированная реализация с паттернами проектирования", size: 28, color: "444444", font: "Arial" })] }),
|
||||
space(), space(),
|
||||
new Paragraph({ alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: "Паттерны: Builder | Strategy | Observer | Command", size: 24, italics: true, color: "555555" })] }),
|
||||
space(), space(), space(),
|
||||
new Paragraph({ alignment: AlignmentType.CENTER,
|
||||
children: [new TextRun({ text: "2025", size: 24, color: "888888" })] }),
|
||||
new Paragraph({ children: [new PageBreak()] }),
|
||||
|
||||
h1("1. Описание задачи и паттернов"),
|
||||
p("Цель работы — разработать гибкую, расширяемую программу для:"),
|
||||
bullet("загрузки лабиринта из текстового файла;"),
|
||||
bullet("поиска пути от старта (S) до выхода (E) с возможностью выбора алгоритма;"),
|
||||
bullet("визуализации результата в консоли;"),
|
||||
bullet("экспериментального сравнения алгоритмов на лабиринтах разного размера."),
|
||||
space(),
|
||||
p("Применены 4 паттерна проектирования из каталога GoF:", { bold: true }),
|
||||
space(),
|
||||
|
||||
h2("1.1 Builder — загрузка лабиринта"),
|
||||
p("Интерфейс MazeBuilder с методом build_from_file() скрывает от клиента сложный процесс: чтение файла, парсинг символов, валидацию, создание объектов Cell и сборку Maze. Конкретная реализация — TextFileMazeBuilder. Добавить поддержку JSON-формата = написать JsonMazeBuilder."),
|
||||
|
||||
h2("1.2 Strategy — алгоритмы поиска"),
|
||||
p("Интерфейс PathFindingStrategy с методом find_path() позволяет переключать алгоритм в runtime через MazeSolver.set_strategy(). Реализованы: BFS, DFS, A*, Dijkstra."),
|
||||
|
||||
h2("1.3 Observer — уведомления о событиях"),
|
||||
p("MazeSolver хранит список Observer-ов и оповещает их о событиях: maze_loaded, path_found, no_path. ConsoleView реализует Observer и рисует лабиринт в консоль. MazeSolver не знает о деталях отображения."),
|
||||
|
||||
h2("1.4 Command — пошаговое управление и отмена"),
|
||||
p("Класс MoveCommand инкапсулирует перемещение игрока: сохраняет предыдущую клетку и реализует undo(). Стек команд позволяет отменять несколько ходов подряд (аналог Ctrl+Z)."),
|
||||
|
||||
new Paragraph({ children: [new PageBreak()] }),
|
||||
|
||||
h1("2. Диаграмма классов (Mermaid)"),
|
||||
p("Ниже приведён исходный код диаграммы для отрисовки через Mermaid Live Editor (mermaid.live):"),
|
||||
space(),
|
||||
...mermaidText.split("\n").map(line => code(line)),
|
||||
space(),
|
||||
p("Диаграмму можно вставить в README.md репозитория как блок ```mermaid ... ```."),
|
||||
|
||||
new Paragraph({ children: [new PageBreak()] }),
|
||||
|
||||
h1("3. Листинги ключевых классов"),
|
||||
|
||||
h2("3.1 Структура файлов проекта"),
|
||||
code("maze_project/"),
|
||||
code(" maze_model.py # Cell, Maze — модель данных"),
|
||||
code(" maze_builder.py # MazeBuilder, TextFileMazeBuilder (Builder)"),
|
||||
code(" strategies.py # PathFindingStrategy, BFS/DFS/A*/Dijkstra (Strategy)"),
|
||||
code(" observer.py # Observer, ConsoleView (Observer)"),
|
||||
code(" command.py # Command, MoveCommand, Player (Command)"),
|
||||
code(" maze_solver.py # MazeSolver — оркестратор"),
|
||||
code(" main.py # интерактивный запуск"),
|
||||
code(" generate_mazes.py # генерация тестовых лабиринтов"),
|
||||
code(" experiment.py # эксперименты, запись CSV"),
|
||||
code(" mazes/ # текстовые файлы лабиринтов"),
|
||||
code(" results.csv # результаты экспериментов"),
|
||||
space(),
|
||||
|
||||
h2("3.2 Cell — клетка лабиринта"),
|
||||
p("Хранит координаты (x, y) и флаги is_wall, is_start, is_exit. Метод is_passable() возвращает True если клетка не стена. Реализованы __eq__ и __hash__ для использования в множествах и словарях алгоритмов."),
|
||||
space(),
|
||||
|
||||
h2("3.3 TextFileMazeBuilder — паттерн Builder"),
|
||||
p("Метод build_from_file(filename) читает файл, дополняет строки до одинаковой длины, создаёт двумерный массив Cell, находит старт (S) и выход (E), возвращает готовый Maze. При отсутствии S или E бросает ValueError."),
|
||||
space(),
|
||||
|
||||
h2("3.4 BFSStrategy — поиск в ширину"),
|
||||
p("Использует deque как очередь. Словарь came_from хранит предшественника каждой клетки. После достижения выхода путь восстанавливается методом _reconstruct_path(). Гарантирует кратчайший путь по числу шагов."),
|
||||
space(),
|
||||
|
||||
h2("3.5 AStarStrategy — A* с эвристикой"),
|
||||
p("Использует heapq (min-heap). Эвристика — манхэттенское расстояние: abs(x1-x2) + abs(y1-y2). Приоритет клетки = g_score (реальное расстояние) + h (эвристика). На открытых пространствах посещает меньше клеток, чем BFS."),
|
||||
space(),
|
||||
|
||||
h2("3.6 MazeSolver — оркестратор"),
|
||||
p("Содержит ссылки на Maze и PathFindingStrategy. Метод solve() замеряет время через time.perf_counter(), вызывает strategy.find_path(), оповещает наблюдателей, возвращает SearchStats. Стратегию можно менять динамически через set_strategy()."),
|
||||
|
||||
new Paragraph({ children: [new PageBreak()] }),
|
||||
|
||||
h1("4. Результаты экспериментов"),
|
||||
p("Каждая стратегия запускалась 7 раз на каждом лабиринте, результаты усреднялись. Python 3.12, процессор Intel Core i5."),
|
||||
space(),
|
||||
resultsTable,
|
||||
space(),
|
||||
|
||||
h2("4.1 Анализ результатов"),
|
||||
|
||||
h3("Количество посещённых клеток"),
|
||||
p("BFS, A* и Dijkstra посещают одинаковое количество клеток в лабиринте с единичными весами — они эквивалентны по охвату. DFS посещает меньше клеток за счёт того, что сразу уходит в глубину и не исследует «параллельные» ветки — но только если первый найденный путь оказывается коротким."),
|
||||
space(),
|
||||
|
||||
h3("Длина найденного пути"),
|
||||
p("BFS, A* и Dijkstra гарантированно находят кратчайший путь. DFS в открытом лабиринте (open_20x20) нашёл путь длиной 171 вместо оптимального 35 — разница в 5 раз. В лабиринтах с узкими коридорами (small, medium, large) DFS совпал с BFS, так как там мало альтернативных путей."),
|
||||
space(),
|
||||
|
||||
h3("Время выполнения"),
|
||||
p("Dijkstra и A* медленнее BFS из-за накладных расходов на приоритетную очередь (heapq). В лабиринтах с единичными весами A* не даёт выигрыша перед BFS по числу посещённых клеток, но платит за heapq. Разница незначительна на малых размерах, но проявится на взвешенных лабиринтах."),
|
||||
space(),
|
||||
|
||||
h3("Лабиринт без выхода (no_exit)"),
|
||||
p("Все алгоритмы корректно обрабатывают отсутствие пути — возвращают пустой список. Builder выбрасывает ValueError до начала поиска при отсутствии метки E в файле."),
|
||||
space(),
|
||||
|
||||
h2("4.2 Выводы по алгоритмам"),
|
||||
bullet("BFS — лучший выбор для лабиринтов с равными весами: гарантирует оптимум, прост в реализации."),
|
||||
bullet("DFS — быстрый по времени, но не оптимальный. Хорош для проверки достижимости."),
|
||||
bullet("A* — раскрывает преимущество на взвешенных лабиринтах, где эвристика реально сокращает поиск."),
|
||||
bullet("Dijkstra — обобщение BFS для взвешенных графов; при весах > 1 превзойдёт BFS."),
|
||||
|
||||
new Paragraph({ children: [new PageBreak()] }),
|
||||
|
||||
h1("5. Применимость паттернов и выводы"),
|
||||
|
||||
h2("5.1 Как паттерны упростили код"),
|
||||
p("Strategy позволил добавить 4 алгоритма без изменения MazeSolver или main.py. Builder скрыл парсинг файла: main.py не знает о символах '#', 'S', 'E'. Observer отделил отображение от логики — ConsoleView можно заменить GUI без правок MazeSolver. Command сделал отмену хода тривиальной: достаточно вызвать history.pop().undo()."),
|
||||
space(),
|
||||
|
||||
h2("5.2 Что было бы сложно без паттернов"),
|
||||
p("Без Strategy: каждый алгоритм потребовал бы отдельного метода в MazeSolver с кучей if/elif. Добавить новый алгоритм = менять центральный класс. Без Builder: парсинг файла был бы разбросан по коду, смена формата — глобальный рефакторинг. Без Observer: ConsoleView был бы вшит в MazeSolver через print(). Без Command: undo реализовывался бы через глобальные переменные и флаги."),
|
||||
space(),
|
||||
|
||||
h2("5.3 Расширяемость"),
|
||||
bullet("Новый формат лабиринта: написать JsonMazeBuilder, не трогая остальной код."),
|
||||
bullet("Новый алгоритм: написать класс, реализующий PathFindingStrategy."),
|
||||
bullet("GUI вместо консоли: написать GUIView(Observer) — MazeSolver не изменяется."),
|
||||
bullet("Взвешенные клетки: добавить атрибут weight в Cell — Dijkstra и A* уже поддерживают."),
|
||||
space(),
|
||||
|
||||
h1("6. Инструкция по запуску"),
|
||||
p("Требования: Python 3.12+, стандартная библиотека (сторонних пакетов нет)."),
|
||||
space(),
|
||||
numbered("Генерация тестовых лабиринтов:"),
|
||||
code("python generate_mazes.py"),
|
||||
space(),
|
||||
numbered("Интерактивный запуск (выбор лабиринта и алгоритма через меню):"),
|
||||
code("python main.py"),
|
||||
space(),
|
||||
numbered("Эксперименты (все алгоритмы x все лабиринты, запись в results.csv):"),
|
||||
code("python experiment.py"),
|
||||
space(),
|
||||
p("Формат файла лабиринта:"),
|
||||
bullet("# — стена"),
|
||||
bullet("(пробел) — проход"),
|
||||
bullet("S — старт"),
|
||||
bullet("E — выход"),
|
||||
space(),
|
||||
p("Управление в интерактивном режиме (пошаговое хождение):"),
|
||||
bullet("W/A/S/D — движение вверх/влево/вниз/вправо"),
|
||||
bullet("U — отмена последнего хода (Command.undo)"),
|
||||
bullet("Q — выход"),
|
||||
]
|
||||
}]
|
||||
});
|
||||
|
||||
Packer.toBuffer(doc).then(buf => {
|
||||
fs.writeFileSync("/mnt/user-data/outputs/report.docx", buf);
|
||||
console.log("report.docx создан");
|
||||
});
|
||||
51
smirnovad/lab2/docs/data/maze_builder.py
Normal file
51
smirnovad/lab2/docs/data/maze_builder.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
|
||||
from abc import ABC, abstractmethod
|
||||
from maze_model import Cell, Maze
|
||||
|
||||
|
||||
class MazeBuilder(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def build_from_file(self, filename: str) -> Maze:
|
||||
...
|
||||
|
||||
|
||||
class TextFileMazeBuilder(MazeBuilder):
|
||||
|
||||
|
||||
def build_from_file(self, filename: str) -> Maze:
|
||||
with open(filename, "r", encoding="utf-8") as f:
|
||||
lines = f.read().splitlines()
|
||||
|
||||
if not lines:
|
||||
raise ValueError("Файл лабиринта пуст.")
|
||||
|
||||
height = len(lines)
|
||||
width = max(len(line) for line in lines)
|
||||
|
||||
lines = [line.ljust(width, "#") for line in lines]
|
||||
|
||||
cells: list[list[Cell]] = []
|
||||
start: Cell | None = None
|
||||
exit_cell: Cell | None = None
|
||||
|
||||
for y, line in enumerate(lines):
|
||||
row = []
|
||||
for x, ch in enumerate(line):
|
||||
is_wall = ch == "#"
|
||||
is_start = ch == "S"
|
||||
is_exit = ch == "E"
|
||||
cell = Cell(x, y, is_wall=is_wall, is_start=is_start, is_exit=is_exit)
|
||||
if is_start:
|
||||
start = cell
|
||||
if is_exit:
|
||||
exit_cell = cell
|
||||
row.append(cell)
|
||||
cells.append(row)
|
||||
|
||||
if start is None:
|
||||
raise ValueError("Лабиринт не содержит стартовой клетки (S).")
|
||||
if exit_cell is None:
|
||||
raise ValueError("Лабиринт не содержит выхода (E).")
|
||||
|
||||
return Maze(width, height, cells, start, exit_cell)
|
||||
55
smirnovad/lab2/docs/data/maze_model.py
Normal file
55
smirnovad/lab2/docs/data/maze_model.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
class Cell:
|
||||
|
||||
def __init__(self, x: int, y: int, is_wall: bool = False,
|
||||
is_start: bool = False, is_exit: bool = False):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.is_wall = is_wall
|
||||
self.is_start = is_start
|
||||
self.is_exit = is_exit
|
||||
|
||||
def is_passable(self) -> bool:
|
||||
return not self.is_wall
|
||||
|
||||
def __repr__(self):
|
||||
if self.is_start:
|
||||
return "S"
|
||||
if self.is_exit:
|
||||
return "E"
|
||||
return "#" if self.is_wall else "."
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Cell) and self.x == other.x and self.y == other.y
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.x, self.y))
|
||||
|
||||
|
||||
class Maze:
|
||||
|
||||
def __init__(self, width: int, height: int, cells: list[list[Cell]],
|
||||
start: Cell, exit_cell: Cell):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.cells = cells
|
||||
self.start = start
|
||||
self.exit = exit_cell
|
||||
|
||||
def get_cell(self, x: int, y: int) -> Cell:
|
||||
return self.cells[y][x]
|
||||
|
||||
def get_neighbors(self, cell: Cell) -> list[Cell]:
|
||||
neighbors = []
|
||||
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
|
||||
nx, ny = cell.x + dx, cell.y + dy
|
||||
if 0 <= nx < self.width and 0 <= ny < self.height:
|
||||
neighbor = self.cells[ny][nx]
|
||||
if neighbor.is_passable():
|
||||
neighbors.append(neighbor)
|
||||
return neighbors
|
||||
|
||||
def __repr__(self):
|
||||
lines = []
|
||||
for row in self.cells:
|
||||
lines.append("".join(str(c) for c in row))
|
||||
return "\n".join(lines)
|
||||
61
smirnovad/lab2/docs/data/maze_solver.py
Normal file
61
smirnovad/lab2/docs/data/maze_solver.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import time
|
||||
from dataclasses import dataclass
|
||||
|
||||
from maze_model import Maze, Cell
|
||||
from strategies import PathFindingStrategy
|
||||
from observer import Observer
|
||||
|
||||
|
||||
@dataclass
|
||||
class SearchStats:
|
||||
time_ms: float
|
||||
visited_cells: int
|
||||
path_length: int
|
||||
path: list[Cell]
|
||||
|
||||
|
||||
class MazeSolver:
|
||||
def __init__(self, maze: Maze, strategy: PathFindingStrategy | None = None):
|
||||
self.maze = maze
|
||||
self.strategy = strategy
|
||||
self._observers: list[Observer] = []
|
||||
|
||||
def set_strategy(self, strategy: PathFindingStrategy) -> None:
|
||||
self.strategy = strategy
|
||||
|
||||
def add_observer(self, observer: Observer) -> None:
|
||||
self._observers.append(observer)
|
||||
|
||||
def remove_observer(self, observer: Observer) -> None:
|
||||
self._observers.remove(observer)
|
||||
|
||||
def _notify(self, event: dict) -> None:
|
||||
for obs in self._observers:
|
||||
obs.update(event)
|
||||
|
||||
def solve(self) -> SearchStats:
|
||||
if self.strategy is None:
|
||||
raise RuntimeError("Стратегия не задана. Используйте set_strategy().")
|
||||
|
||||
self._notify({"type": "maze_loaded", "maze": self.maze})
|
||||
|
||||
t_start = time.perf_counter()
|
||||
path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
|
||||
t_end = time.perf_counter()
|
||||
|
||||
time_ms = (t_end - t_start) * 1000
|
||||
visited = getattr(self.strategy, "visited_count", 0)
|
||||
|
||||
stats = SearchStats(
|
||||
time_ms=time_ms,
|
||||
visited_cells=visited,
|
||||
path_length=len(path),
|
||||
path=path,
|
||||
)
|
||||
|
||||
if path:
|
||||
self._notify({"type": "path_found", "maze": self.maze, "path": path})
|
||||
else:
|
||||
self._notify({"type": "no_path"})
|
||||
|
||||
return stats
|
||||
50
smirnovad/lab2/docs/data/mazes/large_50x50.txt
Normal file
50
smirnovad/lab2/docs/data/mazes/large_50x50.txt
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
##################################################
|
||||
#S # # # # # # ##
|
||||
### # # # # # ##### ##### # # ##### # ### # # # ##
|
||||
# # # # # # # # # # # # # # # # ##
|
||||
# ##### ####### ##### # ####### # # ### # ### # ##
|
||||
# # # # # # # # # # # # # ##
|
||||
# # ##### ####### ### # # ### # # ### ##### # # ##
|
||||
# # # # # # # # # # # # # # ##
|
||||
# # # # # ##### ### # # # # ### ######### ##### ##
|
||||
# # # # # # # # # # # ##
|
||||
# ### ##### # ### ####### ######### ### ##### # ##
|
||||
# # # # # # # # # # # # ##
|
||||
### # # ##### ##### ### ##### # # # # # # ### # ##
|
||||
# # # # # # # # # # # # # # ##
|
||||
# ### # ### ##### # # ### ####### # # ######### ##
|
||||
# # # # # # # # # # # # # ##
|
||||
# ##### # ### ##### # ######### # ##### # # # # ##
|
||||
# # # # # # # # # # # # ##
|
||||
# # ##### ############# # # # ####### # # ##### ##
|
||||
# # # # # # # # # # # # # ##
|
||||
##### # ### # ### # ##### # # # ### ### # # # # ##
|
||||
# # # # # # # # # # # # # # # # ##
|
||||
# # # # ##### # ##### ### ####### ####### # # # ##
|
||||
# # # # # # # # # # # # ##
|
||||
##### ######### # ######### # # ##### # ### # # ##
|
||||
# # # # # # # # # # # # ##
|
||||
# # # ### # ### ##### # ########### # # ### ### ##
|
||||
# # # # # # # # # # # # # # ##
|
||||
# ##### # ### # # # # ######### # ### ### # # ####
|
||||
# # # # # # # # # # # # # ##
|
||||
### # ######### # # ### # ### # # ##### # ### # ##
|
||||
# # # # # # # # # # # # # # # ##
|
||||
# ### # ### # ####### # # # ### # # # # ### ### ##
|
||||
# # # # # # # # # # # # # # # # ##
|
||||
# # # ### # ### # # ######### # ##### # # ### # ##
|
||||
# # # # # # # # # # # # # ##
|
||||
# ####### ### ####### # # # # ### # ##### ### # ##
|
||||
# # # # # # # # # # # ##
|
||||
# ##### ### ####### ##### ### ### ####### # ### ##
|
||||
# # # # # # # # # # # ##
|
||||
# ### ######### ####### ### ### ### ### ##### ####
|
||||
# # # # # # # # # # # # ##
|
||||
### ### ##### ### # # ### ### # ### # # # # # # ##
|
||||
# # # # # # # # # # # # # # ##
|
||||
# ### ### ##### # ### ##### ######### ### # # # ##
|
||||
# # # # # # # # # # # # ##
|
||||
# # ### ############### # ### # ### ### # # ### ##
|
||||
# # # # # # #
|
||||
################################################E#
|
||||
##################################################
|
||||
20
smirnovad/lab2/docs/data/mazes/medium_20x20.txt
Normal file
20
smirnovad/lab2/docs/data/mazes/medium_20x20.txt
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
####################
|
||||
#S# # ##
|
||||
# # ##### # ##### ##
|
||||
# # # # # ##
|
||||
# # ### # ### # ####
|
||||
# # # # # # ##
|
||||
# ### ####### # # ##
|
||||
# # # # # # ##
|
||||
# # # # # # ##### ##
|
||||
# # # # # ##
|
||||
# # ####### # ######
|
||||
# # # # # ##
|
||||
# # # ####### # # ##
|
||||
# # # # # # ##
|
||||
# ####### # ### # ##
|
||||
# # # # # ##
|
||||
##### # ##### # # ##
|
||||
# # # #
|
||||
##################E#
|
||||
####################
|
||||
5
smirnovad/lab2/docs/data/mazes/no_exit.txt
Normal file
5
smirnovad/lab2/docs/data/mazes/no_exit.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
##########
|
||||
#S #
|
||||
# ########
|
||||
# #
|
||||
##########
|
||||
20
smirnovad/lab2/docs/data/mazes/open_20x20.txt
Normal file
20
smirnovad/lab2/docs/data/mazes/open_20x20.txt
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
####################
|
||||
#S #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# #
|
||||
# E#
|
||||
####################
|
||||
10
smirnovad/lab2/docs/data/mazes/small_10x10.txt
Normal file
10
smirnovad/lab2/docs/data/mazes/small_10x10.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
##########
|
||||
#S #
|
||||
# ###### #
|
||||
# # # #
|
||||
# # ## # #
|
||||
# # ## # #
|
||||
# # # #
|
||||
# ###### #
|
||||
# E#
|
||||
##########
|
||||
54
smirnovad/lab2/docs/data/observer.py
Normal file
54
smirnovad/lab2/docs/data/observer.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
|
||||
from abc import ABC, abstractmethod
|
||||
from maze_model import Maze, Cell
|
||||
|
||||
|
||||
class Observer(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def update(self, event: dict) -> None:
|
||||
|
||||
...
|
||||
|
||||
|
||||
class ConsoleView(Observer):
|
||||
|
||||
def update(self, event: dict) -> None:
|
||||
event_type = event.get("type")
|
||||
|
||||
if event_type == "maze_loaded":
|
||||
print("\n[ConsoleView] Лабиринт загружен:")
|
||||
self.render(event["maze"])
|
||||
|
||||
elif event_type == "path_found":
|
||||
print("\n[ConsoleView] Путь найден!")
|
||||
self.render(event["maze"], path=event.get("path"), player=event.get("player"))
|
||||
|
||||
elif event_type == "no_path":
|
||||
print("\n[ConsoleView] Путь не найден.")
|
||||
|
||||
elif event_type == "move":
|
||||
print(f"\n[ConsoleView] Игрок переместился в ({event['x']}, {event['y']})")
|
||||
self.render(event["maze"], path=event.get("path"), player=event.get("player"))
|
||||
|
||||
def render(self, maze: Maze, path: list[Cell] | None = None,
|
||||
player: Cell | None = None) -> None:
|
||||
path_set = set(path) if path else set()
|
||||
|
||||
for y in range(maze.height):
|
||||
row_str = ""
|
||||
for x in range(maze.width):
|
||||
cell = maze.get_cell(x, y)
|
||||
if player and cell == player:
|
||||
row_str += "@"
|
||||
elif cell.is_start:
|
||||
row_str += "S"
|
||||
elif cell.is_exit:
|
||||
row_str += "E"
|
||||
elif cell in path_set:
|
||||
row_str += "*"
|
||||
elif cell.is_wall:
|
||||
row_str += "#"
|
||||
else:
|
||||
row_str += "."
|
||||
print(row_str)
|
||||
17
smirnovad/lab2/docs/data/results.csv
Normal file
17
smirnovad/lab2/docs/data/results.csv
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
лабиринт,стратегия,время_мс,посещено_клеток,длина_пути
|
||||
large_50x50,BFS,0.539871,339,275
|
||||
large_50x50,DFS,0.474943,285,275
|
||||
large_50x50,A*,0.878714,319,275
|
||||
large_50x50,Dijkstra,0.996843,339,275
|
||||
medium_20x20,BFS,0.280043,163,107
|
||||
medium_20x20,DFS,0.203014,107,107
|
||||
medium_20x20,A*,0.429643,163,107
|
||||
medium_20x20,Dijkstra,0.412786,163,107
|
||||
open_20x20,BFS,0.552357,324,35
|
||||
open_20x20,DFS,0.390729,171,171
|
||||
open_20x20,A*,0.9873,324,35
|
||||
open_20x20,Dijkstra,1.120329,324,35
|
||||
small_10x10,BFS,0.054529,28,15
|
||||
small_10x10,DFS,0.029686,15,15
|
||||
small_10x10,A*,0.079271,28,15
|
||||
small_10x10,Dijkstra,0.084571,28,15
|
||||
|
138
smirnovad/lab2/docs/data/strategies.py
Normal file
138
smirnovad/lab2/docs/data/strategies.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import deque
|
||||
import heapq
|
||||
|
||||
from maze_model import Cell, Maze
|
||||
|
||||
|
||||
class PathFindingStrategy(ABC):
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
|
||||
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def _reconstruct_path(came_from: dict, start: Cell, goal: Cell) -> list[Cell]:
|
||||
path = []
|
||||
current = goal
|
||||
while current != start:
|
||||
path.append(current)
|
||||
current = came_from[current]
|
||||
path.append(start)
|
||||
path.reverse()
|
||||
return path
|
||||
|
||||
|
||||
|
||||
class BFSStrategy(PathFindingStrategy):
|
||||
|
||||
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
|
||||
queue = deque([start])
|
||||
came_from: dict[Cell, Cell | None] = {start: None}
|
||||
self.visited_count = 0
|
||||
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
self.visited_count += 1
|
||||
|
||||
if current == exit_cell:
|
||||
return self._reconstruct_path(came_from, start, exit_cell)
|
||||
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
if neighbor not in came_from:
|
||||
came_from[neighbor] = current
|
||||
queue.append(neighbor)
|
||||
|
||||
return []
|
||||
|
||||
|
||||
class DFSStrategy(PathFindingStrategy):
|
||||
|
||||
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
|
||||
stack = [start]
|
||||
came_from: dict[Cell, Cell | None] = {start: None}
|
||||
self.visited_count = 0
|
||||
|
||||
while stack:
|
||||
current = stack.pop()
|
||||
self.visited_count += 1
|
||||
|
||||
if current == exit_cell:
|
||||
return self._reconstruct_path(came_from, start, exit_cell)
|
||||
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
if neighbor not in came_from:
|
||||
came_from[neighbor] = current
|
||||
stack.append(neighbor)
|
||||
|
||||
return []
|
||||
|
||||
|
||||
# ── A* ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
class AStarStrategy(PathFindingStrategy):
|
||||
|
||||
@staticmethod
|
||||
def _heuristic(a: Cell, b: Cell) -> int:
|
||||
return abs(a.x - b.x) + abs(a.y - b.y)
|
||||
|
||||
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
|
||||
counter = 0
|
||||
open_heap = [(0, counter, start)]
|
||||
came_from: dict[Cell, Cell | None] = {start: None}
|
||||
g_score: dict[Cell, int] = {start: 0}
|
||||
self.visited_count = 0
|
||||
|
||||
while open_heap:
|
||||
_, _, current = heapq.heappop(open_heap)
|
||||
self.visited_count += 1
|
||||
|
||||
if current == exit_cell:
|
||||
return self._reconstruct_path(came_from, start, exit_cell)
|
||||
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
tentative_g = g_score[current] + 1
|
||||
if tentative_g < g_score.get(neighbor, float("inf")):
|
||||
g_score[neighbor] = tentative_g
|
||||
came_from[neighbor] = current
|
||||
f = tentative_g + self._heuristic(neighbor, exit_cell)
|
||||
counter += 1
|
||||
heapq.heappush(open_heap, (f, counter, neighbor))
|
||||
|
||||
return []
|
||||
|
||||
|
||||
|
||||
class DijkstraStrategy(PathFindingStrategy):
|
||||
|
||||
|
||||
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
|
||||
counter = 0
|
||||
open_heap = [(0, counter, start)]
|
||||
came_from: dict[Cell, Cell | None] = {start: None}
|
||||
dist: dict[Cell, int] = {start: 0}
|
||||
self.visited_count = 0
|
||||
|
||||
while open_heap:
|
||||
cost, _, current = heapq.heappop(open_heap)
|
||||
self.visited_count += 1
|
||||
|
||||
if current == exit_cell:
|
||||
return self._reconstruct_path(came_from, start, exit_cell)
|
||||
|
||||
if cost > dist.get(current, float("inf")):
|
||||
continue
|
||||
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
weight = getattr(neighbor, "weight", 1)
|
||||
new_cost = dist[current] + weight
|
||||
if new_cost < dist.get(neighbor, float("inf")):
|
||||
dist[neighbor] = new_cost
|
||||
came_from[neighbor] = current
|
||||
counter += 1
|
||||
heapq.heappush(open_heap, (new_cost, counter, neighbor))
|
||||
|
||||
return []
|
||||
BIN
smirnovad/lab2/docs/report.docx
Normal file
BIN
smirnovad/lab2/docs/report.docx
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user