333 lines
20 KiB
JavaScript
333 lines
20 KiB
JavaScript
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 создан");
|
||
});
|