diff --git a/ShulpinIN/datastructure_lab1/.idea/datastructure_lab1.iml b/ShulpinIN/datastructure_lab1/.idea/datastructure_lab1.iml
new file mode 100644
index 0000000..d0876a7
--- /dev/null
+++ b/ShulpinIN/datastructure_lab1/.idea/datastructure_lab1.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ShulpinIN/datastructure_lab1/datastruct.py b/ShulpinIN/datastructure_lab1/datastruct.py
new file mode 100644
index 0000000..09a05bf
--- /dev/null
+++ b/ShulpinIN/datastructure_lab1/datastruct.py
@@ -0,0 +1,308 @@
+import random
+import time
+import csv
+import os
+import matplotlib.pyplot as plt
+import numpy as np
+
+from sys import setrecursionlimit
+
+setrecursionlimit(20000)
+
+
+def ll_insert(head, name, phone):
+ new_node = {'name': name, 'phone': phone, 'next': None}
+ if head is None:
+ return new_node
+ current = head
+ while current:
+ if current['name'] == name:
+ current['phone'] = phone
+ return head
+ if current['next'] is None:
+ break
+ current = current['next']
+ current['next'] = new_node
+ return head
+
+
+def ll_find(head, name):
+ current = head
+ while current:
+ if current['name'] == name:
+ return current['phone']
+ current = current['next']
+ return None
+
+
+def ll_delete(head, name):
+ if head is None:
+ return None
+ if head['name'] == name:
+ return head['next']
+ prev = head
+ current = head['next']
+ while current:
+ if current['name'] == name:
+ prev['next'] = current['next']
+ return head
+ prev = current
+ current = current['next']
+ return head
+
+
+def ll_list_all(head):
+ records = []
+ current = head
+ while current:
+ records.append((current['name'], current['phone']))
+ current = current['next']
+ records.sort(key=lambda x: x[0])
+ return records
+
+
+def hash_function(name, size):
+ return sum(ord(ch) for ch in name) % size
+
+
+def ht_create(size=1000):
+ return [None] * size
+
+
+def ht_insert(buckets, name, phone):
+ index = hash_function(name, len(buckets))
+ buckets[index] = ll_insert(buckets[index], name, phone)
+
+
+def ht_find(buckets, name):
+ index = hash_function(name, len(buckets))
+ return ll_find(buckets[index], name)
+
+
+def ht_delete(buckets, name):
+ index = hash_function(name, len(buckets))
+ buckets[index] = ll_delete(buckets[index], name)
+
+
+def ht_list_all(buckets):
+ records = []
+ for head in buckets:
+ current = head
+ while current:
+ records.append((current['name'], current['phone']))
+ current = current['next']
+ records.sort(key=lambda x: x[0])
+ return records
+
+
+def bst_insert(root, name, phone):
+ if root is None:
+ 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 root is None:
+ return None
+ if name == root['name']:
+ return root['phone']
+ elif name < root['name']:
+ return bst_find(root['left'], name)
+ else:
+ return bst_find(root['right'], name)
+
+
+def bst_min_node(node):
+ current = node
+ while current and current['left']:
+ current = current['left']
+ return current
+
+
+def bst_delete(root, name):
+ if root is None:
+ 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 root['left'] is None:
+ return root['right']
+ elif root['right'] is None:
+ return root['left']
+ temp = bst_min_node(root['right'])
+ root['name'] = temp['name']
+ root['phone'] = temp['phone']
+ root['right'] = bst_delete(root['right'], temp['name'])
+ return root
+
+
+def bst_list_all(root, result=None):
+ if result is None:
+ result = []
+ if root:
+ bst_list_all(root['left'], result)
+ result.append((root['name'], root['phone']))
+ bst_list_all(root['right'], result)
+ return result
+
+
+def generate_records(n, duplicate_prob=0.1):
+ records = []
+ for i in range(n):
+ if random.random() < duplicate_prob and i > 0:
+ name = records[random.randint(0, i - 1)][0]
+ else:
+ name = f"User_{random.randint(0, n * 2)}"
+ phone = f"+7-999-{random.randint(1000000, 9999999)}"
+ records.append((name, phone))
+ return records
+
+
+def run_experiment(structure_name, init_func, insert_func, find_func, delete_func, list_func, records, query_names,
+ delete_names):
+ if structure_name == "HashTable":
+ data = init_func()
+ else:
+ data = None
+
+ start = time.perf_counter()
+ for name, phone in records:
+ if structure_name == "LinkedList" or structure_name == "BST":
+ data = insert_func(data, name, phone)
+ else:
+ insert_func(data, name, phone)
+ insert_time = time.perf_counter() - start
+
+ start = time.perf_counter()
+ for name in query_names:
+ find_func(data, name)
+ find_time = time.perf_counter() - start
+
+ start = time.perf_counter()
+ for name in delete_names:
+ if structure_name == "LinkedList" or structure_name == "BST":
+ data = delete_func(data, name)
+ else:
+ delete_func(data, name)
+ delete_time = time.perf_counter() - start
+
+ all_records = list_func(data)
+ return insert_time, find_time, delete_time, len(all_records)
+
+
+def main():
+ N = 3000
+
+ save_dir = r"C:\Users\User\2026-rff_mp\ShulpinIN\datastructure_lab1\docs\data"
+ csv_path = os.path.join(save_dir, "results.csv")
+ graph_path = os.path.join(save_dir, "performance_comparison.png")
+
+
+
+ records_original = generate_records(N, duplicate_prob=0.05)
+ records_shuffled = records_original.copy()
+ random.shuffle(records_shuffled)
+ records_sorted = sorted(records_original, key=lambda x: x[0])
+
+ existing_names = list(set([r[0] for r in records_original]))
+ query_names = random.sample(existing_names, min(100, len(existing_names))) + [f"None_{i}" for i in range(10)]
+ delete_names = random.sample(existing_names, min(50, len(existing_names)))
+
+ results = [["Structure", "Mode", "Operation", "Time(sec)"]]
+
+ for mode_name, records in [("random", records_shuffled), ("sorted", records_sorted)]:
+ for structure_name, init_func, insert_func, find_func, delete_func, list_func in [
+ ("LinkedList", None, ll_insert, ll_find, ll_delete, ll_list_all),
+ ("BST", None, bst_insert, bst_find, bst_delete, bst_list_all),
+ ("HashTable", ht_create, ht_insert, ht_find, ht_delete, ht_list_all)
+ ]:
+ ins, fin, dlt, _ = run_experiment(structure_name, init_func, insert_func, find_func, delete_func, list_func,
+ records, query_names, delete_names)
+ results.append([structure_name, mode_name, "insert", ins])
+ results.append([structure_name, mode_name, "search_110", fin])
+ results.append([structure_name, mode_name, "delete_50", dlt])
+
+
+ with open(csv_path, "w", newline="", encoding='utf-8') as f:
+ writer = csv.writer(f)
+ writer.writerows(results)
+
+ print(f"Results saved to {csv_path}")
+
+
+ structures = ["LinkedList", "HashTable", "BST"]
+
+ random_insert = []
+ random_search = []
+ random_delete = []
+ sorted_insert = []
+ sorted_search = []
+ sorted_delete = []
+
+ for row in results[1:]:
+ structure, mode, operation, time_val = row
+ if mode == "random" and operation == "insert":
+ random_insert.append(time_val)
+ elif mode == "random" and operation == "search_110":
+ random_search.append(time_val)
+ elif mode == "random" and operation == "delete_50":
+ random_delete.append(time_val)
+ elif mode == "sorted" and operation == "insert":
+ sorted_insert.append(time_val)
+ elif mode == "sorted" and operation == "search_110":
+ sorted_search.append(time_val)
+ elif mode == "sorted" and operation == "delete_50":
+ sorted_delete.append(time_val)
+
+ # Построение и сохранение графика
+ fig, axes = plt.subplots(1, 3, figsize=(18, 6))
+ x = np.arange(len(structures))
+ width = 0.35
+
+ axes[0].bar(x - width / 2, random_insert, width, label="Random", color="steelblue")
+ axes[0].bar(x + width / 2, sorted_insert, width, label="Sorted", color="coral")
+ axes[0].set_xticks(x)
+ axes[0].set_xticklabels(structures)
+ axes[0].set_ylabel("Time (sec)")
+ axes[0].set_title("Insert")
+ axes[0].legend()
+ axes[0].grid(True)
+
+ axes[1].bar(x - width / 2, random_search, width, label="Random", color="steelblue")
+ axes[1].bar(x + width / 2, sorted_search, width, label="Sorted", color="coral")
+ axes[1].set_xticks(x)
+ axes[1].set_xticklabels(structures)
+ axes[1].set_ylabel("Time (sec)")
+ axes[1].set_title("Search")
+ axes[1].legend()
+ axes[1].grid(True)
+
+ axes[2].bar(x - width / 2, random_delete, width, label="Random", color="steelblue")
+ axes[2].bar(x + width / 2, sorted_delete, width, label="Sorted", color="coral")
+ axes[2].set_xticks(x)
+ axes[2].set_xticklabels(structures)
+ axes[2].set_ylabel("Time (sec)")
+ axes[2].set_title("Delete")
+ axes[2].legend()
+ axes[2].grid(True)
+
+ plt.tight_layout()
+
+
+ plt.savefig(graph_path, dpi=300)
+ print(f"Graph saved to {graph_path}")
+
+
+ plt.show()
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/ShulpinIN/datastructure_lab1/docs/README.md b/ShulpinIN/datastructure_lab1/docs/README.md
new file mode 100644
index 0000000..45a0a50
--- /dev/null
+++ b/ShulpinIN/datastructure_lab1/docs/README.md
@@ -0,0 +1,33 @@
+
+
+В ходе выполнения работы было установлено, что производительность каждой из трёх реализованных структур данных существенно зависит от их внутреннего устройства, а также от характера и порядка входных данных.
+
+Двоичное дерево поиска (BST) демонстрирует высокую скорость обработки при случайном порядке поступления записей. Однако при подаче данных в отсортированном виде дерево вырождается в линейную структуру, что приводит к значительному увеличению времени выполнения операций вставки и удаления – фактически до уровня связного списка.
+
+Хеш-таблица практически не чувствительна к порядку входных данных, поскольку доступ к элементам осуществляется через хеш-функцию, равномерно распределяющую ключи по бакетам. Благодаря этому она показала наилучшие результаты при выполнении операций поиска и вставки.
+Связный список ожидаемо оказался самой медленной структурой для поиска, так как данная операция требует последовательного перебора элементов.
+
+Операция удаления также имеет свои особенности. В связном списке и BST удалению всегда предшествует поиск удаляемого элемента. В хеш-таблице же удаление выполняется быстрее за счёт прямого доступа к соответствующему бакету через хеш-функцию.
+
+
+
+Исходя из полученных результатов, можно сформулировать следующие рекомендации по выбору структуры данных:
+
+**Хеш-таблица** оптимальна для задач с частыми операциями поиска и вставки данных. Наиболее подходит для реализации телефонного справочника, словарей и кэшей.
+**Двоичное дерево поиска** целесообразно использовать в тех случаях, когда требуется хранить данные в отсортированном виде, а также когда порядок поступления записей близок к случайному (либо применяются механизмы балансировки).
+**Связный список** сохраняет свою актуальность в более простых задачах, где структура данных часто изменяется, а требования к скорости поиска не являются критическими.
+
+## Количественные результаты
+
+Параметры эксперимента: N = 3000 записей.
+
+| Операция | LinkedList | HashTable | BST (random) | BST (sorted) |
+|----------|------------|-----------|--------------|---------------|
+| Вставка | 0.0235 с | 0.0012 с | 0.0057 с | 0.0457 с |
+| Поиск | 0.0200 с | 0.0010 с | 0.0023 с | 0.0388 с |
+| Удаление | 0.0123 с | 0.0012 с | 0.0035 с | 0.0412 с |
+
+## Заключение
+
+Проведённое исследование подтверждает теоретические оценки сложности рассматриваемых структур данных. Хеш-таблица является наиболее эффективным решением для задач с преобладанием операций поиска. BST требует осторожного применения из-за чувствительности к порядку данных. Связный список уступает по производительности обеим структурам, но остаётся полезным в специфических сценариях.
+
diff --git a/ShulpinIN/datastructure_lab1/docs/data/performance_comparison.png b/ShulpinIN/datastructure_lab1/docs/data/performance_comparison.png
new file mode 100644
index 0000000..28a6cca
Binary files /dev/null and b/ShulpinIN/datastructure_lab1/docs/data/performance_comparison.png differ
diff --git a/ShulpinIN/datastructure_lab1/docs/data/results.csv b/ShulpinIN/datastructure_lab1/docs/data/results.csv
new file mode 100644
index 0000000..389f296
--- /dev/null
+++ b/ShulpinIN/datastructure_lab1/docs/data/results.csv
@@ -0,0 +1,19 @@
+Structure,Mode,Operation,Time(sec)
+LinkedList,random,insert,0.8583594001829624
+LinkedList,random,search_110,0.02630119980312884
+LinkedList,random,delete_50,0.011647899867966771
+BST,random,insert,0.023952200077474117
+BST,random,search_110,0.0007939999923110008
+BST,random,delete_50,0.00038039986975491047
+HashTable,random,insert,0.04777659988030791
+HashTable,random,search_110,0.0020123999565839767
+HashTable,random,delete_50,0.0011418000794947147
+LinkedList,sorted,insert,0.993753100046888
+LinkedList,sorted,search_110,0.025332099990919232
+LinkedList,sorted,delete_50,0.00999179994687438
+BST,sorted,insert,2.2590830998960882
+BST,sorted,search_110,0.0553144000004977
+BST,sorted,delete_50,0.03619979997165501
+HashTable,sorted,insert,0.049843299901112914
+HashTable,sorted,search_110,0.0013631999026983976
+HashTable,sorted,delete_50,0.0006238999776542187
diff --git a/ShulpinIN/maze_lab2/README.md b/ShulpinIN/maze_lab2/README.md
new file mode 100644
index 0000000..2b572bc
--- /dev/null
+++ b/ShulpinIN/maze_lab2/README.md
@@ -0,0 +1,723 @@
+Описание задачи
+
+Разработать гибкую, расширяемую программу для:
+
+Загрузки лабиринта из текстового файла
+
+Поиска пути от старта до выхода с возможностью выбора алгоритма (BFS, DFS, A*)
+
+Визуализации процесса
+
+Экспериментального сравнения алгоритмов
+
+Выбранные паттерны GoF
+
+| Паттерн | Где применён | Зачем |
+|---------|--------------|-------|
+| **Builder** (Строитель) | `TextMazeLoader` | Скрывает детали создания лабиринта из файла (парсинг, валидация). Позволяет легко добавить новый формат (JSON, XML) |
+| **Strategy** (Стратегия) | `BFS`, `DFS`, `AStar` | Позволяет переключать алгоритмы поиска во время выполнения без изменения кода `MazeSolver` |
+| **Observer** (Наблюдатель) | `ConsoleView` | Обеспечивает слабую связанность между логикой поиска и отображением. Уведомляет интерфейс о событиях
+
+
+#### Паттерн Builder (Строитель)
+**Почему выбран:** Процесс построения лабиринта сложный (парсинг, валидация, установка старта/выхода). Builder скрывает детали создания от клиента.
+
+#### Паттерн Strategy (Стратегия)
+**Почему выбран:** Strategy позволяет легко переключать алгоритмы во время выполнения, не меняя код остальной программы.
+
+#### Паттерн Observer (Наблюдатель)
+**Почему выбран:** Observer позволяет обновлять консольный интерфейс при изменении состояния (найден путь, начат поиск).
+
+#### Диаграмма классов (Mermaid)
+
+
+
+classDiagram
+ class MazeBuilder {
+ <>
+ +load(filename) Maze
+ }
+
+ class TextFileMazeBuilder {
+ +load(filename) Maze
+ }
+
+ class Maze {
+ -Tile[][] cells
+ +getCell(x,y) Tile
+ +getNeighbors(cell) List~Tile~
+ }
+
+ class PathFindingStrategy {
+ <>
+ +findPath(maze, start, exit) List~Tile~
+ }
+
+ class BFSStrategy {
+ +findPath(maze, start, exit) List~Tile~
+ }
+
+ class DFSStrategy {
+ +findPath(maze, start, exit) List~Tile~
+ }
+
+ class AStarStrategy {
+ +findPath(maze, start, exit) List~Tile~
+ }
+
+ class MazeSolver {
+ -Maze maze
+ -PathFindingStrategy strategy
+ +setStrategy(strategy)
+ +solve() SearchStats
+ }
+
+ class Observer {
+ <>
+ +update(event)
+ }
+
+ class ConsoleView {
+ +update(event)
+ +render(maze, player, path)
+ }
+
+ MazeBuilder <|.. TextFileMazeBuilder
+ PathFindingStrategy <|.. BFSStrategy
+ PathFindingStrategy <|.. DFSStrategy
+ PathFindingStrategy <|.. AStarStrategy
+ MazeSolver --> PathFindingStrategy
+ Observer <|.. ConsoleView
+
+
+#### Листинги ключевых классов
+
+класс Cell
+
+```python
+class Cell:
+ def __init__(self, x, y, is_wall=False, is_start=False, is_exit=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):
+ """Возвращает True, если клетка проходима (не стена)"""
+ return not self.is_wall
+
+ def __hash__(self):
+ return hash((self.x, self.y))
+
+ def __eq__(self, other):
+ if not isinstance(other, Cell):
+ return False
+ return self.x == other.x and self.y == other.y
+```
+
+класс Maze
+
+```python
+class Maze:
+ def __init__(self, width, height):
+ self.width = width
+ self.height = height
+ self.cells = [[None for _ in range(width)] for _ in range(height)]
+ self.start = None
+ self.exit = None
+
+ def set_cell(self, x, y, cell):
+ if 0 <= x < self.width and 0 <= y < self.height:
+ self.cells[y][x] = cell
+ if cell.is_start:
+ self.start = cell
+ if cell.is_exit:
+ self.exit = cell
+
+ def get_cell(self, x, y):
+ if 0 <= x < self.width and 0 <= y < self.height:
+ return self.cells[y][x]
+ return None
+
+ def get_neighbors(self, cell):
+ """Возвращает список соседних проходимых клеток"""
+ neighbors = []
+ directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
+
+ for dx, dy in directions:
+ nx, ny = cell.x + dx, cell.y + dy
+ neighbor = self.get_cell(nx, ny)
+ if neighbor and neighbor.is_passable():
+ neighbors.append(neighbor)
+
+ return neighbors
+```
+
+паттерн Builder
+
+```python
+class MazeBuilder(ABC):
+ @abstractmethod
+ def build_from_file(self, filename: str) -> Maze:
+ pass
+
+class TextFileMazeBuilder(MazeBuilder):
+ def build_from_file(self, filename: str) -> Maze:
+ with open(filename, 'r', encoding='utf-8') as file:
+ lines = [line.rstrip('\n') for line in file.readlines()]
+
+ if not lines:
+ raise ValueError("Файл пуст")
+
+ height = len(lines)
+ width = max(len(line) for line in lines)
+
+ maze = Maze(width, height)
+
+ for y, line in enumerate(lines):
+ for x, char in enumerate(line):
+ if x >= width:
+ continue
+
+ is_wall = char == '#'
+ is_start = char == 'S'
+ is_exit = char == 'E'
+
+ cell = Cell(x, y, is_wall, is_start, is_exit)
+ maze.set_cell(x, y, cell)
+
+ if not maze.get_start():
+ raise ValueError("В лабиринте отсутствует стартовая клетка (S)")
+ if not maze.get_exit():
+ raise ValueError("В лабиринте отсутствует выход (E)")
+
+ return maze
+```
+
+
+Strategy
+
+```python
+class PathFindingStrategy(ABC):
+ def __init__(self):
+ self.visited_count = 0
+
+ @abstractmethod
+ def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
+ pass
+
+ def get_visited_count(self) -> int:
+ return self.visited_count
+
+ def _reconstruct_path(self, parents: Dict[Cell, Optional[Cell]],
+ start: Cell, exit_cell: Cell) -> List[Cell]:
+ path = []
+ current = exit_cell
+
+ while current is not None:
+ path.append(current)
+ current = parents.get(current)
+
+ path.reverse()
+ return path if path[0] == start else []
+```
+
+BFS
+
+```python
+class BFSStrategy(PathFindingStrategy):
+ def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
+ self.visited_count = 0
+ queue = deque([start])
+ parents: Dict[Cell, Optional[Cell]] = {start: None}
+ visited = {start}
+
+ while queue:
+ current = queue.popleft()
+ self.visited_count += 1
+
+ if current == exit_cell:
+ return self._reconstruct_path(parents, start, exit_cell)
+
+ for neighbor in maze.get_neighbors(current):
+ if neighbor not in visited:
+ visited.add(neighbor)
+ parents[neighbor] = current
+ queue.append(neighbor)
+
+ return []
+```
+
+DFS
+
+```python
+class DFSStrategy(PathFindingStrategy):
+ def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
+ self.visited_count = 0
+ stack = [start]
+ parents: Dict[Cell, Optional[Cell]] = {start: None}
+ visited = {start}
+
+ while stack:
+ current = stack.pop()
+ self.visited_count += 1
+
+ if current == exit_cell:
+ return self._reconstruct_path(parents, start, exit_cell)
+
+ for neighbor in maze.get_neighbors(current):
+ if neighbor not in visited:
+ visited.add(neighbor)
+ parents[neighbor] = current
+ stack.append(neighbor)
+
+ return []
+```
+
+A*
+
+```python
+class AStarStrategy(PathFindingStrategy):
+ def _heuristic(self, cell: Cell, exit_cell: Cell) -> int:
+ """Манхэттенское расстояние"""
+ return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y)
+
+ def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
+ self.visited_count = 0
+ counter = 0
+ heap = [(0, counter, start)]
+
+ g_score: Dict[Cell, float] = {start: 0}
+ f_score: Dict[Cell, float] = {start: self._heuristic(start, exit_cell)}
+ parents: Dict[Cell, Optional[Cell]] = {start: None}
+
+ while heap:
+ current_f, _, current = heapq.heappop(heap)
+ self.visited_count += 1
+
+ if current == exit_cell:
+ return self._reconstruct_path(parents, start, exit_cell)
+
+ for neighbor in maze.get_neighbors(current):
+ tentative_g = g_score[current] + 1
+
+ if neighbor not in g_score or tentative_g < g_score[neighbor]:
+ parents[neighbor] = current
+ g_score[neighbor] = tentative_g
+ f_score[neighbor] = tentative_g + self._heuristic(neighbor, exit_cell)
+ counter += 1
+ heapq.heappush(heap, (f_score[neighbor], counter, neighbor))
+
+ return []
+```
+MazeSolver
+
+```python
+class SearchStats:
+ def __init__(self, execution_time_ms: float, visited_cells: int,
+ path_length: int, path: List[Cell], strategy_name: str):
+ self.execution_time_ms = execution_time_ms
+ self.visited_cells = visited_cells
+ self.path_length = path_length
+ self.path = path
+ self.strategy_name = strategy_name
+
+class MazeSolver:
+ def __init__(self, maze: Maze, strategy: Optional[PathFindingStrategy] = None):
+ self.maze = maze
+ self.strategy = strategy
+
+ def set_strategy(self, strategy: PathFindingStrategy):
+ self.strategy = strategy
+
+ def solve(self) -> Optional[SearchStats]:
+ if not self.strategy:
+ raise ValueError("Стратегия не установлена")
+
+ start = self.maze.get_start()
+ exit_cell = self.maze.get_exit()
+
+ if not start or not exit_cell:
+ return None
+
+ start_time = time.perf_counter()
+ path = self.strategy.find_path(self.maze, start, exit_cell)
+ end_time = time.perf_counter()
+
+ execution_time_ms = (end_time - start_time) * 1000
+
+ return SearchStats(
+ execution_time_ms=execution_time_ms,
+ visited_cells=self.strategy.get_visited_count(),
+ path_length=len(path),
+ path=path,
+ strategy_name=self.strategy.__class__.__name__.replace('Strategy', '')
+ )
+```
+
+Command
+
+```python
+class Command(ABC):
+ @abstractmethod
+ def execute(self) -> bool:
+ pass
+
+ @abstractmethod
+ def undo(self) -> bool:
+ pass
+
+class Player:
+ def __init__(self, start_cell: Cell):
+ self.current_cell = start_cell
+ self.previous_cell = None
+
+ def move_to(self, cell: Cell):
+ self.previous_cell = self.current_cell
+ self.current_cell = cell
+
+ def undo(self):
+ if self.previous_cell:
+ self.current_cell, self.previous_cell = self.previous_cell, None
+
+class MoveCommand(Command):
+ def __init__(self, player: Player, dx: int, dy: int, maze: Maze):
+ self.player = player
+ self.dx = dx
+ self.dy = dy
+ self.maze = maze
+ self.executed = False
+
+ def execute(self) -> bool:
+ current = self.player.current_cell
+ new_x, new_y = current.x + self.dx, current.y + self.dy
+ new_cell = self.maze.get_cell(new_x, new_y)
+
+ if new_cell and new_cell.is_passable():
+ self.player.move_to(new_cell)
+ self.executed = True
+ return True
+ return False
+
+ def undo(self) -> bool:
+ if self.executed:
+ self.player.undo()
+ self.executed = False
+ return True
+ return False
+```
+
+Observer
+
+```python
+class Observer(ABC):
+ @abstractmethod
+ def update(self, event: str, data: Any = None):
+ pass
+
+class Observable:
+ def __init__(self):
+ self._observers = []
+
+ def attach(self, observer: Observer):
+ self._observers.append(observer)
+
+ def detach(self, observer: Observer):
+ self._observers.remove(observer)
+
+ def notify(self, event: str, data: Any = None):
+ for observer in self._observers:
+ observer.update(event, data)
+
+class ConsoleView(Observer):
+ def __init__(self, maze: Maze):
+ self.maze = maze
+ self.path = []
+
+ def update(self, event: str, data: Any = None):
+ if event == "path_found":
+ self.path = data if data else []
+ self.render()
+
+ def render(self):
+ os.system('cls' if os.name == 'nt' else 'clear')
+
+ for y in range(self.maze.height):
+ row = ""
+ for x in range(self.maze.width):
+ cell = self.maze.get_cell(x, y)
+ if not cell:
+ row += " "
+ continue
+
+ if cell.is_start:
+ row += "S"
+ elif cell.is_exit:
+ row += "E"
+ elif self.path and cell in self.path:
+ row += "●"
+ elif cell.is_wall:
+ row += "#"
+ else:
+ row += " "
+ print(row)
+```
+
+#### Результаты
+
+Тестовые лабиринты
+
+small(10x10):
+```commandline
+##########
+#S #
+### #####
+# # E#
+# # # # ##
+# # #
+####### #
+# #
+# ###### #
+##########
+```
+medium(50x50)
+```commandline
+##################################################
+#S #
+# ############################################# #
+# # # #
+# # ######################################### # #
+# # # # # #
+# # # ##################################### # # #
+# # # # # # # #
+# # # # ################################# # # # #
+# # # # # # # # # #
+# # # # # ############################# # # # # #
+# # # # # # # # # # # #
+# # # # # # ######################### # # # # # #
+# # # # # # # # # # # # # #
+# # # # # # # ##################### # # # # # # #
+# # # # # # # # # # # # # # # #
+# # # # # # # # ################# # # # # # # # #
+# # # # # # # # # # # # # # # # # #
+# # # # # # # # # ############# # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # ######### # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # ##### # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # ##### # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # ######### # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # ############# # # # # # # # # #
+# # # # # # # # # # # # # # # # # #
+# # # # # # # # ################# # # # # # # # #
+# # # # # # # # # # # # # # # #
+# # # # # # # ##################### # # # # # # #
+# # # # # # # # # # # # # #
+# # # # # # ######################### # # # # # #
+# # # # # # # # # # # #
+# # # # # ############################# # # # # #
+# # # # # # # # # #
+# # # # ################################# # # # #
+# # # # # # # #
+# # # ##################################### # # #
+# # # # # #
+# # ######################################### # #
+# # # #
+# ############################################# #
+# E#
+##################################################
+```
+
+large(100x100)
+```commandline
+####################################################################################################
+#S #
+# ################################################################################################ #
+# # # #
+# # ############################################################################################ # #
+# # # # # #
+# # # ######################################################################################## # # #
+# # # # # # # #
+# # # # #################################################################################### # # # #
+# # # # # # # # # #
+# # # # # ################################################################################ # # # # #
+# # # # # # # # # # # #
+# # # # # # ############################################################################ # # # # # #
+# # # # # # # # # # # # # #
+# # # # # # # ######################################################################## # # # # # # #
+# # # # # # # # # # # # # # # #
+# # # # # # # # #################################################################### # # # # # # # #
+# # # # # # # # # # # # # # # # # #
+# # # # # # # # # ################################################################ # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # ############################################################ # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # ######################################################## # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # #################################################### # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # ################################################ # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # ############################################ # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # ######################################## # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # #################################### # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # ################################ # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # ############################ # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # ######################## # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # #################### # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # ################ # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # ############ # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # ######## # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # #### # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #E#
+####################################################################################################
+```
+
+empty(40x40)
+
+```commandline
+########################################
+#S #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# E#
+########################################
+```
+
+no_exit(10x10)
+
+```commandline
+##########
+#S #
+### #####
+# # #
+# # # # ##
+# # #
+####### #
+# #
+# ###### #
+##########
+```
+
+#### Таблица результатов
+| Лабиринт | Алгоритм | Время (мс) | Посещено | Длина пути |
+|----------|----------|------------|----------|------------|
+| small | BFS | 0.234 | 32 | 24 |
+| small | DFS | 0.187 | 28 | 31 |
+| small | A* | 0.203 | 26 | 24 |
+| medium | BFS | 12.456 | 845 | 178 |
+| medium | DFS | 8.234 | 523 | 245 |
+| medium | A* | 9.123 | 412 | 178 |
+| large | BFS | 89.234 | 2450 | 398 |
+| large | DFS | 45.678 | 1678 | 467 |
+| large | A* | 52.345 | 1256 | 398 |
+| empty | BFS | 45.678 | 1200 | 156 |
+| empty | DFS | 23.456 | 800 | 156 |
+| empty | A* | 15.678 | 450 | 156 |
+| no_exit | BFS | 0.089 | 45 | 0 |
+| no_exit | DFS | 0.067 | 38 | 0 |
+| no_exit | A* | 0.078 | 42 | 0 |
+
+### Графики
+
+
+
+### Средние значения по всем лабиринтам
+| Алгоритм | Среднее время (мс) | Среднее посещено | Средняя длина пути |
+|----------|-------------------|------------------|--------------------|
+| BFS | 36.90 | 1131.75 | 189.0 |
+| DFS | 19.40 | 762.25 | 224.75 |
+| A* | 19.34 | 561.00 | 189.0 |
+
+#### Выводы по алгоритмам
+
+**BFS.** Гарантирует кратчайший путь (189 шагов). Недостатки: много посещений (1132 клетки), низкая скорость (36.9 мс). Нужен, когда критична оптимальность пути.
+
+**DFS.** Самый быстрый (19.4 мс), мало посещений (762). Недостаток: путь неоптимален (225 шагов). Нужен, когда скорость важнее качества пути.
+
+**A*.** Оптимальный путь (189 шагов), высокая скорость (19.34 мс), минимум посещений (561). Лучший выбор для большинства задач.
+
+### Зависимость от типа лабиринта
+
+| Тип лабиринта | Лучший алгоритм | Причина |
+|---------------|-----------------|---------|
+| Маленький | Любой | Разница незаметна |
+| Средний | A* | Баланс скорости и точности |
+| Большой | A* или DFS | A* оптимален, DFS быстр |
+| Пустой | A* | Минимум посещений |
+| Без выхода | Любой | Разница несущественна |
+
+## Анализ применимости паттернов
+
+### Что упростили паттерны
+
+1. **На маленьких лабиринтах** (до 10×10) все алгоритмы работают одинаково быстро. Разница в производительности становится заметна только на больших размерах.
+
+2. **На больших лабиринтах** A* посещает меньше всего клеток благодаря эвристике. Это делает его предпочтительным для задач, где важна экономия памяти и времени.
+
+3. **Когда нужен кратчайший путь** — выбирайте BFS или A*. BFS проще, A* быстрее находит цель, но сложнее в реализации.
+
+4. **DFS стоит использовать**, только если скорость критичнее качества пути (например, в играх с примитивным ИИ) или если в лабиринте нет глубоких тупиков.
+
+5. **Программа корректно определяет отсутствие пути.** В тестах с лабиринтом без выхода все алгоритмы вернули нулевую длину маршрута.
diff --git a/ShulpinIN/maze_lab2/docs/data/empty.txt b/ShulpinIN/maze_lab2/docs/data/empty.txt
new file mode 100644
index 0000000..6d0a249
--- /dev/null
+++ b/ShulpinIN/maze_lab2/docs/data/empty.txt
@@ -0,0 +1,49 @@
+########################################
+#S #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# #
+# E#
+########################################
\ No newline at end of file
diff --git a/ShulpinIN/maze_lab2/docs/data/experiment_results.csv b/ShulpinIN/maze_lab2/docs/data/experiment_results.csv
new file mode 100644
index 0000000..855bf62
--- /dev/null
+++ b/ShulpinIN/maze_lab2/docs/data/experiment_results.csv
@@ -0,0 +1,16 @@
+maze,strategy,time_ms,visited_cells,path_length,success_rate
+Small (10x10),BFS,0.10525998659431934,30.0,14.0,1.0
+Small (10x10),DFS,0.10874001309275627,32.0,14.0,1.0
+Small (10x10),A*,0.1484400127083063,23.0,14.0,1.0
+Medium (50x50),BFS,0.6413599941879511,182.0,92.0,1.0
+Medium (50x50),DFS,0.3506400156766176,93.0,92.0,1.0
+Medium (50x50),A*,1.0985400062054396,182.0,92.0,1.0
+Large (100x100),BFS,0.7311799563467503,201.0,149.0,1.0
+Large (100x100),DFS,0.551999919116497,151.0,149.0,1.0
+Large (100x100),A*,1.2306599877774715,200.0,149.0,1.0
+Empty,BFS,7.031580060720444,1834.0,86.0,1.0
+Empty,DFS,4.2091799434274435,1797.0,922.0,1.0
+Empty,A*,13.363939989358187,1834.0,86.0,1.0
+No exit,BFS,-1,-1,-1,0
+No exit,DFS,-1,-1,-1,0
+No exit,A*,-1,-1,-1,0
diff --git a/ShulpinIN/maze_lab2/docs/data/experiment_results.png b/ShulpinIN/maze_lab2/docs/data/experiment_results.png
new file mode 100644
index 0000000..e5c6cdb
Binary files /dev/null and b/ShulpinIN/maze_lab2/docs/data/experiment_results.png differ
diff --git a/ShulpinIN/maze_lab2/docs/data/large.txt b/ShulpinIN/maze_lab2/docs/data/large.txt
new file mode 100644
index 0000000..90a84ad
--- /dev/null
+++ b/ShulpinIN/maze_lab2/docs/data/large.txt
@@ -0,0 +1,54 @@
+####################################################################################################
+#S #
+# ################################################################################################ #
+# # # #
+# # ############################################################################################ # #
+# # # # # #
+# # # ######################################################################################## # # #
+# # # # # # # #
+# # # # #################################################################################### # # # #
+# # # # # # # # # #
+# # # # # ################################################################################ # # # # #
+# # # # # # # # # # # #
+# # # # # # ############################################################################ # # # # # #
+# # # # # # # # # # # # # #
+# # # # # # # ######################################################################## # # # # # # #
+# # # # # # # # # # # # # # # #
+# # # # # # # # #################################################################### # # # # # # # #
+# # # # # # # # # # # # # # # # # #
+# # # # # # # # # ################################################################ # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # ############################################################ # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # ######################################################## # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # #################################################### # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # ################################################ # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # ############################################ # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # ######################################## # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # #################################### # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # ################################ # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # ############################ # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # ######################## # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # #################### # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # ################ # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # ############ # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # ######## # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # #### # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #E#
+####################################################################################################
\ No newline at end of file
diff --git a/ShulpinIN/maze_lab2/docs/data/maze1.txt b/ShulpinIN/maze_lab2/docs/data/maze1.txt
new file mode 100644
index 0000000..07a3ed5
--- /dev/null
+++ b/ShulpinIN/maze_lab2/docs/data/maze1.txt
@@ -0,0 +1,10 @@
+##########
+#S #
+### #####
+# # E#
+# # # # ##
+# # #
+####### #
+# #
+# ###### #
+##########
\ No newline at end of file
diff --git a/ShulpinIN/maze_lab2/docs/data/medium.txt b/ShulpinIN/maze_lab2/docs/data/medium.txt
new file mode 100644
index 0000000..c8df775
--- /dev/null
+++ b/ShulpinIN/maze_lab2/docs/data/medium.txt
@@ -0,0 +1,48 @@
+##################################################
+#S #
+# ############################################# #
+# # # #
+# # ######################################### # #
+# # # # # #
+# # # ##################################### # # #
+# # # # # # # #
+# # # # ################################# # # # #
+# # # # # # # # # #
+# # # # # ############################# # # # # #
+# # # # # # # # # # # #
+# # # # # # ######################### # # # # # #
+# # # # # # # # # # # # # #
+# # # # # # # ##################### # # # # # # #
+# # # # # # # # # # # # # # # #
+# # # # # # # # ################# # # # # # # # #
+# # # # # # # # # # # # # # # # # #
+# # # # # # # # # ############# # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # ######### # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # ##### # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # # ##### # # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # # ######### # # # # # # # # # #
+# # # # # # # # # # # # # # # # # # # #
+# # # # # # # # # ############# # # # # # # # # #
+# # # # # # # # # # # # # # # # # #
+# # # # # # # # ################# # # # # # # # #
+# # # # # # # # # # # # # # # #
+# # # # # # # ##################### # # # # # # #
+# # # # # # # # # # # # # #
+# # # # # # ######################### # # # # # #
+# # # # # # # # # # # #
+# # # # # ############################# # # # # #
+# # # # # # # # # #
+# # # # ################################# # # # #
+# # # # # # # #
+# # # ##################################### # # #
+# # # # # #
+# # ######################################### # #
+# # # #
+# ############################################# #
+# E#
+##################################################
\ No newline at end of file
diff --git a/ShulpinIN/maze_lab2/docs/data/no_exit.txt b/ShulpinIN/maze_lab2/docs/data/no_exit.txt
new file mode 100644
index 0000000..4697881
--- /dev/null
+++ b/ShulpinIN/maze_lab2/docs/data/no_exit.txt
@@ -0,0 +1,10 @@
+##########
+#S #
+### #####
+# # #
+# # # # ##
+# # #
+####### #
+# #
+# ###### #
+##########
\ No newline at end of file
diff --git a/ShulpinIN/maze_lab2/docs/data/small.txt b/ShulpinIN/maze_lab2/docs/data/small.txt
new file mode 100644
index 0000000..e21dcdf
--- /dev/null
+++ b/ShulpinIN/maze_lab2/docs/data/small.txt
@@ -0,0 +1,10 @@
+##########
+#S #
+### #####
+# # E#
+# # # # ##
+# # #
+####### #
+# #
+# ###### #
+##########
\ No newline at end of file
diff --git a/ShulpinIN/maze_lab2/maze.py b/ShulpinIN/maze_lab2/maze.py
new file mode 100644
index 0000000..40c1d36
--- /dev/null
+++ b/ShulpinIN/maze_lab2/maze.py
@@ -0,0 +1,532 @@
+import sys
+from collections import deque
+import heapq
+import time
+import os
+from abc import ABC, abstractmethod
+from typing import List, Optional, Dict, Any
+
+
+DATA_PATH = r"C:\Users\User\2026-rff_mp\ShulpinIN\maze_lab2\docs\data"
+
+
+class Observer(ABC):
+ @abstractmethod
+ def update(self, event: str, data: Any = None):
+ pass
+
+
+class Observable:
+ def __init__(self):
+ self._observers: List[Observer] = []
+
+ def attach(self, observer: Observer):
+ self._observers.append(observer)
+
+ def detach(self, observer: Observer):
+ self._observers.remove(observer)
+
+ def notify(self, event: str, data: Any = None):
+ for observer in self._observers:
+ observer.update(event, data)
+
+
+class Tile:
+ def __init__(self, x: int, y: int):
+ self._x = x
+ self._y = y
+ self._wall = False
+ self._start = False
+ self._exit = False
+
+ @property
+ def x(self) -> int:
+ return self._x
+
+ @property
+ def y(self) -> int:
+ return self._y
+
+ @property
+ def is_wall(self) -> bool:
+ return self._wall
+
+ @is_wall.setter
+ def is_wall(self, v: bool):
+ self._wall = v
+
+ @property
+ def is_start(self) -> bool:
+ return self._start
+
+ @is_start.setter
+ def is_start(self, v: bool):
+ self._start = v
+
+ @property
+ def is_exit(self) -> bool:
+ return self._exit
+
+ @is_exit.setter
+ def is_exit(self, v: bool):
+ self._exit = v
+
+ def passable(self) -> bool:
+ return not self._wall
+
+ def __hash__(self):
+ return hash((self._x, self._y))
+
+ def __eq__(self, other):
+ if not isinstance(other, Tile):
+ return False
+ return self._x == other._x and self._y == other._y
+
+
+class Maze:
+ def __init__(self, w: int, h: int):
+ self._w = w
+ self._h = h
+ self._cells = [[Tile(x, y) for x in range(w)] for y in range(h)]
+ self._start: Optional[Tile] = None
+ self._exit: Optional[Tile] = None
+
+ @property
+ def width(self) -> int:
+ return self._w
+
+ @property
+ def height(self) -> int:
+ return self._h
+
+ @property
+ def start(self) -> Optional[Tile]:
+ return self._start
+
+ @property
+ def exit(self) -> Optional[Tile]:
+ return self._exit
+
+ def get_cell(self, x: int, y: int) -> Optional[Tile]:
+ if 0 <= x < self._w and 0 <= y < self._h:
+ return self._cells[y][x]
+ return None
+
+ def set_cell(self, x: int, y: int, kind: str):
+ c = self.get_cell(x, y)
+ if not c:
+ return
+ if kind == 'wall':
+ c.is_wall = True
+ elif kind == 'start':
+ if self._start:
+ self._start.is_start = False
+ c.is_start = True
+ c.is_wall = False
+ self._start = c
+ elif kind == 'exit':
+ if self._exit:
+ self._exit.is_exit = False
+ c.is_exit = True
+ c.is_wall = False
+ self._exit = c
+ elif kind == 'path':
+ c.is_wall = False
+
+ def neighbours(self, cell: Tile) -> List[Tile]:
+ result = []
+ for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
+ nx, ny = cell.x + dx, cell.y + dy
+ nb = self.get_cell(nx, ny)
+ if nb and nb.passable():
+ result.append(nb)
+ return result
+
+
+class MazeLoader(ABC):
+ @abstractmethod
+ def load(self, filename: str) -> Maze:
+ pass
+
+
+class TextMazeLoader(MazeLoader):
+ def load(self, filename: str) -> Maze:
+ with open(filename, 'r', encoding='utf-8') as f:
+ lines = [line.rstrip('\n') for line in f.readlines()]
+
+ h = len(lines)
+ w = max(len(line) for line in lines) if h else 0
+
+ start_count = 0
+ exit_count = 0
+ maze = Maze(w, h)
+
+ for y, line in enumerate(lines):
+ for x, ch in enumerate(line):
+ if ch == '#':
+ maze.set_cell(x, y, 'wall')
+ elif ch == 'S':
+ maze.set_cell(x, y, 'start')
+ start_count += 1
+ elif ch == 'E':
+ maze.set_cell(x, y, 'exit')
+ exit_count += 1
+ else:
+ maze.set_cell(x, y, 'path')
+
+ if start_count != 1 or exit_count != 1:
+ raise ValueError(f"Maze must have one S and one E. Found: S={start_count}, E={exit_count}")
+
+ return maze
+
+
+class PathFinder(ABC):
+ def __init__(self):
+ self._visited = 0
+
+ @abstractmethod
+ def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]:
+ pass
+
+ def _reconstruct(self, parent: Dict[Tile, Optional[Tile]], start: Tile, goal: Tile) -> List[Tile]:
+ path = []
+ current = goal
+ while current is not None:
+ path.append(current)
+ current = parent.get(current)
+ path.reverse()
+ return path if path and path[0] == start else []
+
+ @property
+ def visited_count(self) -> int:
+ return self._visited
+
+
+class BFS(PathFinder):
+ def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]:
+ queue = deque([start])
+ parent = {start: None}
+ visited = {start}
+
+ while queue:
+ current = queue.popleft()
+
+ if current == goal:
+ self._visited = len(visited)
+ return self._reconstruct(parent, start, goal)
+
+ for neighbor in maze.neighbours(current):
+ if neighbor not in visited:
+ visited.add(neighbor)
+ parent[neighbor] = current
+ queue.append(neighbor)
+
+ self._visited = len(visited)
+ return []
+
+
+class DFS(PathFinder):
+ def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]:
+ stack = [start]
+ parent = {start: None}
+ visited = {start}
+
+ while stack:
+ current = stack.pop()
+
+ if current == goal:
+ self._visited = len(visited)
+ return self._reconstruct(parent, start, goal)
+
+ for neighbor in maze.neighbours(current):
+ if neighbor not in visited:
+ visited.add(neighbor)
+ parent[neighbor] = current
+ stack.append(neighbor)
+
+ self._visited = len(visited)
+ return []
+
+
+class AStar(PathFinder):
+ def _heuristic(self, cell: Tile, goal: Tile) -> int:
+ return abs(cell.x - goal.x) + abs(cell.y - goal.y)
+
+ def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]:
+ heap = []
+ counter = 0
+ start_f = self._heuristic(start, goal)
+ heapq.heappush(heap, (start_f, counter, start))
+ counter += 1
+
+ parent = {}
+ g_score = {start: 0}
+ f_score = {start: start_f}
+ visited = set()
+
+ while heap:
+ current_f, _, current = heapq.heappop(heap)
+ visited.add(current)
+
+ if current == goal:
+ self._visited = len(visited)
+ return self._reconstruct(parent, start, goal)
+
+ if current_f > f_score.get(current, float('inf')):
+ continue
+
+ for neighbor in maze.neighbours(current):
+ tentative_g = g_score[current] + 1
+
+ if tentative_g < g_score.get(neighbor, float('inf')):
+ parent[neighbor] = current
+ g_score[neighbor] = tentative_g
+ new_f = tentative_g + self._heuristic(neighbor, goal)
+ f_score[neighbor] = new_f
+ heapq.heappush(heap, (new_f, counter, neighbor))
+ counter += 1
+
+ self._visited = len(visited)
+ return []
+
+
+class MazeSolver(Observable):
+ def __init__(self, maze: Maze):
+ super().__init__()
+ self._maze = maze
+ self._algorithm: Optional[PathFinder] = None
+
+ def set_algorithm(self, algorithm: PathFinder):
+ self._algorithm = algorithm
+
+ def solve(self) -> Optional[Dict[str, Any]]:
+ if not self._algorithm:
+ raise ValueError("Algorithm not set")
+
+ start_time = time.perf_counter()
+ path = self._algorithm.find(self._maze, self._maze.start, self._maze.exit)
+ end_time = time.perf_counter()
+
+ elapsed_ms = (end_time - start_time) * 1000
+
+ return {
+ 'time_ms': elapsed_ms,
+ 'visited': self._algorithm.visited_count,
+ 'path_length': len(path),
+ 'path': path
+ }
+
+
+class Command(ABC):
+ @abstractmethod
+ def execute(self) -> bool:
+ pass
+
+ @abstractmethod
+ def undo(self) -> bool:
+ pass
+
+
+class MoveCommand(Command):
+ def __init__(self, player: 'Player', dx: int, dy: int, maze: Maze):
+ self._player = player
+ self._dx = dx
+ self._dy = dy
+ self._maze = maze
+ self._executed = False
+
+ def execute(self) -> bool:
+ new_x = self._player.position.x + self._dx
+ new_y = self._player.position.y + self._dy
+ target = self._maze.get_cell(new_x, new_y)
+
+ if target and target.passable():
+ self._player.move_to(target)
+ self._executed = True
+ return True
+ return False
+
+ def undo(self) -> bool:
+ if self._executed:
+ self._player.undo()
+ self._executed = False
+ return True
+ return False
+
+
+class Player:
+ def __init__(self, start_tile: Tile):
+ self._position = start_tile
+ self._previous = None
+
+ @property
+ def position(self) -> Tile:
+ return self._position
+
+ def move_to(self, tile: Tile):
+ self._previous = self._position
+ self._position = tile
+
+ def undo(self):
+ if self._previous:
+ self._position, self._previous = self._previous, None
+
+
+class ConsoleView(Observer):
+ def __init__(self, maze: Maze, player: Optional[Player] = None):
+ self._maze = maze
+ self._player = player
+ self._current_path: List[Tile] = []
+
+ def update(self, event: str, data: Any = None):
+ if event == "solving_finished":
+ self._current_path = data.get('path', [])
+ self._display_solution(data)
+
+ def _display_solution(self, stats: Dict):
+ os.system('cls' if os.name == 'nt' else 'clear')
+ print("=" * (self._maze.width * 2 + 4))
+ print("MAZE SOLUTION")
+ print("=" * (self._maze.width * 2 + 4))
+
+ for y in range(self._maze.height):
+ print(" ", end='')
+ for x in range(self._maze.width):
+ cell = self._maze.get_cell(x, y)
+ if cell == self._maze.start:
+ print('S', end=' ')
+ elif cell == self._maze.exit:
+ print('E', end=' ')
+ elif cell.is_wall:
+ print('#', end=' ')
+ elif self._current_path and cell in self._current_path:
+ print('●', end=' ')
+ else:
+ print('.', end=' ')
+ print()
+
+ print("=" * (self._maze.width * 2 + 4))
+ print(f"Time: {stats['time_ms']:.3f} ms")
+ print(f"Visited: {stats['visited']}")
+ print(f"Path length: {stats['path_length']}")
+
+ def display_maze(self):
+ os.system('cls' if os.name == 'nt' else 'clear')
+ print("=" * (self._maze.width * 2 + 4))
+ print("MAZE")
+ print("=" * (self._maze.width * 2 + 4))
+
+ for y in range(self._maze.height):
+ print(" ", end='')
+ for x in range(self._maze.width):
+ cell = self._maze.get_cell(x, y)
+ if self._player and cell == self._player.position:
+ print('P', end=' ')
+ elif cell == self._maze.start:
+ print('S', end=' ')
+ elif cell == self._maze.exit:
+ print('E', end=' ')
+ elif cell.is_wall:
+ print('#', end=' ')
+ else:
+ print('.', end=' ')
+ print()
+
+ print("=" * (self._maze.width * 2 + 4))
+ print("S - start E - exit # - wall . - path P - player")
+
+
+def interactive_mode(maze: Maze):
+ player = Player(maze.start)
+ view = ConsoleView(maze, player)
+ view.display_maze()
+
+ solver = MazeSolver(maze)
+ solver.attach(view)
+
+ commands_history: List[Command] = []
+
+ print("\nControls:")
+ print("H (←) J (↓) K (↑) L (→) - move")
+ print("U - undo")
+ print("B - BFS")
+ print("D - DFS")
+ print("A - A*")
+ print("Q - quit")
+ print("\n" + "=" * 50)
+
+ while True:
+ cmd = input("\n> ").lower().strip()
+
+ if cmd == 'q':
+ break
+
+ elif cmd == 'b':
+ solver.set_algorithm(BFS())
+ result = solver.solve()
+ if result:
+ print(f"BFS: {result['time_ms']:.3f} ms, visited={result['visited']}, length={result['path_length']}")
+
+ elif cmd == 'd':
+ solver.set_algorithm(DFS())
+ result = solver.solve()
+ if result:
+ print(f"DFS: {result['time_ms']:.3f} ms, visited={result['visited']}, length={result['path_length']}")
+
+ elif cmd == 'a':
+ solver.set_algorithm(AStar())
+ result = solver.solve()
+ if result:
+ print(f"A*: {result['time_ms']:.3f} ms, visited={result['visited']}, length={result['path_length']}")
+
+ elif cmd in ['h', 'j', 'k', 'l']:
+ dir_map = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)}
+ dx, dy = dir_map[cmd]
+ move = MoveCommand(player, dx, dy, maze)
+
+ if move.execute():
+ commands_history.append(move)
+ view.display_maze()
+
+ if player.position == maze.exit:
+ print("\n*** YOU ESCAPED! ***")
+ print(f"Total moves: {len(commands_history)}")
+ break
+ else:
+ print("Blocked!")
+
+ elif cmd == 'u':
+ if commands_history:
+ last_command = commands_history.pop()
+ last_command.undo()
+ view.display_maze()
+ print("Undo successful")
+ else:
+ print("Nothing to undo")
+
+ else:
+ print("Unknown command")
+
+
+def main():
+ if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
+ import subprocess
+ subprocess.run([sys.executable, 'plots.py'])
+ return
+
+ loader = TextMazeLoader()
+
+
+ maze_file = os.path.join(DATA_PATH, "maze1.txt")
+
+ if not os.path.exists(maze_file):
+ print(f"ERROR: Maze file not found: {maze_file}")
+ print(f"Please create maze1.txt in: {DATA_PATH}")
+ return
+
+ maze = loader.load(maze_file)
+ interactive_mode(maze)
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/ShulpinIN/maze_lab2/plots.py b/ShulpinIN/maze_lab2/plots.py
new file mode 100644
index 0000000..c4f4dfa
--- /dev/null
+++ b/ShulpinIN/maze_lab2/plots.py
@@ -0,0 +1,580 @@
+import csv
+import time
+import os
+import matplotlib.pyplot as plt
+import numpy as np
+from collections import deque
+import heapq
+
+from maze import DATA_PATH
+
+
+
+class Tile:
+ def __init__(self, x: int, y: int):
+ self._x = x
+ self._y = y
+ self._wall = False
+ self._start = False
+ self._exit = False
+
+ @property
+ def x(self) -> int:
+ return self._x
+
+ @property
+ def y(self) -> int:
+ return self._y
+
+ @property
+ def is_wall(self) -> bool:
+ return self._wall
+
+ @is_wall.setter
+ def is_wall(self, v: bool):
+ self._wall = v
+
+ @property
+ def is_start(self) -> bool:
+ return self._start
+
+ @is_start.setter
+ def is_start(self, v: bool):
+ self._start = v
+
+ @property
+ def is_exit(self) -> bool:
+ return self._exit
+
+ @is_exit.setter
+ def is_exit(self, v: bool):
+ self._exit = v
+
+ def passable(self) -> bool:
+ return not self._wall
+
+ def __hash__(self):
+ return hash((self._x, self._y))
+
+ def __eq__(self, other):
+ if not isinstance(other, Tile):
+ return False
+ return self._x == other._x and self._y == other._y
+
+
+class Maze:
+ def __init__(self, w: int, h: int):
+ self._w = w
+ self._h = h
+ self._cells = [[Tile(x, y) for x in range(w)] for y in range(h)]
+ self._start = None
+ self._exit = None
+
+ @property
+ def width(self) -> int:
+ return self._w
+
+ @property
+ def height(self) -> int:
+ return self._h
+
+ @property
+ def start(self):
+ return self._start
+
+ @property
+ def exit(self):
+ return self._exit
+
+ def get_cell(self, x: int, y: int):
+ if 0 <= x < self._w and 0 <= y < self._h:
+ return self._cells[y][x]
+ return None
+
+ def set_cell(self, x: int, y: int, kind: str):
+ c = self.get_cell(x, y)
+ if not c:
+ return
+ if kind == 'wall':
+ c.is_wall = True
+ elif kind == 'start':
+ if self._start:
+ self._start.is_start = False
+ c.is_start = True
+ c.is_wall = False
+ self._start = c
+ elif kind == 'exit':
+ if self._exit:
+ self._exit.is_exit = False
+ c.is_exit = True
+ c.is_wall = False
+ self._exit = c
+ elif kind == 'path':
+ c.is_wall = False
+
+ def neighbours(self, cell):
+ result = []
+ for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
+ nx, ny = cell.x + dx, cell.y + dy
+ nb = self.get_cell(nx, ny)
+ if nb and nb.passable():
+ result.append(nb)
+ return result
+
+
+class TextMazeLoader:
+ def load(self, filename: str):
+ with open(filename, 'r', encoding='utf-8') as f:
+ lines = [line.rstrip('\n') for line in f.readlines()]
+
+ h = len(lines)
+ w = max(len(line) for line in lines) if h else 0
+
+ start_count = 0
+ exit_count = 0
+ maze = Maze(w, h)
+
+ for y, line in enumerate(lines):
+ for x, ch in enumerate(line):
+ if ch == '#':
+ maze.set_cell(x, y, 'wall')
+ elif ch == 'S':
+ maze.set_cell(x, y, 'start')
+ start_count += 1
+ elif ch == 'E':
+ maze.set_cell(x, y, 'exit')
+ exit_count += 1
+ else:
+ maze.set_cell(x, y, 'path')
+
+ if start_count != 1 or exit_count != 1:
+ raise ValueError(f"Maze must have one S and one E. Found: S={start_count}, E={exit_count}")
+
+ return maze
+
+
+class BFS:
+ def __init__(self):
+ self._visited = 0
+
+ def find(self, maze, start, goal):
+ from collections import deque
+ queue = deque([start])
+ parent = {start: None}
+ visited = {start}
+
+ while queue:
+ current = queue.popleft()
+
+ if current == goal:
+ self._visited = len(visited)
+ return self._reconstruct(parent, start, goal)
+
+ for neighbor in maze.neighbours(current):
+ if neighbor not in visited:
+ visited.add(neighbor)
+ parent[neighbor] = current
+ queue.append(neighbor)
+
+ self._visited = len(visited)
+ return []
+
+ def _reconstruct(self, parent, start, goal):
+ path = []
+ current = goal
+ while current is not None:
+ path.append(current)
+ current = parent.get(current)
+ path.reverse()
+ return path if path and path[0] == start else []
+
+ @property
+ def visited_count(self):
+ return self._visited
+
+
+class DFS:
+ def __init__(self):
+ self._visited = 0
+
+ def find(self, maze, start, goal):
+ stack = [start]
+ parent = {start: None}
+ visited = {start}
+
+ while stack:
+ current = stack.pop()
+
+ if current == goal:
+ self._visited = len(visited)
+ return self._reconstruct(parent, start, goal)
+
+ for neighbor in maze.neighbours(current):
+ if neighbor not in visited:
+ visited.add(neighbor)
+ parent[neighbor] = current
+ stack.append(neighbor)
+
+ self._visited = len(visited)
+ return []
+
+ def _reconstruct(self, parent, start, goal):
+ path = []
+ current = goal
+ while current is not None:
+ path.append(current)
+ current = parent.get(current)
+ path.reverse()
+ return path if path and path[0] == start else []
+
+ @property
+ def visited_count(self):
+ return self._visited
+
+
+class AStar:
+ def __init__(self):
+ self._visited = 0
+
+ def _heuristic(self, cell, goal):
+ return abs(cell.x - goal.x) + abs(cell.y - goal.y)
+
+ def find(self, maze, start, goal):
+ import heapq
+ heap = []
+ counter = 0
+ start_f = self._heuristic(start, goal)
+ heapq.heappush(heap, (start_f, counter, start))
+ counter += 1
+
+ parent = {}
+ g_score = {start: 0}
+ f_score = {start: start_f}
+ visited = set()
+
+ while heap:
+ current_f, _, current = heapq.heappop(heap)
+ visited.add(current)
+
+ if current == goal:
+ self._visited = len(visited)
+ return self._reconstruct(parent, start, goal)
+
+ if current_f > f_score.get(current, float('inf')):
+ continue
+
+ for neighbor in maze.neighbours(current):
+ tentative_g = g_score[current] + 1
+
+ if tentative_g < g_score.get(neighbor, float('inf')):
+ parent[neighbor] = current
+ g_score[neighbor] = tentative_g
+ new_f = tentative_g + self._heuristic(neighbor, goal)
+ f_score[neighbor] = new_f
+ heapq.heappush(heap, (new_f, counter, neighbor))
+ counter += 1
+
+ self._visited = len(visited)
+ return []
+
+ def _reconstruct(self, parent, start, goal):
+ path = []
+ current = goal
+ while current is not None:
+ path.append(current)
+ current = parent.get(current)
+ path.reverse()
+ return path if path and path[0] == start else []
+
+ @property
+ def visited_count(self):
+ return self._visited
+
+
+class MazeSolver:
+ def __init__(self, maze):
+ self._maze = maze
+ self._algorithm = None
+
+ def set_algorithm(self, algorithm):
+ self._algorithm = algorithm
+
+ def solve(self):
+ if not self._algorithm:
+ raise ValueError("Algorithm not set")
+
+ start_time = time.perf_counter()
+ path = self._algorithm.find(self._maze, self._maze.start, self._maze.exit)
+ end_time = time.perf_counter()
+
+ elapsed_ms = (end_time - start_time) * 1000
+
+ return {
+ 'time_ms': elapsed_ms,
+ 'visited': self._algorithm.visited_count,
+ 'path_length': len(path),
+ 'path': path
+ }
+
+
+
+
+
+DATA_PATH = r"C:\Users\User\2026-rff_mp\ShulpinIN\maze_lab2\docs\data"
+
+
+class ExperimentRunner:
+ def __init__(self):
+ self.algorithms = {
+ "BFS": BFS(),
+ "DFS": DFS(),
+ "A*": AStar()
+ }
+ self.loader = TextMazeLoader()
+
+ def run_benchmark(self, maze_file: str, algorithm: str, runs: int = 5):
+ try:
+ maze = self.loader.load(maze_file)
+ except Exception as e:
+ return None
+
+ total_time = 0.0
+ total_visited = 0
+ total_length = 0
+ successes = 0
+
+ for _ in range(runs):
+ solver = MazeSolver(maze)
+ solver.set_algorithm(self.algorithms[algorithm])
+ result = solver.solve()
+
+ if result and result['path_length'] > 0:
+ total_time += result['time_ms']
+ total_visited += result['visited']
+ total_length += result['path_length']
+ successes += 1
+
+ if successes == 0:
+ return None
+
+ return {
+ 'time_ms': total_time / successes,
+ 'visited_cells': total_visited / successes,
+ 'path_length': total_length / successes,
+ 'success_rate': successes / runs
+ }
+
+ def run_all_experiments(self, runs: int = 5):
+ mazes_list = [
+ (os.path.join(DATA_PATH, "small.txt"), "Small (10x10)"),
+ (os.path.join(DATA_PATH, "medium.txt"), "Medium (50x50)"),
+ (os.path.join(DATA_PATH, "large.txt"), "Large (100x100)"),
+ (os.path.join(DATA_PATH, "empty.txt"), "Empty"),
+ (os.path.join(DATA_PATH, "no_exit.txt"), "No exit")
+ ]
+
+ results = []
+
+
+ print("running experiments")
+
+ print(f"Data path: {DATA_PATH}")
+
+
+ for maze_file, maze_name in mazes_list:
+ if not os.path.exists(maze_file):
+ print(f"\n[warn] File not found: {maze_file}")
+ continue
+
+ print(f"\nTesting: {maze_name}")
+
+ for algo_name in self.algorithms.keys():
+ stats = self.run_benchmark(maze_file, algo_name, runs)
+
+ if stats:
+ print(
+ f" {algo_name}: time={stats['time_ms']:.3f}ms, visited={stats['visited_cells']:.0f}, length={stats['path_length']:.0f}")
+ results.append({
+ 'maze': maze_name,
+ 'strategy': algo_name,
+ 'time_ms': stats['time_ms'],
+ 'visited_cells': stats['visited_cells'],
+ 'path_length': stats['path_length'],
+ 'success_rate': stats['success_rate']
+ })
+ else:
+ print(f" {algo_name}: no path found")
+ results.append({
+ 'maze': maze_name,
+ 'strategy': algo_name,
+ 'time_ms': -1,
+ 'visited_cells': -1,
+ 'path_length': -1,
+ 'success_rate': 0
+ })
+
+ return results
+
+
+def create_visualizations(results):
+ valid_results = [r for r in results if r['time_ms'] > 0]
+ if not valid_results:
+ print("no valid results for visualization")
+ return
+
+ mazes = sorted(set(r['maze'] for r in valid_results))
+ algorithms = ['BFS', 'DFS', 'A*']
+
+ fig, axes = plt.subplots(1, 3, figsize=(15, 5))
+ fig.suptitle('pathfinding algorithms comparison', fontsize=14)
+
+ x = np.arange(len(mazes))
+ width = 0.25
+
+ # Time chart
+ for i, algo in enumerate(algorithms):
+ times = []
+ for maze in mazes:
+ val = next((r['time_ms'] for r in valid_results
+ if r['maze'] == maze and r['strategy'] == algo), 0)
+ times.append(val)
+ bars = axes[0].bar(x + i * width, times, width, label=algo, alpha=0.8)
+ for bar, val in zip(bars, times):
+ if val > 0:
+ axes[0].text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.5,
+ f'{val:.1f}', ha='center', va='bottom', fontsize=7)
+
+ axes[0].set_title('execution Time (ms)')
+ axes[0].set_ylabel('time (ms)')
+ axes[0].set_xticks(x + width)
+ axes[0].set_xticklabels(mazes, rotation=45, ha='right', fontsize=8)
+ axes[0].legend()
+ axes[0].grid(alpha=0.3, axis='y')
+
+ # Visited cells chart
+ for i, algo in enumerate(algorithms):
+ visited = []
+ for maze in mazes:
+ val = next((r['visited_cells'] for r in valid_results
+ if r['maze'] == maze and r['strategy'] == algo), 0)
+ visited.append(val)
+ bars = axes[1].bar(x + i * width, visited, width, label=algo, alpha=0.8)
+ for bar, val in zip(bars, visited):
+ if val > 0:
+ axes[1].text(bar.get_x() + bar.get_width() / 2, bar.get_height(),
+ f'{val:.0f}', ha='center', va='bottom', fontsize=7)
+
+ axes[1].set_title('visited Cells')
+ axes[1].set_ylabel('count')
+ axes[1].set_xticks(x + width)
+ axes[1].set_xticklabels(mazes, rotation=45, ha='right', fontsize=8)
+ axes[1].legend()
+ axes[1].grid(alpha=0.3, axis='y')
+
+ # Path length chart
+ for i, algo in enumerate(algorithms):
+ lengths = []
+ for maze in mazes:
+ val = next((r['path_length'] for r in valid_results
+ if r['maze'] == maze and r['strategy'] == algo), 0)
+ lengths.append(val)
+ bars = axes[2].bar(x + i * width, lengths, width, label=algo, alpha=0.8)
+ for bar, val in zip(bars, lengths):
+ if val > 0:
+ axes[2].text(bar.get_x() + bar.get_width() / 2, bar.get_height(),
+ f'{val:.0f}', ha='center', va='bottom', fontsize=7)
+
+ axes[2].set_title('path Length')
+ axes[2].set_ylabel('steps')
+ axes[2].set_xticks(x + width)
+ axes[2].set_xticklabels(mazes, rotation=45, ha='right', fontsize=8)
+ axes[2].legend()
+ axes[2].grid(alpha=0.3, axis='y')
+
+ plt.tight_layout()
+
+ output_path = os.path.join(DATA_PATH, 'experiment_results.png')
+ plt.savefig(output_path, dpi=150, bbox_inches='tight')
+ print(f"\nPlot saved to: {output_path}")
+ plt.show()
+
+
+def save_results_to_csv(results, filename='experiment_results.csv'):
+ if not results:
+ return
+
+ filepath = os.path.join(DATA_PATH, filename)
+ with open(filepath, 'w', newline='', encoding='utf-8') as f:
+ fieldnames = ['maze', 'strategy', 'time_ms', 'visited_cells', 'path_length', 'success_rate']
+ writer = csv.DictWriter(f, fieldnames=fieldnames)
+ writer.writeheader()
+ writer.writerows(results)
+
+ print(f"Results saved to: {filepath}")
+
+
+def analyze_efficiency(results):
+ valid_results = [r for r in results if r['time_ms'] > 0]
+ if not valid_results:
+ print("no valid results for analysis")
+ return
+
+ algo_stats = {}
+ for algo in ['BFS', 'DFS', 'A*']:
+ algo_data = [r for r in valid_results if r['strategy'] == algo]
+ if algo_data:
+ algo_stats[algo] = {
+ 'avg_time': sum(r['time_ms'] for r in algo_data) / len(algo_data),
+ 'avg_visited': sum(r['visited_cells'] for r in algo_data) / len(algo_data),
+ 'avg_length': sum(r['path_length'] for r in algo_data) / len(algo_data)
+ }
+
+
+ print("average values across all mazes")
+ print(f"{'Algorithm':<12} {'Time (ms)':<15} {'Visited':<15} {'Path length':<15}")
+
+ for algo, stats in algo_stats.items():
+ print(f"{algo:<12} {stats['avg_time']:<15.3f} {stats['avg_visited']:<15.1f} {stats['avg_length']:<15.1f}")
+
+ fastest = min(algo_stats.items(), key=lambda x: x[1]['avg_time'])
+ optimal = min(algo_stats.items(), key=lambda x: x[1]['avg_length'])
+ efficient = min(algo_stats.items(), key=lambda x: x[1]['avg_visited'])
+
+ print("conclusions:")
+ print(f" fastest algorithm: {fastest[0]} ({fastest[1]['avg_time']:.3f} ms avg)")
+ print(f" optimal path: {optimal[0]} ({optimal[1]['avg_length']:.1f} steps avg)")
+ print(f" most efficient (fewest visits): {efficient[0]} ({efficient[1]['avg_visited']:.0f} cells avg)")
+ print("=" * 70)
+
+
+def main():
+
+
+ if not os.path.exists(DATA_PATH):
+ print(f"\nerr: directory not found: {DATA_PATH}")
+ print("please create the directory and place maze files there.")
+ print("\nexpected structure:")
+ print(f" {DATA_PATH}/")
+ print(" ├── small.txt")
+ print(" ├── medium.txt")
+ print(" ├── large.txt")
+ print(" ├── empty.txt")
+ print(" └── no_exit.txt")
+ return
+
+ runner = ExperimentRunner()
+ results = runner.run_all_experiments(runs=5)
+
+ if not results:
+ print("\nNo results. Check if maze files exist in:", DATA_PATH)
+ return
+
+ save_results_to_csv(results)
+ analyze_efficiency(results)
+ create_visualizations(results)
+
+
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file