diff --git a/krasnovia/lab1/docs/data/lab1.py b/krasnovia/lab1/docs/data/lab1.py new file mode 100644 index 0000000..ef15ae5 --- /dev/null +++ b/krasnovia/lab1/docs/data/lab1.py @@ -0,0 +1,249 @@ +import time +import random +import csv +import os +import sys +import matplotlib.pyplot as plt + +sys.setrecursionlimit(20000) + +BASE_PATH = r"E:\репозиторий\2026-rff_mp\krasnovia\lab1" +DOCS_PATH = os.path.join(BASE_PATH, "docs") +DATA_PATH = os.path.join(DOCS_PATH, "data") + +for p in [DOCS_PATH, DATA_PATH]: + if not os.path.exists(p): + os.makedirs(p) + +def ll_insert(head, name, phone): + return {'name': name, 'phone': phone, 'next': head} + +def ll_find(head, name): + curr = head + while curr: + if curr['name'] == name: return curr['phone'] + curr = curr['next'] + return None + +def ll_delete(head, name): + if not head: return None + if head['name'] == name: return head['next'] + curr = head + while curr['next']: + if curr['next']['name'] == name: + curr['next'] = curr['next']['next'] + return head + curr = curr['next'] + return head + +def ll_list_all(head): + res = [] + curr = head + while curr: + res.append((curr['name'], curr['phone'])) + curr = curr['next'] + return sorted(res) + +def ht_insert(buckets, name, phone): + idx = hash(name) % len(buckets) + buckets[idx] = ll_insert(buckets[idx], name, phone) + +def ht_find(buckets, name): + idx = hash(name) % len(buckets) + return ll_find(buckets[idx], name) + +def ht_delete(buckets, name): + idx = hash(name) % len(buckets) + buckets[idx] = ll_delete(buckets[idx], name) + +def ht_list_all(buckets): + all_recs = [] + for b in buckets: + curr = b + while curr: + all_recs.append((curr['name'], curr['phone'])) + curr = curr['next'] + return sorted(all_recs) + +def bst_insert(root, name, phone): + if not root: + return {'name': name, 'phone': phone, 'left': None, 'right': None} + if name < root['name']: + root['left'] = bst_insert(root['left'], name, phone) + elif name > root['name']: + root['right'] = bst_insert(root['right'], name, phone) + else: + root['phone'] = phone + return root + +def bst_find(root, name): + if not root: return None + if root['name'] == name: return root['phone'] + if name < root['name']: return bst_find(root['left'], name) + return bst_find(root['right'], name) + +def bst_delete(root, name): + if not root: return None + if name < root['name']: + root['left'] = bst_delete(root['left'], name) + elif name > root['name']: + root['right'] = bst_delete(root['right'], name) + else: + if not root['left']: return root['right'] + if not root['right']: return root['left'] + temp = root['right'] + while temp['left']: temp = temp['left'] + root['name'], root['phone'] = temp['name'], temp['phone'] + root['right'] = bst_delete(root['right'], temp['name']) + return root + +def bst_list_all(root): + res = [] + def _inorder(node): + if node: + _inorder(node['left']) + res.append((node['name'], node['phone'])) + _inorder(node['right']) + _inorder(root) + return res + +all_results_csv = [] +summary_for_report = [] + +def run_experiment(struct_type, mode, data): + print(f"Processing: {struct_type} ({mode})") + ins_times, find_times, del_times = [], [], [] + + for i in range(5): + container = [None]*1000 if struct_type == "HashTable" else None + + start = time.perf_counter() + for n, p in data: + if struct_type == "LinkedList": container = ll_insert(container, n, p) + elif struct_type == "HashTable": ht_insert(container, n, p) + elif struct_type == "BST": container = bst_insert(container, n, p) + ins_times.append(time.perf_counter() - start) + + search_list = [d[0] for d in random.sample(data, 100)] + [f"None_{j}" for j in range(10)] + start = time.perf_counter() + for s_name in search_list: + if struct_type == "LinkedList": ll_find(container, s_name) + elif struct_type == "HashTable": ht_find(container, s_name) + elif struct_type == "BST": bst_find(container, s_name) + find_times.append(time.perf_counter() - start) + + del_list = [d[0] for d in random.sample(data, 50)] + start = time.perf_counter() + for d_name in del_list: + if struct_type == "LinkedList": container = ll_delete(container, d_name) + elif struct_type == "HashTable": ht_delete(container, d_name) + elif struct_type == "BST": container = bst_delete(container, d_name) + del_times.append(time.perf_counter() - start) + + all_results_csv.append([struct_type, mode, f"Run {i+1}", ins_times[-1], find_times[-1], del_times[-1]]) + + avg_ins = sum(ins_times) / 5 + avg_find = sum(find_times) / 5 + avg_del = sum(del_times) / 5 + + all_results_csv.append([struct_type, mode, "AVERAGE", avg_ins, avg_find, avg_del]) + summary_for_report.append({"name": struct_type, "mode": mode, "ins": avg_ins, "find": avg_find, "del": avg_del}) + +N = 10000 +records_raw = [(f"User_{i:05d}", f"8-900-{random.randint(100, 999)}") for i in range(N)] +records_shuffled = records_raw[:] +random.shuffle(records_shuffled) +records_sorted = sorted(records_raw) + +for m_name, d_set in [("случайный", records_shuffled), ("сортированный", records_sorted)]: + for s_type in ["LinkedList", "HashTable", "BST"]: + run_experiment(s_type, m_name, d_set) + +with open(os.path.join(DATA_PATH, "results.csv"), "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerow(["Структура", "Режим", "Итерация", "Вставка", "Поиск", "Удаление"]) + writer.writerows(all_results_csv) + +def create_plots(): + labels = ["insert", "find", "delete"] + structs = ["LinkedList", "HashTable", "BST"] + colors = ['#5dade2', '#e67e22', '#58d68d'] + + fig1, axs = plt.subplots(1, 3, figsize=(18, 6)) + fig1.suptitle("Влияние порядка данных на время операций", fontsize=16, fontweight='bold') + + for i, s_name in enumerate(structs): + rand_data = next(r for r in summary_for_report if r['name'] == s_name and r['mode'] == "случайный") + sort_data = next(r for r in summary_for_report if r['name'] == s_name and r['mode'] == "сортированный") + + x = [0, 1, 2] + width = 0.35 + axs[i].bar([p - width/2 for p in x], [rand_data['ins'], rand_data['find'], rand_data['del']], width, label='случайный', color=colors[0]) + axs[i].bar([p + width/2 for p in x], [sort_data['ins'], sort_data['find'], sort_data['del']], width, label='сортированный', color='#e74c3c', alpha=0.8) + + axs[i].set_title(s_name, fontweight='bold') + axs[i].set_xticks(x) + axs[i].set_xticklabels(labels) + axs[i].set_ylabel("Время (с)") + axs[i].legend() + axs[i].grid(axis='y', linestyle='--', alpha=0.3) + + plt.tight_layout(rect=[0, 0.03, 1, 0.95]) + plt.savefig(os.path.join(DATA_PATH, "order_impact.png")) + + fig2, axs2 = plt.subplots(1, 3, figsize=(18, 6)) + fig2.suptitle(f"Сравнение структур данных (N={N})", fontsize=16, fontweight='bold') + + op_keys = ['ins', 'find', 'del'] + op_names = ['insert', 'find', 'delete'] + + for i, op in enumerate(op_keys): + plot_labels = [] + plot_values = [] + plot_colors = [] + + for r in summary_for_report: + plot_labels.append(f"{r['name']}\n({r['mode'][:4]})") + plot_values.append(r[op]) + if r['name'] == "LinkedList": plot_colors.append(colors[0]) + elif r['name'] == "HashTable": plot_colors.append(colors[1]) + else: plot_colors.append(colors[2]) + + bars = axs2[i].bar(plot_labels, plot_values, color=plot_colors) + axs2[i].set_title(f"Операция: {op_names[i]}", fontweight='bold') + axs2[i].set_ylabel("Время (с)") + axs2[i].tick_params(axis='x', rotation=15) + + for bar in bars: + height = bar.get_height() + axs2[i].text(bar.get_x() + bar.get_width()/2., height, f'{height:.4f}', ha='center', va='bottom', fontsize=8) + + plt.tight_layout(rect=[0, 0.03, 1, 0.95]) + plt.savefig(os.path.join(DATA_PATH, "struct_comparison.png")) + +create_plots() + +with open(os.path.join(DOCS_PATH, "report.md"), "w", encoding="utf-8") as f: + f.write("# Технический отчет: Сравнительный анализ структур данных\n\n") + f.write("## 1. Вводные данные\n") + f.write(f"Целью теста является оценка производительности LinkedList, HashTable и BST на массиве из {N} элементов. ") + f.write("Анализировались сценарии со случайным распределением и предварительной сортировкой ключей.\n\n") + + f.write("## 2. Результаты измерений (AVG)\n") + f.write("| Алгоритм | Входные данные | Вставка (с) | Поиск (с) | Удаление (с) |\n") + f.write("| :--- | :--- | :--- | :--- | :--- |\n") + for r in summary_for_report: + f.write(f"| {r['name']} | {r['mode']} | {r['ins']:.6f} | {r['find']:.6f} | {r['del']:.6f} |\n") + + f.write("\n## 3. Визуальный анализ\n") + f.write("### Сравнение по типам операций\n![Сравнение](data/struct_comparison.png)\n\n") + f.write("### Влияние упорядоченности на производительность\n![Влияние порядка](data/order_impact.png)\n\n") + + f.write("## 4. Экспертные выводы\n") + f.write("- **Эффект вырождения BST:** На отсортированных последовательностях BST демонстрирует критический рост времени выполнения (деградация до $O(N)$). ") + f.write("Это связано с отсутствием балансировки, превращающим дерево в линейный список.\n") + f.write("- **Инвариантность HashTable:** Хеш-таблица показывает наиболее стабильные результаты. Скорость доступа не коррелирует с порядком входных данных.\n") + f.write("- **Линейная сложность LinkedList:** Связный список предсказуемо неэффективен при поиске, так как требует итерации по всей глубине структуры.\n") + f.write("- **Итоговая оценка:** Для систем с высокой интенсивностью поиска и вставки оптимальным выбором является HashTable.") + +print("Готово.") \ No newline at end of file diff --git a/krasnovia/lab1/docs/data/order_impact.png b/krasnovia/lab1/docs/data/order_impact.png new file mode 100644 index 0000000..e39a5cc Binary files /dev/null and b/krasnovia/lab1/docs/data/order_impact.png differ diff --git a/krasnovia/lab1/docs/data/results.csv b/krasnovia/lab1/docs/data/results.csv new file mode 100644 index 0000000..48e1492 --- /dev/null +++ b/krasnovia/lab1/docs/data/results.csv @@ -0,0 +1,37 @@ +Структура,Режим,Итерация,Вставка,Поиск,Удаление +LinkedList,случайный,Run 1,0.0036254000006010756,0.06929340001079254,0.040904800000134856 +LinkedList,случайный,Run 2,0.002705999999307096,0.09314480000466574,0.038945499996771105 +LinkedList,случайный,Run 3,0.0035097999934805557,0.0652599999884842,0.03580480000528041 +LinkedList,случайный,Run 4,0.004379200006951578,0.060941299991100095,0.04131999998935498 +LinkedList,случайный,Run 5,0.003272000001743436,0.06662459998915438,0.03727009998692665 +LinkedList,случайный,AVERAGE,0.0034984800004167482,0.07105281999683939,0.0388490399956936 +HashTable,случайный,Run 1,0.007146899995859712,0.00018819999240804464,8.869999146554619e-05 +HashTable,случайный,Run 2,0.006990299996687099,0.00013760000001639128,8.589999924879521e-05 +HashTable,случайный,Run 3,0.007395300010102801,0.0001466999965487048,8.320000779349357e-05 +HashTable,случайный,Run 4,0.007719999994151294,0.00023800000781193376,0.00013099999341648072 +HashTable,случайный,Run 5,0.007573700000648387,0.00018960000306833535,0.00011110000195913017 +HashTable,случайный,AVERAGE,0.007365239999489859,0.00018001999997068195,9.997999877668917e-05 +BST,случайный,Run 1,0.04626839999400545,0.00047990000166464597,0.00024569999368395656 +BST,случайный,Run 2,0.0475841000006767,0.0004754999972647056,0.00026119999529328197 +BST,случайный,Run 3,0.046892100013792515,0.0004844000068260357,0.0002472000051056966 +BST,случайный,Run 4,0.047048299995367415,0.0004321000014897436,0.00024170000688172877 +BST,случайный,Run 5,0.04865149999386631,0.0004741000011563301,0.00025040000036824495 +BST,случайный,AVERAGE,0.04728887999954168,0.0004692000016802922,0.00024924000026658175 +LinkedList,сортированный,Run 1,0.004157000003033318,0.08125500001187902,0.044403499996406026 +LinkedList,сортированный,Run 2,0.0029534000059356913,0.06697529999655671,0.04485000000568107 +LinkedList,сортированный,Run 3,0.002979500000947155,0.06968830000550952,0.04757019999669865 +LinkedList,сортированный,Run 4,0.003208699999959208,0.06227809999836609,0.03610610000032466 +LinkedList,сортированный,Run 5,0.002962500002468005,0.06485759999486618,0.03632800000195857 +LinkedList,сортированный,AVERAGE,0.0032522200024686755,0.0690108600014355,0.0418515600002138 +HashTable,сортированный,Run 1,0.006838200002675876,0.00020619999850168824,0.00011320000339765102 +HashTable,сортированный,Run 2,0.006913500008522533,0.00015800000983290374,8.230000094044954e-05 +HashTable,сортированный,Run 3,0.006470899999840185,0.00016349999350495636,8.939999679569155e-05 +HashTable,сортированный,Run 4,0.0065700999984983355,0.00014420000661630183,8.969999908003956e-05 +HashTable,сортированный,Run 5,0.006396099997800775,0.00014509999891743064,9.229998977389187e-05 +HashTable,сортированный,AVERAGE,0.006637760001467541,0.00016340000147465616,9.33799979975447e-05 +BST,сортированный,Run 1,19.100887599997805,0.17849370000476483,0.09569349999947008 +BST,сортированный,Run 2,19.370542799995746,0.15886150000733323,0.11082600000372622 +BST,сортированный,Run 3,19.196645500007435,0.17154130000562873,0.1037713999976404 +BST,сортированный,Run 4,19.184918099999777,0.16993090001051314,0.11102890000620391 +BST,сортированный,Run 5,19.424080700002378,0.16240569998626597,0.0897938999987673 +BST,сортированный,AVERAGE,19.255414940000627,0.1682466200029012,0.10222274000116158 diff --git a/krasnovia/lab1/docs/data/struct_comparison.png b/krasnovia/lab1/docs/data/struct_comparison.png new file mode 100644 index 0000000..508d310 Binary files /dev/null and b/krasnovia/lab1/docs/data/struct_comparison.png differ diff --git a/krasnovia/lab1/docs/report.md b/krasnovia/lab1/docs/report.md new file mode 100644 index 0000000..3b25b3f --- /dev/null +++ b/krasnovia/lab1/docs/report.md @@ -0,0 +1,27 @@ +# Технический отчет: Сравнительный анализ структур данных + +## 1. Вводные данные +Целью теста является оценка производительности LinkedList, HashTable и BST на массиве из 10000 элементов. Анализировались сценарии со случайным распределением и предварительной сортировкой ключей. + +## 2. Результаты измерений (AVG) +| Алгоритм | Входные данные | Вставка (с) | Поиск (с) | Удаление (с) | +| :--- | :--- | :--- | :--- | :--- | +| LinkedList | случайный | 0.003498 | 0.071053 | 0.038849 | +| HashTable | случайный | 0.007365 | 0.000180 | 0.000100 | +| BST | случайный | 0.047289 | 0.000469 | 0.000249 | +| LinkedList | сортированный | 0.003252 | 0.069011 | 0.041852 | +| HashTable | сортированный | 0.006638 | 0.000163 | 0.000093 | +| BST | сортированный | 19.255415 | 0.168247 | 0.102223 | + +## 3. Визуальный анализ +### Сравнение по типам операций +![Сравнение](data/struct_comparison.png) + +### Влияние упорядоченности на производительность +![Влияние порядка](data/order_impact.png) + +## 4. Экспертные выводы +- **Эффект вырождения BST:** На отсортированных последовательностях BST демонстрирует критический рост времени выполнения (деградация до $O(N)$). Это связано с отсутствием балансировки, превращающим дерево в линейный список. +- **Инвариантность HashTable:** Хеш-таблица показывает наиболее стабильные результаты. Скорость доступа не коррелирует с порядком входных данных. +- **Линейная сложность LinkedList:** Связный список предсказуемо неэффективен при поиске, так как требует итерации по всей глубине структуры. +- **Итоговая оценка:** Для систем с высокой интенсивностью поиска и вставки оптимальным выбором является HashTable. \ No newline at end of file diff --git a/krasnovia/lab2/docs/data/command.py b/krasnovia/lab2/docs/data/command.py new file mode 100644 index 0000000..ef2bc76 --- /dev/null +++ b/krasnovia/lab2/docs/data/command.py @@ -0,0 +1,61 @@ +""" +Этап 5.2: Паттерн Command — пошаговое перемещение игрока с отменой хода. + +Зачем Command? +Каждое перемещение инкапсулировано в объекте. Это позволяет: +- хранить историю ходов +- отменять последний ход (undo) через Ctrl+Z +- повторять ходы +""" + +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/krasnovia/lab2/docs/data/experiment.py b/krasnovia/lab2/docs/data/experiment.py new file mode 100644 index 0000000..6d60e01 --- /dev/null +++ b/krasnovia/lab2/docs/data/experiment.py @@ -0,0 +1,82 @@ +""" +Этап 6: Экспериментальная часть. + +Запускает все стратегии на всех лабиринтах 7 раз, +усредняет результаты и сохраняет в results.csv. + +Запуск: python experiment.py +""" + +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/krasnovia/lab2/docs/data/generate_mazes.py b/krasnovia/lab2/docs/data/generate_mazes.py new file mode 100644 index 0000000..368a065 --- /dev/null +++ b/krasnovia/lab2/docs/data/generate_mazes.py @@ -0,0 +1,125 @@ +""" +Генерирует тестовые лабиринты в папку mazes/. + +Запуск: python generate_mazes.py +""" + +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}") + + +# ── 1. Маленький 10×10 с простым путём ─────────────────────────────────────── +small = [ + "##########", + "#S #", + "# ###### #", + "# # # #", + "# # ## # #", + "# # ## # #", + "# # # #", + "# ###### #", + "# E#", + "##########", +] +save_maze("small_10x10.txt", small) + + +# ── 2. Средний 20×20 с тупиками ────────────────────────────────────────────── +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()) + + +# ── 3. Большой 50×50 с запутанной структурой ───────────────────────────────── +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()) + + +# ── 4. «Пустой» лабиринт (без стен внутри) ─────────────────────────────────── +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()) + + +# ── 5. Лабиринт без выхода ─────────────────────────────────────────────────── +no_exit = [ + "##########", + "#S #", + "# ########", + "# #", + "##########", +] +save_maze("no_exit.txt", no_exit) + +print("\nВсе лабиринты созданы в папке mazes/") diff --git a/krasnovia/lab2/docs/data/main.py b/krasnovia/lab2/docs/data/main.py new file mode 100644 index 0000000..4274b6e --- /dev/null +++ b/krasnovia/lab2/docs/data/main.py @@ -0,0 +1,138 @@ +""" +Главный файл запуска — интерактивное меню. + +Запуск: python main.py +""" + +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): + """Пошаговое ручное перемещение игрока по лабиринту (паттерн Command).""" + 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("╔══════════════════════════════╗") + print("║ Решатель лабиринтов ║") + 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/krasnovia/lab2/docs/data/make_report.js b/krasnovia/lab2/docs/data/make_report.js new file mode 100644 index 0000000..6a8f0f6 --- /dev/null +++ b/krasnovia/lab2/docs/data/make_report.js @@ -0,0 +1,340 @@ +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 { <> +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()] }), + + // ── 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 создан"); +}); diff --git a/krasnovia/lab2/docs/data/maze_builder.py b/krasnovia/lab2/docs/data/maze_builder.py new file mode 100644 index 0000000..910169d --- /dev/null +++ b/krasnovia/lab2/docs/data/maze_builder.py @@ -0,0 +1,71 @@ +""" +Этап 2: Паттерн Builder — загрузка лабиринта из файла. + +Формат файла: + # — стена + ' ' (пробел) — проход + S — старт + E — выход +""" + +from abc import ABC, abstractmethod +from maze_model import Cell, Maze + + +class MazeBuilder(ABC): + """Интерфейс строителя лабиринта (паттерн Builder).""" + + @abstractmethod + def build_from_file(self, filename: str) -> Maze: + """Читает файл и возвращает готовый объект Maze.""" + ... + + +class TextFileMazeBuilder(MazeBuilder): + """ + Конкретный строитель: читает текстовый файл и строит Maze. + + Зачем Builder? + Процесс построения многошаговый: чтение, парсинг, валидация, + поиск старта/выхода, создание объектов Cell. Builder скрывает + эту сложность от клиента. Чтобы добавить JSON-формат достаточно + написать JsonMazeBuilder, не трогая остальной код. + """ + + 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/krasnovia/lab2/docs/data/maze_model.py b/krasnovia/lab2/docs/data/maze_model.py new file mode 100644 index 0000000..7a32090 --- /dev/null +++ b/krasnovia/lab2/docs/data/maze_model.py @@ -0,0 +1,63 @@ +""" +Этап 1: Модель лабиринта — классы Cell и Maze +""" + +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: + """True, если клетка проходима (не стена).""" + 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 # cells[y][x] + 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/krasnovia/lab2/docs/data/maze_solver.py b/krasnovia/lab2/docs/data/maze_solver.py new file mode 100644 index 0000000..cd927c8 --- /dev/null +++ b/krasnovia/lab2/docs/data/maze_solver.py @@ -0,0 +1,85 @@ +""" +Этап 4: Класс-оркестратор MazeSolver. + +Принимает лабиринт и стратегию, запускает поиск, +собирает статистику и уведомляет наблюдателей. +""" + +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 # длина найденного пути (0 если не найден) + path: list[Cell] # сам путь + + +class MazeSolver: + """ + Оркестратор: связывает лабиринт, стратегию и наблюдателей. + + Паттерны внутри: + - Strategy: алгоритм задаётся снаружи через set_strategy() + - Observer: подписчики получают события о ходе поиска + """ + + def __init__(self, maze: Maze, strategy: PathFindingStrategy | None = None): + self.maze = maze + self.strategy = strategy + self._observers: list[Observer] = [] + + # ── Strategy ────────────────────────────────────────────────────────────── + + def set_strategy(self, strategy: PathFindingStrategy) -> None: + """Динамически меняет алгоритм поиска.""" + self.strategy = strategy + + # ── Observer ────────────────────────────────────────────────────────────── + + 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) + + # ── Solve ───────────────────────────────────────────────────────────────── + + 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/krasnovia/lab2/docs/data/mazes/large_50x50.txt b/krasnovia/lab2/docs/data/mazes/large_50x50.txt new file mode 100644 index 0000000..60a2c26 --- /dev/null +++ b/krasnovia/lab2/docs/data/mazes/large_50x50.txt @@ -0,0 +1,50 @@ +################################################## +#S # # # # # # ## +### # # # # # ##### ##### # # ##### # ### # # # ## +# # # # # # # # # # # # # # # # ## +# ##### ####### ##### # ####### # # ### # ### # ## +# # # # # # # # # # # # # ## +# # ##### ####### ### # # ### # # ### ##### # # ## +# # # # # # # # # # # # # # ## +# # # # # ##### ### # # # # ### ######### ##### ## +# # # # # # # # # # # ## +# ### ##### # ### ####### ######### ### ##### # ## +# # # # # # # # # # # # ## +### # # ##### ##### ### ##### # # # # # # ### # ## +# # # # # # # # # # # # # # ## +# ### # ### ##### # # ### ####### # # ######### ## +# # # # # # # # # # # # # ## +# ##### # ### ##### # ######### # ##### # # # # ## +# # # # # # # # # # # # ## +# # ##### ############# # # # ####### # # ##### ## +# # # # # # # # # # # # # ## +##### # ### # ### # ##### # # # ### ### # # # # ## +# # # # # # # # # # # # # # # # ## +# # # # ##### # ##### ### ####### ####### # # # ## +# # # # # # # # # # # # ## +##### ######### # ######### # # ##### # ### # # ## +# # # # # # # # # # # # ## +# # # ### # ### ##### # ########### # # ### ### ## +# # # # # # # # # # # # # # ## +# ##### # ### # # # # ######### # ### ### # # #### +# # # # # # # # # # # # # ## +### # ######### # # ### # ### # # ##### # ### # ## +# # # # # # # # # # # # # # # ## +# ### # ### # ####### # # # ### # # # # ### ### ## +# # # # # # # # # # # # # # # # ## +# # # ### # ### # # ######### # ##### # # ### # ## +# # # # # # # # # # # # # ## +# ####### ### ####### # # # # ### # ##### ### # ## +# # # # # # # # # # # ## +# ##### ### ####### ##### ### ### ####### # ### ## +# # # # # # # # # # # ## +# ### ######### ####### ### ### ### ### ##### #### +# # # # # # # # # # # # ## +### ### ##### ### # # ### ### # ### # # # # # # ## +# # # # # # # # # # # # # # ## +# ### ### ##### # ### ##### ######### ### # # # ## +# # # # # # # # # # # # ## +# # ### ############### # ### # ### ### # # ### ## +# # # # # # # +################################################E# +################################################## \ No newline at end of file diff --git a/krasnovia/lab2/docs/data/mazes/medium_20x20.txt b/krasnovia/lab2/docs/data/mazes/medium_20x20.txt new file mode 100644 index 0000000..d834468 --- /dev/null +++ b/krasnovia/lab2/docs/data/mazes/medium_20x20.txt @@ -0,0 +1,20 @@ +#################### +#S# # ## +# # ##### # ##### ## +# # # # # ## +# # ### # ### # #### +# # # # # # ## +# ### ####### # # ## +# # # # # # ## +# # # # # # ##### ## +# # # # # ## +# # ####### # ###### +# # # # # ## +# # # ####### # # ## +# # # # # # ## +# ####### # ### # ## +# # # # # ## +##### # ##### # # ## +# # # # +##################E# +#################### \ No newline at end of file diff --git a/krasnovia/lab2/docs/data/mazes/no_exit.txt b/krasnovia/lab2/docs/data/mazes/no_exit.txt new file mode 100644 index 0000000..f7d20d8 --- /dev/null +++ b/krasnovia/lab2/docs/data/mazes/no_exit.txt @@ -0,0 +1,5 @@ +########## +#S # +# ######## +# # +########## \ No newline at end of file diff --git a/krasnovia/lab2/docs/data/mazes/open_20x20.txt b/krasnovia/lab2/docs/data/mazes/open_20x20.txt new file mode 100644 index 0000000..10bbaf0 --- /dev/null +++ b/krasnovia/lab2/docs/data/mazes/open_20x20.txt @@ -0,0 +1,20 @@ +#################### +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +#################### \ No newline at end of file diff --git a/krasnovia/lab2/docs/data/mazes/small_10x10.txt b/krasnovia/lab2/docs/data/mazes/small_10x10.txt new file mode 100644 index 0000000..5595bf5 --- /dev/null +++ b/krasnovia/lab2/docs/data/mazes/small_10x10.txt @@ -0,0 +1,10 @@ +########## +#S # +# ###### # +# # # # +# # ## # # +# # ## # # +# # # # +# ###### # +# E# +########## \ No newline at end of file diff --git a/krasnovia/lab2/docs/data/observer.py b/krasnovia/lab2/docs/data/observer.py new file mode 100644 index 0000000..8975cc8 --- /dev/null +++ b/krasnovia/lab2/docs/data/observer.py @@ -0,0 +1,79 @@ +""" +Этап 5.1: Паттерн Observer — уведомления об изменении состояния. + +Зачем Observer? +MazeSolver не знает, кто хочет получать уведомления о событиях. +Он просто оповещает всех подписчиков. ConsoleView можно заменить +на GUI-вью или логгер без изменения MazeSolver. +""" + +from abc import ABC, abstractmethod +from maze_model import Maze, Cell + + +class Observer(ABC): + """Интерфейс наблюдателя.""" + + @abstractmethod + def update(self, event: dict) -> None: + """ + event — словарь с ключом "type": + "maze_loaded" — лабиринт загружен + "path_found" — путь найден + "no_path" — путь не найден + """ + ... + + +class ConsoleView(Observer): + """ + Наблюдатель: выводит лабиринт и путь в консоль. + + Символы: + # — стена + . — проход + S — старт + E — выход + * — найденный путь + @ — текущее положение игрока + """ + + 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/krasnovia/lab2/docs/data/results.csv b/krasnovia/lab2/docs/data/results.csv new file mode 100644 index 0000000..2dabd6f --- /dev/null +++ b/krasnovia/lab2/docs/data/results.csv @@ -0,0 +1,17 @@ +лабиринт,стратегия,время_мс,посещено_клеток,длина_пути +large_50x50,BFS,0.547751,339,275 +large_50x50,DFS,0.47275,285,275 +large_50x50,A*,0.844742,319,275 +large_50x50,Dijkstra,1.007925,339,275 +medium_20x20,BFS,0.255465,163,107 +medium_20x20,DFS,0.214549,107,107 +medium_20x20,A*,0.422447,163,107 +medium_20x20,Dijkstra,0.449911,163,107 +open_20x20,BFS,0.53038,324,35 +open_20x20,DFS,0.341214,171,171 +open_20x20,A*,1.065749,324,35 +open_20x20,Dijkstra,1.128275,324,35 +small_10x10,BFS,0.075246,28,15 +small_10x10,DFS,0.024954,15,15 +small_10x10,A*,0.080671,28,15 +small_10x10,Dijkstra,0.088109,28,15 diff --git a/krasnovia/lab2/docs/data/strategies.py b/krasnovia/lab2/docs/data/strategies.py new file mode 100644 index 0000000..e4fe1ad --- /dev/null +++ b/krasnovia/lab2/docs/data/strategies.py @@ -0,0 +1,175 @@ +""" +Этап 3: Паттерн Strategy — алгоритмы поиска пути. + +Зачем Strategy? +Позволяет менять алгоритм поиска во время выполнения без изменения +остального кода. Добавить новый алгоритм = написать новый класс. +""" + +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]: + """ + Возвращает список клеток от старта до выхода (включительно). + Пустой список — если путь не найден. + Счётчик посещённых клеток сохраняется в self.visited_count. + """ + ... + + # Вспомогательный метод восстановления пути по словарю предшественников + @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 + + +# ── BFS ────────────────────────────────────────────────────────────────────── + +class BFSStrategy(PathFindingStrategy): + """ + Поиск в ширину (BFS). + Гарантирует кратчайший путь по числу шагов. + Использует очередь (deque). + """ + + 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 [] # путь не найден + + +# ── DFS ────────────────────────────────────────────────────────────────────── + +class DFSStrategy(PathFindingStrategy): + """ + Поиск в глубину (DFS). + Быстр, но не гарантирует кратчайший путь. + Использует стек (list). + """ + + 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): + """ + Алгоритм A* с манхэттенской эвристикой. + Компромисс между BFS (оптимальность) и скоростью. + Использует приоритетную очередь (heapq). + """ + + @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]: + # (f_score, уникальный_счётчик, клетка) — счётчик нужен для стабильного сравнения + 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 [] + + +# ── Dijkstra ───────────────────────────────────────────────────────────────── + +class DijkstraStrategy(PathFindingStrategy): + """ + Алгоритм Дейкстры. + В базовом лабиринте (все веса = 1) совпадает с BFS, + но полезен при взвешенных клетках (болото, песок и т.д.). + """ + + 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): + # Вес клетки: можно расширить через cell.weight + 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/krasnovia/lab2/docs/report.docx b/krasnovia/lab2/docs/report.docx new file mode 100644 index 0000000..066f13c Binary files /dev/null and b/krasnovia/lab2/docs/report.docx differ