2026-rff_mp/smirnovad/lab2/docs/data/make_report.js
2026-05-17 16:50:47 +03:00

333 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 создан");
});