"""
Лабораторная работа: Применение паттернов проектирования
Этапы 1-6: Модель лабиринта, Builder, Strategy, MazeSolver, Observer/Command, эксперименты
"""

import time
import csv
import random
from collections import deque
from typing import List, Tuple, Dict, Set, Optional
import heapq
from dataclasses import dataclass
from abc import ABC, abstractmethod

# ============================================================
# Этап 1. Модель лабиринта
# ============================================================
class Cell:
    """Клетка лабиринта."""
    def __init__(self, x: int, y: int, is_wall: bool = False, weight: int = 1):
        self.x = x
        self.y = y
        self.is_wall = is_wall
        self.is_start = False
        self.is_exit = False
        self.weight = weight  # для взвешенных лабиринтов

    def is_passable(self) -> bool:
        return not self.is_wall

    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))

    def __repr__(self):
        return f"Cell({self.x},{self.y})"


class Maze:
    """Лабиринт, содержащий сетку клеток."""
    def __init__(self, width: int, height: int):
        self.width = width
        self.height = height
        self.cells = [[Cell(x, y) for x in range(width)] for y in range(height)]
        self.start_cell = None
        self.exit_cell = None

    def get_cell(self, x: int, y: int) -> Cell:
        if 0 <= x < self.width and 0 <= y < self.height:
            return self.cells[y][x]
        raise IndexError("Координаты вне границ лабиринта")

    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:
                n = self.cells[ny][nx]
                if n.is_passable():
                    neighbors.append(n)
        return neighbors

    def set_start(self, x: int, y: int):
        cell = self.get_cell(x, y)
        cell.is_start = True
        self.start_cell = cell

    def set_exit(self, x: int, y: int):
        cell = self.get_cell(x, y)
        cell.is_exit = True
        self.exit_cell = cell

    def copy(self):
        """Создаёт глубокую копию лабиринта (для взвешенных вариантов)."""
        new_maze = Maze(self.width, self.height)
        for y in range(self.height):
            for x in range(self.width):
                orig = self.cells[y][x]
                new_maze.cells[y][x] = Cell(x, y, orig.is_wall, orig.weight)
                if orig.is_start:
                    new_maze.set_start(x, y)
                if orig.is_exit:
                    new_maze.set_exit(x, y)
        return new_maze


# ============================================================
# Этап 2. Builder для загрузки из текстового файла
# ============================================================
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 f:
            lines = [line.rstrip('\n') for line in f]

        if not lines:
            raise ValueError("Файл пуст")

        height = len(lines)
        width = max(len(line) for line in lines)

        grid = []
        for y, line in enumerate(lines):
            row = []
            for x in range(width):
                ch = line[x] if x < len(line) else ' '
                row.append(ch)
            grid.append(row)

        maze = Maze(width, height)
        start_found = exit_found = False

        for y in range(height):
            for x in range(width):
                ch = grid[y][x]
                cell = maze.get_cell(x, y)

                if ch == '#':
                    cell.is_wall = True
                elif ch == 'S':
                    if start_found:
                        raise ValueError("Обнаружено несколько стартовых клеток 'S'")
                    cell.is_start = True
                    maze.start_cell = cell
                    start_found = True
                elif ch == 'E':
                    if exit_found:
                        raise ValueError("Обнаружено несколько выходных клеток 'E'")
                    cell.is_exit = True
                    maze.exit_cell = cell
                    exit_found = True
                elif ch != ' ':
                    raise ValueError(f"Недопустимый символ '{ch}' в позиции ({x},{y})")

        if not start_found:
            raise ValueError("Отсутствует стартовая клетка 'S'")
        if not exit_found:
            raise ValueError("Отсутствует выходная клетка 'E'")

        return maze


# ============================================================
# Этап 3. Стратегии поиска пути (возвращают путь и число посещённых)
# ============================================================
class PathFindingStrategy(ABC):
    @abstractmethod
    def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
        """Возвращает (путь, количество посещённых клеток)."""
        pass


class BFSStrategy(PathFindingStrategy):
    """Поиск в ширину – гарантирует кратчайший путь."""
    def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
        if start == exit_cell:
            return [start], 1

        queue = deque([start])
        visited = {start}
        parent = {start: None}
        visited_count = 1

        while queue:
            cur = queue.popleft()
            if cur == exit_cell:
                path = []
                while cur is not None:
                    path.append(cur)
                    cur = parent[cur]
                path.reverse()
                return path, visited_count

            for nb in maze.get_neighbors(cur):
                if nb not in visited:
                    visited.add(nb)
                    visited_count += 1
                    parent[nb] = cur
                    queue.append(nb)

        return [], visited_count


class DFSStrategy(PathFindingStrategy):
    """Поиск в глубину – быстрый, но не гарантирует кратчайший путь."""
    def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
        if start == exit_cell:
            return [start], 1

        stack = [(start, [start])]
        visited = set()
        visited_count = 0

        while stack:
            cur, path = stack.pop()
            if cur in visited:
                continue
            visited.add(cur)
            visited_count += 1

            if cur == exit_cell:
                return path, visited_count

            for nb in maze.get_neighbors(cur):
                if nb not in visited:
                    stack.append((nb, path + [nb]))

        return [], visited_count


class AStarStrategy(PathFindingStrategy):
    """А* с эвристикой Манхэттенского расстояния."""
    def _heuristic(self, a: Cell, b: Cell) -> float:
        return abs(a.x - b.x) + abs(a.y - b.y)

    def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
        if start == exit_cell:
            return [start], 1

        open_set = []
        counter = 0
        heapq.heappush(open_set, (0, counter, start))
        g_score = {start: 0}
        f_score = {start: self._heuristic(start, exit_cell)}
        parent = {start: None}
        visited_count = 1

        while open_set:
            _, _, cur = heapq.heappop(open_set)
            if cur == exit_cell:
                path = []
                while cur is not None:
                    path.append(cur)
                    cur = parent[cur]
                path.reverse()
                return path, visited_count

            for nb in maze.get_neighbors(cur):
                move_cost = nb.weight
                tentative = g_score[cur] + move_cost
                if nb not in g_score or tentative < g_score[nb]:
                    parent[nb] = cur
                    g_score[nb] = tentative
                    f_score[nb] = tentative + self._heuristic(nb, exit_cell)
                    counter += 1
                    heapq.heappush(open_set, (f_score[nb], counter, nb))
                    visited_count += 1

        return [], visited_count


class DijkstraStrategy(PathFindingStrategy):
    """Алгоритм Дейкстры для взвешенных графов."""
    def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
        if start == exit_cell:
            return [start], 1

        pq = []
        counter = 0
        heapq.heappush(pq, (0, counter, start))
        dist = {start: 0}
        parent = {start: None}
        visited_count = 1

        while pq:
            cur_dist, _, cur = heapq.heappop(pq)
            if cur_dist > dist.get(cur, float('inf')):
                continue

            if cur == exit_cell:
                path = []
                while cur is not None:
                    path.append(cur)
                    cur = parent[cur]
                path.reverse()
                return path, visited_count

            for nb in maze.get_neighbors(cur):
                new_dist = cur_dist + nb.weight
                if new_dist < dist.get(nb, float('inf')):
                    dist[nb] = new_dist
                    parent[nb] = cur
                    counter += 1
                    heapq.heappush(pq, (new_dist, counter, nb))
                    visited_count += 1

        return [], visited_count


# ============================================================
# Этап 4. MazeSolver (оркестратор)
# ============================================================
@dataclass
class SearchStats:
    path_length: int
    visited_cells: int
    time_ms: float


class MazeSolver:
    """Оркестратор: управляет лабиринтом и стратегией поиска."""
    def __init__(self, maze: Maze, strategy: PathFindingStrategy):
        self.maze = maze
        self.strategy = strategy

    def set_strategy(self, strategy: PathFindingStrategy):
        self.strategy = strategy

    def solve(self) -> SearchStats:
        start_time = time.perf_counter()
        path, visited = self.strategy.find_path(self.maze, self.maze.start_cell, self.maze.exit_cell)
        end_time = time.perf_counter()
        return SearchStats(len(path), visited, (end_time - start_time) * 1000.0)


# ============================================================
# Этап 5. Observer и Command (визуализация и пошаговое управление)
# ============================================================
class Observer(ABC):
    @abstractmethod
    def update(self, event: str, data: dict = None):
        pass


class ConsoleView(Observer):
    """Отображает лабиринт, позицию игрока и найденный путь."""
    def __init__(self):
        self.last_maze = None
        self.last_player_pos = None
        self.last_path = None

    def update(self, event: str, data: dict = None):
        if event == "maze_loaded":
            self.last_maze = data["maze"]
            self.render()
        elif event == "player_moved":
            self.last_maze = data["maze"]
            self.last_player_pos = data["player_pos"]
            self.render()
        elif event == "path_found":
            self.last_path = data["path"]
            self.render()
        elif event == "clear_path":
            self.last_path = None
            self.render()

    def render(self):
        if self.last_maze is None:
            print("Нет лабиринта для отображения")
            return

        maze = self.last_maze
        player = self.last_player_pos
        path_set = set(self.last_path) if self.last_path else set()

        for y in range(maze.height):
            row = []
            for x in range(maze.width):
                cell = maze.get_cell(x, y)
                if player and cell == player:
                    row.append('@')
                elif cell == maze.start_cell:
                    row.append('S')
                elif cell == maze.exit_cell:
                    row.append('E')
                elif cell in path_set and cell.is_passable():
                    row.append('*')
                elif cell.is_wall:
                    row.append('#')
                else:
                    row.append(' ')
            print(''.join(row))
        print()


class Player:
    """Игрок, перемещающийся по лабиринту."""
    def __init__(self, start_cell: Cell):
        self.position = start_cell

    def move_to(self, new_cell: Cell):
        self.position = new_cell


class Command(ABC):
    @abstractmethod
    def execute(self) -> bool:
        pass

    @abstractmethod
    def undo(self):
        pass


class MoveCommand(Command):
    """Команда перемещения игрока."""
    def __init__(self, player: Player, maze: Maze, direction: str):
        self.player = player
        self.maze = maze
        self.direction = direction
        self.prev_position = player.position
        self.new_position = None

    def execute(self) -> bool:
        dx, dy = 0, 0
        if self.direction == 'W':
            dy = -1
        elif self.direction == 'S':
            dy = 1
        elif self.direction == 'A':
            dx = -1
        elif self.direction == 'D':
            dx = 1
        else:
            return False

        nx = self.player.position.x + dx
        ny = self.player.position.y + dy
        try:
            target = self.maze.get_cell(nx, ny)
            if target.is_passable():
                self.new_position = target
                self.player.move_to(target)
                return True
        except IndexError:
            pass
        return False

    def undo(self):
        if self.prev_position:
            self.player.move_to(self.prev_position)


class GameController:
    """Управляет игрой, наблюдателями и командами."""
    def __init__(self, maze: Maze):
        self.maze = maze
        self.player = Player(maze.start_cell)
        self.observers = []
        self.command_stack = []

    def attach(self, observer: Observer):
        self.observers.append(observer)

    def detach(self, observer: Observer):
        self.observers.remove(observer)

    def notify(self, event: str, data: dict = None):
        for obs in self.observers:
            obs.update(event, data or {})

    def load_maze(self, maze: Maze):
        self.maze = maze
        self.player = Player(maze.start_cell)
        self.notify("maze_loaded", {"maze": maze})

    def find_path(self, strategy: PathFindingStrategy) -> List[Cell]:
        solver = MazeSolver(self.maze, strategy)
        stats = solver.solve()
        print(f"Длина пути: {stats.path_length}, посещено: {stats.visited_cells}, время: {stats.time_ms:.3f} мс")
        path, _ = strategy.find_path(self.maze, self.maze.start_cell, self.maze.exit_cell)
        self.notify("path_found", {"path": path})
        return path

    def clear_path(self):
        self.notify("clear_path", {})

    def execute_command(self, cmd: Command):
        if cmd.execute():
            self.command_stack.append(cmd)
            self.notify("player_moved", {"maze": self.maze, "player_pos": self.player.position})

    def undo(self):
        if self.command_stack:
            cmd = self.command_stack.pop()
            cmd.undo()
            self.notify("player_moved", {"maze": self.maze, "player_pos": self.player.position})


# ============================================================
# Этап 6. Генераторы тестовых лабиринтов
# ============================================================
def generate_simple_maze(width: int, height: int) -> Maze:
    """Маленький лабиринт с простым путём."""
    maze = Maze(width, height)
    for y in range(height):
        for x in range(width):
            maze.cells[y][x].is_wall = True

    x, y = 0, 0
    path = [(x, y)]
    while x < width - 1 or y < height - 1:
        if x < width - 1 and (y == height - 1 or random.random() < 0.5):
            x += 1
        else:
            if y < height - 1:
                y += 1
        path.append((x, y))

    for px, py in path:
        maze.cells[py][px].is_wall = False

    maze.set_start(0, 0)
    maze.set_exit(width - 1, height - 1)
    return maze


def generate_with_dead_ends(width: int, height: int) -> Maze:
    """Средний лабиринт с гарантированным путём и множеством тупиков."""
    maze = Maze(width, height)
    for y in range(height):
        for x in range(width):
            maze.cells[y][x].is_wall = True

    x, y = 0, 0
    main_path = []
    while x < width - 1 or y < height - 1:
        main_path.append((x, y))
        if x < width - 1 and (y == height - 1 or random.random() < 0.6):
            x += 1
        else:
            if y < height - 1:
                y += 1
            else:
                x += 1
    main_path.append((width - 1, height - 1))

    for px, py in main_path:
        maze.cells[py][px].is_wall = False

    num_dead_ends = int(width * height * 0.08)
    for _ in range(num_dead_ends):
        base_x, base_y = random.choice(main_path)
        directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
        random.shuffle(directions)
        for dx, dy in directions:
            nx, ny = base_x + dx, base_y + dy
            if 0 <= nx < width and 0 <= ny < height and maze.cells[ny][nx].is_wall:
                length = random.randint(2, 4)
                for step in range(length):
                    if 0 <= nx < width and 0 <= ny < height and maze.cells[ny][nx].is_wall:
                        maze.cells[ny][nx].is_wall = False
                        nx += dx
                        ny += dy
                    else:
                        break
                break

    maze.set_start(0, 0)
    maze.set_exit(width - 1, height - 1)
    return maze


def generate_complex_maze(width: int, height: int) -> Maze:
    """Большой лабиринт с гарантированным путём и высокой запутанностью."""
    maze = Maze(width, height)
    for y in range(height):
        for x in range(width):
            maze.cells[y][x].is_wall = True

    x, y = 0, 0
    main_path = []
    while x < width - 1 or y < height - 1:
        main_path.append((x, y))
        if x < width - 1 and (y == height - 1 or random.random() < 0.7):
            x += 1
        else:
            if y < height - 1:
                y += 1
            else:
                x += 1
    main_path.append((width - 1, height - 1))

    for px, py in main_path:
        maze.cells[py][px].is_wall = False

    num_branches = int(width * height * 0.12)
    for _ in range(num_branches):
        base_x, base_y = random.choice(main_path)
        directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
        random.shuffle(directions)
        for dx, dy in directions:
            nx, ny = base_x + dx, base_y + dy
            if 0 <= nx < width and 0 <= ny < height and maze.cells[ny][nx].is_wall:
                length = random.randint(1, 5)
                branch = []
                for step in range(length):
                    if 0 <= nx < width and 0 <= ny < height and maze.cells[ny][nx].is_wall:
                        maze.cells[ny][nx].is_wall = False
                        branch.append((nx, ny))
                        nx += dx
                        ny += dy
                    else:
                        break
                if random.random() < 0.3 and len(branch) >= 2:
                    bx, by = branch[-1]
                    for ddx, ddy in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
                        nnx, nny = bx + ddx, by + ddy
                        if (0 <= nnx < width and 0 <= nny < height and
                            maze.cells[nny][nnx].is_wall and random.random() < 0.5):
                            maze.cells[nny][nnx].is_wall = False
                break

    maze.set_start(0, 0)
    maze.set_exit(width - 1, height - 1)
    return maze


def generate_empty_maze(width: int, height: int) -> Maze:
    """Пустой лабиринт без стен."""
    maze = Maze(width, height)
    for y in range(height):
        for x in range(width):
            maze.cells[y][x].is_wall = False
    maze.set_start(0, 0)
    maze.set_exit(width - 1, height - 1)
    return maze


def generate_no_exit_maze(width: int, height: int) -> Maze:
    """Лабиринт без выхода (выход окружён стенами)."""
    maze = generate_empty_maze(width, height)
    ex, ey = width - 1, height - 1
    for dx, dy in [(0, 0), (0, -1), (0, 1), (-1, 0), (1, 0), (-1, -1), (-1, 1), (1, -1), (1, 1)]:
        nx, ny = ex + dx, ey + dy
        if 0 <= nx < width and 0 <= ny < height:
            if not (nx == 0 and ny == 0):
                maze.cells[ny][nx].is_wall = True
    maze.cells[ey][ex].is_wall = False
    maze.set_exit(ex, ey)
    return maze


# ============================================================
# Экспериментальная часть
# ============================================================
def run_experiment(maze: Maze, strategies: List[Tuple[str, PathFindingStrategy]], runs: int = 5) -> List[dict]:
    """Запускает эксперимент на одном лабиринте и возвращает усреднённые результаты."""
    results = []
    for name, strategy in strategies:
        times = []
        visited_counts = []
        path_lengths = []
        for _ in range(runs):
            solver = MazeSolver(maze, strategy)
            stats = solver.solve()
            times.append(stats.time_ms)
            visited_counts.append(stats.visited_cells)
            path_lengths.append(stats.path_length)

        avg_time = sum(times) / runs
        variance = sum((t - avg_time) ** 2 for t in times) / runs
        std_time = variance ** 0.5

        results.append({
            'maze_type': '',
            'strategy': name,
            'avg_time_ms': avg_time,
            'std_time_ms': std_time,
            'avg_visited': sum(visited_counts) / runs,
            'avg_path_len': sum(path_lengths) / runs,
            'path_found': all(l > 0 for l in path_lengths)
        })
    return results


def save_results_to_csv(results: List[dict], filename: str):
    """Сохраняет результаты в CSV с разделителем ';' для совместимости с Excel."""
    if not results:
        return
    with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
        writer = csv.DictWriter(f, fieldnames=results[0].keys(), delimiter=';')
        writer.writeheader()
        for row in results:
            row_copy = {}
            for k, v in row.items():
                if isinstance(v, float):
                    row_copy[k] = f"{v:.6f}".replace(',', '.')
                else:
                    row_copy[k] = v
            writer.writerow(row_copy)


# ============================================================
# Взвешенные лабиринты (опциональное задание)
# ============================================================
def assign_weights_random(maze: Maze, weights: List[Tuple[float, int]]) -> Maze:
    """Присваивает веса клеткам согласно вероятностям."""
    for y in range(maze.height):
        for x in range(maze.width):
            if not maze.cells[y][x].is_wall:
                r = random.random()
                cum = 0
                for prob, w in weights:
                    cum += prob
                    if r < cum:
                        maze.cells[y][x].weight = w
                        break
    return maze


def weighted_experiment():
    """Дополнительный эксперимент со взвешенными клетками."""
    print("\n=== ВЗВЕШЕННЫЕ ЛАБИРИНТЫ (опциональное задание) ===")
    maze = generate_with_dead_ends(30, 30)
    assign_weights_random(maze, [(0.8, 1), (0.15, 3), (0.05, 2)])

    strategies = [
        ("A* (манхэттен)", AStarStrategy()),
        ("Dijkstra", DijkstraStrategy())
    ]

    print("Лабиринт 30x30 со взвешенными клетками (болото 3, песок 2, асфальт 1)")
    results = run_experiment(maze, strategies, runs=10)

    for r in results:
        print(f"{r['strategy']:15} | Время: {r['avg_time_ms']:.2f} мс | "
              f"Посещено: {r['avg_visited']:.0f} | Длина пути: {r['avg_path_len']:.0f}")

    # Сравнение с BFS
    bfs = BFSStrategy()
    path_bfs, _ = bfs.find_path(maze, maze.start_cell, maze.exit_cell)
    if path_bfs:
        cost_bfs = sum(cell.weight for cell in path_bfs)
        print(f"BFS нашёл путь длиной {len(path_bfs)} клеток, стоимость = {cost_bfs}")

    path_dijkstra, _ = DijkstraStrategy().find_path(maze, maze.start_cell, maze.exit_cell)
    if path_dijkstra:
        cost_dijkstra = sum(cell.weight for cell in path_dijkstra)
        print(f"Dijkstra нашёл путь длиной {len(path_dijkstra)} клеток, стоимость = {cost_dijkstra}")

    path_astar, _ = AStarStrategy().find_path(maze, maze.start_cell, maze.exit_cell)
    if path_astar:
        cost_astar = sum(cell.weight for cell in path_astar)
        print(f"A* нашёл путь длиной {len(path_astar)} клеток, стоимость = {cost_astar}")


# ============================================================
# Демонстрация работы Observer и Command (по желанию)
# ============================================================
def demo_observer_command():
    """Демонстрирует паттерны Observer и Command."""
    print("\n=== ДЕМОНСТРАЦИЯ OBSERVER И COMMAND ===")
    maze = generate_simple_maze(10, 10)

    controller = GameController(maze)
    view = ConsoleView()
    controller.attach(view)

    print("Лабиринт загружен:")
    controller.load_maze(maze)

    print("Поиск пути с помощью BFS:")
    controller.find_path(BFSStrategy())

    input("Нажмите Enter для пошагового управления...")

    controller.clear_path()
    print("\nУправление: W/A/S/D - движение, Z - отмена, Q - выход")
    while True:
        cmd = input("> ").upper().strip()
        if cmd == 'Q':
            break
        elif cmd == 'Z':
            controller.undo()
        elif cmd in ('W', 'A', 'S', 'D'):
            move_cmd = MoveCommand(controller.player, controller.maze, cmd)
            controller.execute_command(move_cmd)
        else:
            print("Неизвестная команда")


# ============================================================
# Основной эксперимент
# ============================================================
def main():
    """Основной эксперимент: сравнение стратегий на различных лабиринтах."""
    print("=== ЗАПУСК ЭКСПЕРИМЕНТОВ ===")

    strategies = [
        ("BFS", BFSStrategy()),
        ("DFS", DFSStrategy()),
        ("A*", AStarStrategy()),
        ("Dijkstra", DijkstraStrategy())
    ]

    # Генерация тестовых лабиринтов
    maze_definitions = {
        "small_10x10_simple": generate_simple_maze(10, 10),
        "medium_50x50_deadends": generate_with_dead_ends(50, 50),
        "large_100x100_complex": generate_complex_maze(100, 100),
        "empty_50x50": generate_empty_maze(50, 50),
        "no_exit_50x50": generate_no_exit_maze(50, 50)
    }

    all_results = []

    for maze_name, maze in maze_definitions.items():
        print(f"\nЗапуск на лабиринте: {maze_name} ({maze.width}x{maze.height})")
        results = run_experiment(maze, strategies, runs=5)

        for r in results:
            r['maze_type'] = maze_name
            all_results.append(r)

        # Вывод промежуточных результатов
        for r in results:
            print(f"  {r['strategy']:8} | Время: {r['avg_time_ms']:7.2f}±{r['std_time_ms']:.2f} мс | "
                  f"Посещено: {r['avg_visited']:7.0f} | Длина пути: {r['avg_path_len']:5.0f}")

    # Сохранение результатов
    save_results_to_csv(all_results, "experiment_results.csv")
    print("\nРезультаты сохранены в experiment_results.csv")

    # Вывод сводной таблицы
    print("\n" + "=" * 100)
    print("СВОДНАЯ ТАБЛИЦА РЕЗУЛЬТАТОВ")
    print("=" * 100)
    print(f"{'Лабиринт':<25} | {'Стратегия':<10} | {'Время (мс)':<15} | {'Посещено':<10} | {'Длина пути':<10}")
    print("-" * 100)
    for r in all_results:
        print(f"{r['maze_type']:<25} | {r['strategy']:<10} | {r['avg_time_ms']:>8.2f} ± {r['std_time_ms']:<5.2f} | "
              f"{r['avg_visited']:>8.0f}   | {r['avg_path_len']:>8.0f}")


# ============================================================
# Запуск
# ============================================================
if __name__ == "__main__":
    main()

    # Раскомментируйте для демонстрации:
    # demo_observer_command()
    # weighted_experiment()