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 { <> +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 { <> +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 { <> +update(event) } class ConsoleView { +update(event) +render() } class Command { <> +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 создан"); });