From 0f511b0572894c13cef60891ac74a48cd8023afc Mon Sep 17 00:00:00 2001 From: smirnovad Date: Sun, 17 May 2026 16:50:47 +0300 Subject: [PATCH] [2] labirint --- smirnovad/lab1/docs/data/maze_performance.txt | 5 + smirnovad/lab2/docs/.vscode/launch.json | 15 + smirnovad/lab2/docs/data/command.py | 47 +++ smirnovad/lab2/docs/data/experiment.py | 74 ++++ smirnovad/lab2/docs/data/generate_mazes.py | 115 ++++++ smirnovad/lab2/docs/data/main.py | 127 +++++++ smirnovad/lab2/docs/data/make_report.js | 332 ++++++++++++++++++ smirnovad/lab2/docs/data/maze_builder.py | 51 +++ smirnovad/lab2/docs/data/maze_model.py | 55 +++ smirnovad/lab2/docs/data/maze_solver.py | 61 ++++ .../lab2/docs/data/mazes/large_50x50.txt | 50 +++ .../lab2/docs/data/mazes/medium_20x20.txt | 20 ++ smirnovad/lab2/docs/data/mazes/no_exit.txt | 5 + smirnovad/lab2/docs/data/mazes/open_20x20.txt | 20 ++ .../lab2/docs/data/mazes/small_10x10.txt | 10 + smirnovad/lab2/docs/data/observer.py | 54 +++ smirnovad/lab2/docs/data/results.csv | 17 + smirnovad/lab2/docs/data/strategies.py | 138 ++++++++ smirnovad/lab2/docs/report.docx | Bin 0 -> 15827 bytes 19 files changed, 1196 insertions(+) create mode 100644 smirnovad/lab1/docs/data/maze_performance.txt create mode 100644 smirnovad/lab2/docs/.vscode/launch.json create mode 100644 smirnovad/lab2/docs/data/command.py create mode 100644 smirnovad/lab2/docs/data/experiment.py create mode 100644 smirnovad/lab2/docs/data/generate_mazes.py create mode 100644 smirnovad/lab2/docs/data/main.py create mode 100644 smirnovad/lab2/docs/data/make_report.js create mode 100644 smirnovad/lab2/docs/data/maze_builder.py create mode 100644 smirnovad/lab2/docs/data/maze_model.py create mode 100644 smirnovad/lab2/docs/data/maze_solver.py create mode 100644 smirnovad/lab2/docs/data/mazes/large_50x50.txt create mode 100644 smirnovad/lab2/docs/data/mazes/medium_20x20.txt create mode 100644 smirnovad/lab2/docs/data/mazes/no_exit.txt create mode 100644 smirnovad/lab2/docs/data/mazes/open_20x20.txt create mode 100644 smirnovad/lab2/docs/data/mazes/small_10x10.txt create mode 100644 smirnovad/lab2/docs/data/observer.py create mode 100644 smirnovad/lab2/docs/data/results.csv create mode 100644 smirnovad/lab2/docs/data/strategies.py create mode 100644 smirnovad/lab2/docs/report.docx diff --git a/smirnovad/lab1/docs/data/maze_performance.txt b/smirnovad/lab1/docs/data/maze_performance.txt new file mode 100644 index 0000000..9a657b2 --- /dev/null +++ b/smirnovad/lab1/docs/data/maze_performance.txt @@ -0,0 +1,5 @@ +Отчет по анализу алгоритмов поиска пути +======================================== +Алгоритм: BFSStrategy +- Время выполнения: 0.0506 мс +- Просмотрено узлов: 30 diff --git a/smirnovad/lab2/docs/.vscode/launch.json b/smirnovad/lab2/docs/.vscode/launch.json new file mode 100644 index 0000000..0854bd9 --- /dev/null +++ b/smirnovad/lab2/docs/.vscode/launch.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/smirnovad/lab2/docs/data/command.py b/smirnovad/lab2/docs/data/command.py new file mode 100644 index 0000000..1613523 --- /dev/null +++ b/smirnovad/lab2/docs/data/command.py @@ -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})") diff --git a/smirnovad/lab2/docs/data/experiment.py b/smirnovad/lab2/docs/data/experiment.py new file mode 100644 index 0000000..3e03189 --- /dev/null +++ b/smirnovad/lab2/docs/data/experiment.py @@ -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}") diff --git a/smirnovad/lab2/docs/data/generate_mazes.py b/smirnovad/lab2/docs/data/generate_mazes.py new file mode 100644 index 0000000..536a38f --- /dev/null +++ b/smirnovad/lab2/docs/data/generate_mazes.py @@ -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/") diff --git a/smirnovad/lab2/docs/data/main.py b/smirnovad/lab2/docs/data/main.py new file mode 100644 index 0000000..4878e14 --- /dev/null +++ b/smirnovad/lab2/docs/data/main.py @@ -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() diff --git a/smirnovad/lab2/docs/data/make_report.js b/smirnovad/lab2/docs/data/make_report.js new file mode 100644 index 0000000..ed124bb --- /dev/null +++ b/smirnovad/lab2/docs/data/make_report.js @@ -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 { <> +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 создан"); +}); diff --git a/smirnovad/lab2/docs/data/maze_builder.py b/smirnovad/lab2/docs/data/maze_builder.py new file mode 100644 index 0000000..fdbc866 --- /dev/null +++ b/smirnovad/lab2/docs/data/maze_builder.py @@ -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) diff --git a/smirnovad/lab2/docs/data/maze_model.py b/smirnovad/lab2/docs/data/maze_model.py new file mode 100644 index 0000000..b1012bd --- /dev/null +++ b/smirnovad/lab2/docs/data/maze_model.py @@ -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) diff --git a/smirnovad/lab2/docs/data/maze_solver.py b/smirnovad/lab2/docs/data/maze_solver.py new file mode 100644 index 0000000..86ed945 --- /dev/null +++ b/smirnovad/lab2/docs/data/maze_solver.py @@ -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 diff --git a/smirnovad/lab2/docs/data/mazes/large_50x50.txt b/smirnovad/lab2/docs/data/mazes/large_50x50.txt new file mode 100644 index 0000000..60a2c26 --- /dev/null +++ b/smirnovad/lab2/docs/data/mazes/large_50x50.txt @@ -0,0 +1,50 @@ +################################################## +#S # # # # # # ## +### # # # # # ##### ##### # # ##### # ### # # # ## +# # # # # # # # # # # # # # # # ## +# ##### ####### ##### # ####### # # ### # ### # ## +# # # # # # # # # # # # # ## +# # ##### ####### ### # # ### # # ### ##### # # ## +# # # # # # # # # # # # # # ## +# # # # # ##### ### # # # # ### ######### ##### ## +# # # # # # # # # # # ## +# ### ##### # ### ####### ######### ### ##### # ## +# # # # # # # # # # # # ## +### # # ##### ##### ### ##### # # # # # # ### # ## +# # # # # # # # # # # # # # ## +# ### # ### ##### # # ### ####### # # ######### ## +# # # # # # # # # # # # # ## +# ##### # ### ##### # ######### # ##### # # # # ## +# # # # # # # # # # # # ## +# # ##### ############# # # # ####### # # ##### ## +# # # # # # # # # # # # # ## +##### # ### # ### # ##### # # # ### ### # # # # ## +# # # # # # # # # # # # # # # # ## +# # # # ##### # ##### ### ####### ####### # # # ## +# # # # # # # # # # # # ## +##### ######### # ######### # # ##### # ### # # ## +# # # # # # # # # # # # ## +# # # ### # ### ##### # ########### # # ### ### ## +# # # # # # # # # # # # # # ## +# ##### # ### # # # # ######### # ### ### # # #### +# # # # # # # # # # # # # ## +### # ######### # # ### # ### # # ##### # ### # ## +# # # # # # # # # # # # # # # ## +# ### # ### # ####### # # # ### # # # # ### ### ## +# # # # # # # # # # # # # # # # ## +# # # ### # ### # # ######### # ##### # # ### # ## +# # # # # # # # # # # # # ## +# ####### ### ####### # # # # ### # ##### ### # ## +# # # # # # # # # # # ## +# ##### ### ####### ##### ### ### ####### # ### ## +# # # # # # # # # # # ## +# ### ######### ####### ### ### ### ### ##### #### +# # # # # # # # # # # # ## +### ### ##### ### # # ### ### # ### # # # # # # ## +# # # # # # # # # # # # # # ## +# ### ### ##### # ### ##### ######### ### # # # ## +# # # # # # # # # # # # ## +# # ### ############### # ### # ### ### # # ### ## +# # # # # # # +################################################E# +################################################## \ No newline at end of file diff --git a/smirnovad/lab2/docs/data/mazes/medium_20x20.txt b/smirnovad/lab2/docs/data/mazes/medium_20x20.txt new file mode 100644 index 0000000..d834468 --- /dev/null +++ b/smirnovad/lab2/docs/data/mazes/medium_20x20.txt @@ -0,0 +1,20 @@ +#################### +#S# # ## +# # ##### # ##### ## +# # # # # ## +# # ### # ### # #### +# # # # # # ## +# ### ####### # # ## +# # # # # # ## +# # # # # # ##### ## +# # # # # ## +# # ####### # ###### +# # # # # ## +# # # ####### # # ## +# # # # # # ## +# ####### # ### # ## +# # # # # ## +##### # ##### # # ## +# # # # +##################E# +#################### \ No newline at end of file diff --git a/smirnovad/lab2/docs/data/mazes/no_exit.txt b/smirnovad/lab2/docs/data/mazes/no_exit.txt new file mode 100644 index 0000000..f7d20d8 --- /dev/null +++ b/smirnovad/lab2/docs/data/mazes/no_exit.txt @@ -0,0 +1,5 @@ +########## +#S # +# ######## +# # +########## \ No newline at end of file diff --git a/smirnovad/lab2/docs/data/mazes/open_20x20.txt b/smirnovad/lab2/docs/data/mazes/open_20x20.txt new file mode 100644 index 0000000..10bbaf0 --- /dev/null +++ b/smirnovad/lab2/docs/data/mazes/open_20x20.txt @@ -0,0 +1,20 @@ +#################### +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +#################### \ No newline at end of file diff --git a/smirnovad/lab2/docs/data/mazes/small_10x10.txt b/smirnovad/lab2/docs/data/mazes/small_10x10.txt new file mode 100644 index 0000000..5595bf5 --- /dev/null +++ b/smirnovad/lab2/docs/data/mazes/small_10x10.txt @@ -0,0 +1,10 @@ +########## +#S # +# ###### # +# # # # +# # ## # # +# # ## # # +# # # # +# ###### # +# E# +########## \ No newline at end of file diff --git a/smirnovad/lab2/docs/data/observer.py b/smirnovad/lab2/docs/data/observer.py new file mode 100644 index 0000000..b57ea87 --- /dev/null +++ b/smirnovad/lab2/docs/data/observer.py @@ -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) diff --git a/smirnovad/lab2/docs/data/results.csv b/smirnovad/lab2/docs/data/results.csv new file mode 100644 index 0000000..bb51dbe --- /dev/null +++ b/smirnovad/lab2/docs/data/results.csv @@ -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 diff --git a/smirnovad/lab2/docs/data/strategies.py b/smirnovad/lab2/docs/data/strategies.py new file mode 100644 index 0000000..aa29338 --- /dev/null +++ b/smirnovad/lab2/docs/data/strategies.py @@ -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 [] diff --git a/smirnovad/lab2/docs/report.docx b/smirnovad/lab2/docs/report.docx new file mode 100644 index 0000000000000000000000000000000000000000..c4aae52ca9fe227f11dde3683488fd2d3fbaf474 GIT binary patch literal 15827 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

rKy5*A_mw8&X_#GZK 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 D-76U7 literal 0 HcmV?d00001