2026-rff_mp/krasnovia/lab2/docs/data/make_report.js

341 lines
22 KiB
JavaScript
Raw Normal View History

2026-05-20 20:04:18 +00:00
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,
});
// ── Диаграмма классов (mermaid текст) ────────────────────────────────────────
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()] }),
// ── 1. ОПИСАНИЕ ЗАДАЧИ ────────────────────────────────────────────────
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()] }),
// ── 2. ДИАГРАММА КЛАССОВ ──────────────────────────────────────────────
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()] }),
// ── 3. СТРУКТУРА ПРОЕКТА ─────────────────────────────────────────────
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()] }),
// ── 4. ЭКСПЕРИМЕНТЫ ───────────────────────────────────────────────────
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()] }),
// ── 5. ПРИМЕНИМОСТЬ ПАТТЕРНОВ ─────────────────────────────────────────
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 создан");
});