import sys
from collections import deque
import heapq
import time
import os

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):
        return not self.is_wall
    
    def __repr__(self):
        return f"Cell({self.x}, {self.y})"


class Maze:
    """лабиринт"""
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.cells = [[Cell(x, y) for x in range(width)] for y in range(height)]
        self.start = None
        self.exit = None
    
    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 = [(-1, 0), (1, 0), (0, -1), (0, 1)]
        for dx, dy in directions:
            neighbor = self.get_cell(cell.x + dx, cell.y + dy)
            if neighbor and neighbor.is_passable():
                neighbors.append(neighbor)
        return neighbors
    
    def set_start(self, x, y):
        cell = self.get_cell(x, y)
        if cell:
            cell.is_start = True
            self.start = cell
    
    def set_exit(self, x, y):
        cell = self.get_cell(x, y)
        if cell:
            cell.is_exit = True
            self.exit = cell



class MazeBuilder:
    """интерфейс строителя лабиринта"""
    
    def buildFromFile(self, filename):
        raise NotImplementedError


class TextFileMazeBuilder(MazeBuilder):
    """загрузка лабиринта из текстового файла"""
    
    def buildFromFile(self, filename):
        with open(filename, 'r', encoding='utf-8') as f:
            lines = [line.rstrip('\n') for line in f.readlines()]
        
        height = len(lines)
        width = max(len(line) for line in lines) if height > 0 else 0
        
        for i in range(height):
            if len(lines[i]) < width:
                lines[i] = lines[i] + ' ' * (width - len(lines[i]))
        
        maze = Maze(width, height)
        start_count = 0
        exit_count = 0
        
        for y, line in enumerate(lines):
            for x, ch in enumerate(line):
                if ch == '#':
                    maze.get_cell(x, y).is_wall = True
                elif ch == 'S':
                    maze.set_start(x, y)
                    start_count += 1
                elif ch == 'E':
                    maze.set_exit(x, y)
                    exit_count += 1
                else:
                    maze.get_cell(x, y).is_wall = False
        
        if start_count != 1 or exit_count != 1:
            raise ValueError(f"Ошибка: S={start_count}, E={exit_count} (нужно по одному)")
        
        return maze
    
class SearchStats:
    """статистика поиска"""
    def __init__(self, time_ms=0, visited_cells=0, path_length=0):
        self.time_ms = time_ms
        self.visited_cells = visited_cells
        self.path_length = path_length
    
    def __str__(self):
        return f"Время: {self.time_ms:.2f} мс, Посещено: {self.visited_cells}, Длина пути: {self.path_length}"


class PathFindingStrategy:
    """интерфейс стратегии поиска пути"""
    
    def findPath(self, maze, start, exit):
        raise NotImplementedError
    
    def get_name(self):
        raise NotImplementedError


class BFSStrategy(PathFindingStrategy):
    """BFS - гарантирует кратчайший путь"""
    
    def get_name(self):
        return "BFS (Поиск в ширину)"
    
    def findPath(self, maze, start, exit):
        from collections import deque
        
        if not start or not exit:
            return [], 0
        
        queue = deque([(start, [start])])
        visited = {start}
        
        while queue:
            current, path = queue.popleft()
            
            if current == exit:
                return path, len(visited)
            
            for neighbor in maze.get_neighbors(current):
                if neighbor not in visited:
                    visited.add(neighbor)
                    queue.append((neighbor, path + [neighbor]))
        
        return [], len(visited)


class DFSStrategy(PathFindingStrategy):
    """DFS - быстрый, но не обязательно кратчайший"""
    
    def get_name(self):
        return "DFS (Поиск в глубину)"
    
    def findPath(self, maze, start, exit):
        if not start or not exit:
            return [], 0
        
        stack = [(start, [start])]
        visited = {start}
        
        while stack:
            current, path = stack.pop()
            
            if current == exit:
                return path, len(visited)
            
            for neighbor in maze.get_neighbors(current):
                if neighbor not in visited:
                    visited.add(neighbor)
                    stack.append((neighbor, path + [neighbor]))
        
        return [], len(visited)


class AStarStrategy(PathFindingStrategy):
    """алгоритм A Star - оптимальный и быстрый с эвристикой"""
    
    def get_name(self):
        return "A Star"
    
    def _heuristic(self, a, b):
        return abs(a.x - b.x) + abs(a.y - b.y)
    
    def findPath(self, maze, start, exit):
        if not start or not exit:
            return [], 0
        
        import heapq
        
        heap = []
        counter = 0
        start_f = self._heuristic(start, exit)
        heapq.heappush(heap, (start_f, counter, start))
        
        came_from = {}
        g_score = {start: 0}
        f_score = {start: start_f}
        visited = set()
        visited.add(start)
        
        while heap:
            current_f, _, current = heapq.heappop(heap)
            
            if current == exit:
                path = []
                while current in came_from:
                    path.append(current)
                    current = came_from[current]
                path.append(start)
                path.reverse()
                return path, len(visited)
            
            if current_f > f_score.get(current, float('inf')):
                continue
            
            for neighbor in maze.get_neighbors(current):
                tentative_g = g_score[current] + 1
                
                if tentative_g < g_score.get(neighbor, float('inf')):
                    came_from[neighbor] = current
                    g_score[neighbor] = tentative_g
                    new_f = tentative_g + self._heuristic(neighbor, exit)
                    f_score[neighbor] = new_f
                    counter += 1
                    heapq.heappush(heap, (new_f, counter, neighbor))
                    visited.add(neighbor)
        
        return [], len(visited)


class DijkstraStrategy(PathFindingStrategy):
    """алгоритм Дейкстры"""
    
    def get_name(self):
        return "Дейкстра (Dijkstra)"
    
    def findPath(self, maze, start, exit):
        if not start or not exit:
            return [], 0
        
        import heapq
        
        heap = []
        counter = 0
        heapq.heappush(heap, (0, counter, start))
        
        distances = {start: 0}
        came_from = {}
        visited = set()
        visited.add(start)
        
        while heap:
            current_dist, _, current = heapq.heappop(heap)
            
            if current == exit:
                path = []
                while current in came_from:
                    path.append(current)
                    current = came_from[current]
                path.append(start)
                path.reverse()
                return path, len(visited)
            
            if current_dist > distances.get(current, float('inf')):
                continue
            
            for neighbor in maze.get_neighbors(current):
                new_dist = current_dist + 1
                
                if new_dist < distances.get(neighbor, float('inf')):
                    distances[neighbor] = new_dist
                    came_from[neighbor] = current
                    counter += 1
                    heapq.heappush(heap, (new_dist, counter, neighbor))
                    visited.add(neighbor)
        
        return [], len(visited)
    


class MazeSolver:
    """решатель лабиринта - оркестратор, использующий стратегию"""
    
    def __init__(self, maze):
        self.maze = maze
        self._strategy = None
    
    def setStrategy(self, strategy):
        """динамическая смена стратегии поиска"""
        self._strategy = strategy
        print(f"  Стратегия изменена на: {strategy.get_name()}")
    
    def solve(self):
        """
        решение лабиринта с использованием текущей стратегии.
        возвращает время, посещённые клетки, длина пути
        """
        if self._strategy is None:
            raise ValueError("Стратегия не установлена. Используйте setStrategy()")
        
        
        start_time = time.perf_counter()
        
      
        path, visited = self._strategy.findPath(self.maze, self.maze.start, self.maze.exit)
        
        
        end_time = time.perf_counter()
        time_ms = (end_time - start_time) * 1000
        
        
        stats = SearchStats(
            time_ms=time_ms,
            visited_cells=visited,
            path_length=len(path) if path else 0
        )
        
        return path, stats


class Observer:
    """интерфейс наблюдателя"""
    
    def update(self, event, data):
        raise NotImplementedError


class ConsoleDisplay(Observer):
    """консольная визуализация - наблюдатель"""
    
    def __init__(self):
        self._last_path = None
        self._last_maze = None
    
    def update(self, event, data):
        if event == "maze_loaded":
            self._draw_maze(data)
        elif event == "path_found":
            self._last_path = data
            self._show_path(data)
        elif event == "player_moved":
            self._draw_maze_with_player(data)
    
    def _draw_maze(self, maze):
        """Отрисовка лабиринта"""
        os.system('cls' if os.name == 'nt' else 'clear')
        print("=" * (maze.width * 2 + 4))
        print("ЛАБИРИНТ")
        print("=" * (maze.width * 2 + 4))
        
        for y in range(maze.height):
            line = ""
            for x in range(maze.width):
                cell = maze.get_cell(x, y)
                if cell == maze.start:
                    line += "S "
                elif cell == maze.exit:
                    line += "E "
                elif cell.is_wall:
                    line += "# "
                else:
                    line += ". "
            print(line)
        
        print("=" * (maze.width * 2 + 4))
        print("S - старт   E - выход   # - стена   . - проход")
    
    def _draw_maze_with_player(self, game_state):
        """отрисовка лабиринта с игроком"""
        maze = game_state['maze']
        player = game_state['player']
        
        os.system('cls' if os.name == 'nt' else 'clear')
        print("=" * (maze.width * 2 + 4))
        print("ЛАБИРИНТ (P - игрок)")
        print("=" * (maze.width * 2 + 4))
        
        for y in range(maze.height):
            line = ""
            for x in range(maze.width):
                cell = maze.get_cell(x, y)
                if player and cell == player.get_position():
                    line += "P "
                elif cell == maze.start:
                    line += "S "
                elif cell == maze.exit:
                    line += "E "
                elif cell.is_wall:
                    line += "# "
                else:
                    line += ". "
            print(line)
        
        print("=" * (maze.width * 2 + 4))
        if player:
            pos = player.get_position()
            print(f"Игрок: ({pos.x}, {pos.y})")
        print("S - старт   E - выход   # - стена   . - проход   P - игрок")
    
    def _show_path(self, path):
        """Показ информации о найденном пути"""
        if not path:
            print("\n  Путь не найден!")
            return
        print(f"\n  Путь найден! Длина: {len(path)} клеток")



class Command:
    """Интерфейс команды"""
    
    def execute(self):
        raise NotImplementedError
    
    def undo(self):
        raise NotImplementedError


class MoveCommand(Command):
    """Команда перемещения игрока"""
    
    def __init__(self, player, direction, maze):
        self._player = player
        self._dx, self._dy = direction
        self._maze = maze
        self._executed = False
        self._prev_position = None
    
    def execute(self):
        """Выполнение перемещения"""
        if self._executed:
            return False
        
        pos = self._player.get_position()
        new_x = pos.x + self._dx
        new_y = pos.y + self._dy
        target = self._maze.get_cell(new_x, new_y)
        
        if target and target.is_passable():
            self._prev_position = pos
            self._player.set_position(target)
            self._executed = True
            return True
        return False
    
    def undo(self):
        """Отмена перемещения"""
        if not self._executed or self._prev_position is None:
            return False
        
        self._player.set_position(self._prev_position)
        self._executed = False
        return True
    
    def get_name(self):
        dir_names = {(-1, 0): "ВЛЕВО", (1, 0): "ВПРАВО", (0, -1): "ВВЕРХ", (0, 1): "ВНИЗ"}
        return f"Перемещение {dir_names.get((self._dx, self._dy), 'НЕИЗВЕСТНО')}"


class CommandInvoker:
    """Инвокер команд (история для undo/redo)"""
    
    def __init__(self):
        self._history = []
        self._redo_stack = []
    
    def execute(self, command):
        """Выполнение команды с сохранением в истории"""
        if command.execute():
            self._history.append(command)
            self._redo_stack.clear()
            return True
        return False
    
    def undo(self):
        """Отмена последней команды"""
        if not self._history:
            return False
        
        command = self._history.pop()
        if command.undo():
            self._redo_stack.append(command)
            return True
        return False
    
    def redo(self):
        """Повтор отменённой команды"""
        if not self._redo_stack:
            return False
        
        command = self._redo_stack.pop()
        if command.execute():
            self._history.append(command)
            return True
        return False
    
    def get_history_size(self):
        return len(self._history)



class Player:
    """Игрок, перемещающийся по лабиринту"""
    
    def __init__(self, start_cell):
        self._position = start_cell
        self._start = start_cell
    
    def get_position(self):
        return self._position
    
    def set_position(self, cell):
        self._position = cell
    
    def reset(self):
        self._position = self._start
    
    def is_at_exit(self, maze):
        return self._position == maze.exit
    
    def get_steps_count(self, invoker):
        return invoker.get_history_size()



class GameController:
    """контроллер, объединяющий все компоненты"""
    
    def __init__(self, maze):
        self.maze = maze
        self.player = Player(maze.start)
        self.solver = MazeSolver(maze)
        self.invoker = CommandInvoker()
        self.view = ConsoleDisplay()
    
    def run(self):
        """запуск интерактивного режима"""
        self.view.update("maze_loaded", self.maze)
        
        print("УПРАВЛЕНИЕ:")
        print("  H/J/K/Ll - движение")
        print("  U - отменить ход")
        print("  R - повторить ход")
        print("  B - BFS поиск пути")
        print("  D - DFS поиск пути")
        print("  A - A* поиск пути")
        print("  P - показать путь")
        print("  Q - выход")
        
        path = None
        last_strategy_name = ""
        
        while True:
            cmd = input("\nКоманда > ").lower()
            
            if cmd == 'q':
                print("До встречи!")
                break
            
            elif cmd in ['h', 'j', 'k', 'l']:
                dir_map = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)}
                command = MoveCommand(self.player, dir_map[cmd], self.maze)
                
                if self.invoker.execute(command):
                    self.view.update("player_moved", {
                        'maze': self.maze,
                        'player': self.player
                    })
                    
                    if self.player.is_at_exit(self.maze):
                        print(f"\n  *** ПОБЕДА! ВЫХОД ДОСТИГНУТ за {self.player.get_steps_count(self.invoker)} шагов! ***")
                        break
                else:
                    print("  Стена! Нельзя пройти.")
            
            elif cmd == 'u':
                if self.invoker.undo():
                    self.view.update("player_moved", {
                        'maze': self.maze,
                        'player': self.player
                    })
                    print("  Отменено")
                else:
                    print("  Нечего отменять")
            
            elif cmd == 'r':
                if self.invoker.redo():
                    self.view.update("player_moved", {
                        'maze': self.maze,
                        'player': self.player
                    })
                    print("  Повторено")
                else:
                    print("  Нечего повторять")
            
            elif cmd == 'b':
                self.solver.setStrategy(BFSStrategy())
                start_time = time.perf_counter()
                path, stats = self.solver.solve()
                self.view.update("path_found", path)
                print(f"  BFS: {stats}")
                last_strategy_name = "BFS"
            
            elif cmd == 'd':
                self.solver.setStrategy(DFSStrategy())
                path, stats = self.solver.solve()
                self.view.update("path_found", path)
                print(f"  DFS: {stats}")
                last_strategy_name = "DFS"
            
            elif cmd == 'a':
                self.solver.setStrategy(AStarStrategy())
                path, stats = self.solver.solve()
                self.view.update("path_found", path)
                print(f"  A*: {stats}")
                last_strategy_name = "A*"
            
            elif cmd == 'p':
                if path:
                    self._show_path_on_maze(path)
                else:
                    print("  Сначала найдите путь (B, D или A)")
            
            else:
                print("  Неизвестная команда")
    
    def _show_path_on_maze(self, path):
        """показать путь на лабиринте"""
        os.system('cls' if os.name == 'nt' else 'clear')
        print("=" * (self.maze.width * 2 + 4))
        print("ЛАБИРИНТ С ПУТЁМ (* - путь)")
        print("=" * (self.maze.width * 2 + 4))
        
        path_set = set(path)
        
        for y in range(self.maze.height):
            line = ""
            for x in range(self.maze.width):
                cell = self.maze.get_cell(x, y)
                if cell == self.player.get_position():
                    line += "P "
                elif cell == self.maze.start:
                    line += "S "
                elif cell == self.maze.exit:
                    line += "E "
                elif cell in path_set and cell.is_passable():
                    line += "* "
                elif cell.is_wall:
                    line += "# "
                else:
                    line += ". "
            print(line)
        
        print("=" * (self.maze.width * 2 + 4))
        print("S - старт   E - выход   # - стена   . - проход   * - путь   P - игрок")
        input("\nНажмите Enter для продолжения...")
        self.view.update("player_moved", {
            'maze': self.maze,
            'player': self.player
        })

class MazeGenerator:
    """генератор тестовых лабиринтов различной сложности"""
    
    @staticmethod
    def create_empty_maze(width, height):
        """пустой лабиринт без стен"""
        maze = Maze(width, height)
        for y in range(height):
            for x in range(width):
                maze.get_cell(x, y).is_wall = False
        maze.set_start(0, 0)
        maze.set_exit(width - 1, height - 1)
        return maze
    
    @staticmethod
    def create_simple_maze(width, height):
        """простой лабиринт с прямым путём"""
        maze = Maze(width, height)
        
        # Заполняем стенами
        for y in range(height):
            for x in range(width):
                maze.get_cell(x, y).is_wall = True
        
        # Создаём прямой путь
        for i in range(min(width, height)):
            maze.get_cell(i, i).is_wall = False
            if i + 1 < width:
                maze.get_cell(i + 1, i).is_wall = False
            if i + 1 < height:
                maze.get_cell(i, i + 1).is_wall = False
        
        maze.set_start(0, 0)
        maze.set_exit(width - 1, height - 1)
        return maze
    
    @staticmethod
    def generate_dfs_maze(width, height):
        """генерация запутанного лабиринта алгоритмом DFS"""
        maze = Maze(width, height)
        
        # заполняем стенами
        for y in range(height):
            for x in range(width):
                maze.get_cell(x, y).is_wall = True
        
        start_x, start_y = 1, 1
        maze.get_cell(start_x, start_y).is_wall = False
        
        stack = [(start_x, start_y)]
        visited = {(start_x, start_y)}
        directions = [(0, -2), (0, 2), (-2, 0), (2, 0)]
        
        while stack:
            x, y = stack[-1]
            neighbors = []
            
            for dx, dy in directions:
                nx, ny = x + dx, y + dy
                if 0 < nx < width - 1 and 0 < ny < height - 1 and (nx, ny) not in visited:
                    neighbors.append((nx, ny, dx, dy))
            
            if neighbors:
                import random
                nx, ny, dx, dy = random.choice(neighbors)
                maze.get_cell(x + dx // 2, y + dy // 2).is_wall = False
                maze.get_cell(nx, ny).is_wall = False
                visited.add((nx, ny))
                stack.append((nx, ny))
            else:
                stack.pop()
        
        maze.set_start(start_x, start_y)
        
        # ищем дальнюю точку для выхода
        farthest = (start_x, start_y)
        max_dist = 0
        for y in range(height):
            for x in range(width):
                cell = maze.get_cell(x, y)
                if cell and not cell.is_wall:
                    dist = abs(x - start_x) + abs(y - start_y)
                    if dist > max_dist:
                        max_dist = dist
                        farthest = (x, y)
        
        # Устанавливаем выход
        maze.set_exit(farthest[0], farthest[1])
        
        # Дополнительная проверка: если выход всё ещё None - создаём принудительно
        if maze.exit is None:
            for y in range(height):
                for x in range(width):
                    cell = maze.get_cell(x, y)
                    if cell and not cell.is_wall and not cell.is_start:
                        maze.set_exit(x, y)
                        break
                if maze.exit:
                    break
        
        return maze
    
    @staticmethod
    def create_no_exit_maze(width, height):
        """лабиринт без выхода"""
        maze = MazeGenerator.generate_dfs_maze(width, height)
        if maze.exit:
            maze.exit.is_wall = True
            maze.exit.is_exit = False
            maze.exit = None
        return maze
    
    @staticmethod
    def save_to_file(maze, filename):
        """сохранение лабиринта в файл"""
        with open(filename, 'w', encoding='utf-8') as f:
            for y in range(maze.height):
                line = ""
                for x in range(maze.width):
                    cell = maze.get_cell(x, y)
                    if cell.is_start:
                        line += 'S'
                    elif cell.is_exit:
                        line += 'E'
                    elif cell.is_wall:
                        line += '#'
                    else:
                        line += '.'
                f.write(line + '\n')

class ExperimentRunner:
    """запуск экспериментов и сбор статистики"""
    
    def __init__(self, runs_per_experiment=5):
        self.runs_per_experiment = runs_per_experiment
        self.results = []
    
    def run_experiment(self, maze, strategy, maze_name):
        """запуск одного эксперимента"""
        times = []
        visited = []
        path_lengths = []
        
        for _ in range(self.runs_per_experiment):
            start_time = time.perf_counter()
            path, visited_count = strategy.findPath(maze, maze.start, maze.exit)
            end_time = time.perf_counter()
            
            times.append((end_time - start_time) * 1000)
            visited.append(visited_count)
            path_lengths.append(len(path) if path else 0)
        
        return {
            'maze_name': maze_name,
            'strategy': strategy.get_name(),
            'avg_time_ms': sum(times) / len(times),
            'min_time_ms': min(times),
            'max_time_ms': max(times),
            'avg_visited_cells': sum(visited) / len(visited),
            'min_visited_cells': min(visited),
            'max_visited_cells': max(visited),
            'avg_path_length': sum(path_lengths) / len(path_lengths),
            'min_path_length': min(path_lengths),
            'max_path_length': max(path_lengths),
            'path_found': any(pl > 0 for pl in path_lengths),
            'runs': self.runs_per_experiment
        }
    
    def run_all_experiments(self):
        """запуск всех экспериментов на всех лабиринтах"""
        print("ГЕНЕРАЦИЯ ТЕСТОВЫХ ЛАБИРИНТОВ")
        
        # Создаём тестовые лабиринты
        test_mazes = {
            'tiny_simple (10x10)': MazeGenerator.create_simple_maze(10, 10),
            'small_empty (20x20)': MazeGenerator.create_empty_maze(20, 20),
            'medium_dfs (30x30)': MazeGenerator.generate_dfs_maze(30, 30),
            'medium_complex (40x40)': MazeGenerator.generate_dfs_maze(40, 40),
            'large_dfs (50x50)': MazeGenerator.generate_dfs_maze(50, 50),
            'very_large_dfs (100x100)': MazeGenerator.generate_dfs_maze(100, 100),
            'no_exit (20x20)': MazeGenerator.create_no_exit_maze(20, 20)
        }
        
        # Сохраняем лабиринты в файлы
        for name, maze in test_mazes.items():
            filename = f"test_{name.replace(' ', '_').replace('(', '').replace(')', '')}.txt"
            MazeGenerator.save_to_file(maze, filename)
            print(f"  Создан: {filename}")
        
        # Стратегии для тестирования
        strategies = [
            BFSStrategy(),
            DFSStrategy(),
            AStarStrategy(),
            DijkstraStrategy()
        ]
        
        print("ЗАПУСК ЭКСПЕРИМЕНТОВ")
        
        for maze_name, maze in test_mazes.items():
            print(f"\n Лабиринт: {maze_name}")
            print(f"   Размер: {maze.width}x{maze.height}")
            print(f"   Старт: ({maze.start.x}, {maze.start.y})")
            
            # Проверяем, есть ли выход
            if maze.exit:
                print(f"   Выход: ({maze.exit.x}, {maze.exit.y})")
            else:
                print(f"   Выход: ОТСУТСТВУЕТ")
            
            for strategy in strategies:
                print(f"   → {strategy.get_name()}...", end=" ", flush=True)
                result = self.run_experiment(maze, strategy, maze_name)
                self.results.append(result)
                
                status = "✓" if result['path_found'] else "✗"
                print(f"{status} {result['avg_time_ms']:.2f}мс, "
                      f"{result['avg_visited_cells']:.0f} клеток, "
                      f"{result['avg_path_length']:.1f} шагов")
                
    def save_to_csv(self, filename="experiment_results.csv"):
        import csv

        if not self.results:
            print("Нет результатов для сохранения")
            return
        
        with open(filename, 'w', newline='', encoding='utf-8-sig') as csvfile:
            fieldnames = [
                'maze_name', 'strategy', 'runs',
                'avg_time_ms', 'min_time_ms', 'max_time_ms',
                'avg_visited_cells', 'min_visited_cells', 'max_visited_cells',
                'avg_path_length', 'min_path_length', 'max_path_length',
                'path_found'
            ]
            writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter=';')
            writer.writeheader()
            
            for result in self.results:
                writer.writerow(result)

        if os.path.exists(filename):
            print(f"\n Результаты сохранены в {filename}")
            print(f" Размер файла: {os.path.getsize(filename)} байт")
        else:
            print(f"\n Ошибка: файл {filename} не создан")
        
        
    
    def print_summary(self):
        print("СВОДНАЯ СТАТИСТИКА ЭКСПЕРИМЕНТОВ")


        # Группировка по лабиринтам
        grouped = {}
        for result in self.results:
            name = result['maze_name']
            if name not in grouped:
                grouped[name] = []
            grouped[name].append(result)
        
        for maze_name, results in grouped.items():
            print(f"\n {maze_name}")
            print(f"{'Стратегия':<25} {'Время(мс)':<12} {'Посещено':<12} {'Длина пути':<12} {'Найден':<8}")
            
            for result in sorted(results, key=lambda x: x['avg_time_ms']):
                status = "✓" if result['path_found'] else "✗"
                print(f"{result['strategy']:<25} "
                      f"{result['avg_time_ms']:<12.2f} "
                      f"{result['avg_visited_cells']:<12.0f} "
                      f"{result['avg_path_length']:<12.1f} "
                      f"{status:<8}")
        
        print("ОБЩАЯ СТАТИСТИКА ПО СТРАТЕГИЯМ")
        
        strategy_stats = {}
        for result in self.results:
            name = result['strategy']
            if name not in strategy_stats:
                strategy_stats[name] = {'times': [], 'visited': [], 'lengths': []}
            strategy_stats[name]['times'].append(result['avg_time_ms'])
            strategy_stats[name]['visited'].append(result['avg_visited_cells'])
            strategy_stats[name]['lengths'].append(result['avg_path_length'])
        
        print(f"\n{'Стратегия':<25} {'Ср.время(мс)':<15} {'Ср.посещено':<15} {'Ср.длина':<12}")
        
        for name, stats in strategy_stats.items():
            avg_time = sum(stats['times']) / len(stats['times'])
            avg_visited = sum(stats['visited']) / len(stats['visited'])
            avg_length = sum(stats['lengths']) / len(stats['lengths'])
            print(f"{name:<25} {avg_time:<15.2f} {avg_visited:<15.0f} {avg_length:<12.1f}")
    
    def print_conclusions(self):
        print("ВЫВОДЫ И РЕКОМЕНДАЦИИ")
       
        
        # Находим лучшие стратегии
        bfs_results = [r for r in self.results if r['strategy'] == "BFS (Поиск в ширину)" and r['path_found']]
        dfs_results = [r for r in self.results if r['strategy'] == "DFS (Поиск в глубину)" and r['path_found']]
        astar_results = [r for r in self.results if r['strategy'] == "A* (A Star)" and r['path_found']]
        dijkstra_results = [r for r in self.results if r['strategy'] == "Дейкстра (Dijkstra)" and r['path_found']]
        
        conclusions = []
        
        if bfs_results:
            avg_bfs_time = sum(r['avg_time_ms'] for r in bfs_results) / len(bfs_results)
            avg_bfs_length = sum(r['avg_path_length'] for r in bfs_results) / len(bfs_results)
            conclusions.append(f"  • BFS: среднее время {avg_bfs_time:.2f}мс, длина пути {avg_bfs_length:.1f}")
        
        if dfs_results:
            avg_dfs_time = sum(r['avg_time_ms'] for r in dfs_results) / len(dfs_results)
            avg_dfs_length = sum(r['avg_path_length'] for r in dfs_results) / len(dfs_results)
            conclusions.append(f"  • DFS: среднее время {avg_dfs_time:.2f}мс, длина пути {avg_dfs_length:.1f}")
        
        if astar_results:
            avg_astar_time = sum(r['avg_time_ms'] for r in astar_results) / len(astar_results)
            avg_astar_length = sum(r['avg_path_length'] for r in astar_results) / len(astar_results)
            conclusions.append(f"  • A*: среднее время {avg_astar_time:.2f}мс, длина пути {avg_astar_length:.1f}")
        
        if dijkstra_results:
            avg_dijkstra_time = sum(r['avg_time_ms'] for r in dijkstra_results) / len(dijkstra_results)
            avg_dijkstra_length = sum(r['avg_path_length'] for r in dijkstra_results) / len(dijkstra_results)
            conclusions.append(f"  • Дейкстра: среднее время {avg_dijkstra_time:.2f}мс, длина пути {avg_dijkstra_length:.1f}")
        
        print("\n РЕЗУЛЬТАТЫ АНАЛИЗА:\n")
        for c in conclusions:
            print(c)
        
        print("\n РЕКОМЕНДАЦИИ:\n")
        print("  1. Для маленьких лабиринтов - любой алгоритм работает быстро")
        print("  2. Для больших лабиринтов - A* даёт лучший компромисс скорость/качество")
        print("  3. BFS гарантирует кратчайший путь, но медленнее на больших картах")
        print("  4. DFS самый быстрый, но путь может быть неоптимальным")
        print("  5. Если путь не существует - BFS и A* эффективнее обнаруживают это")


def plot_experiment_results(csv_filename="experiment_results.csv"):
    """построение графиков по результатам экспериментов"""
    try:
        import matplotlib.pyplot as plt
        import pandas as pd
        import numpy as np
    except ImportError:
        print("Установите: pip install matplotlib pandas")
        return
    
    if not os.path.exists(csv_filename):
        print("Файл результатов не найден")
        return
    
    if os.path.getsize(csv_filename) == 0:
        print(f"Файл {csv_filename} пустой. Сначала запустите эксперименты.")
        return
    
    df = pd.read_csv(csv_filename, sep=';', encoding='utf-8-sig')
    
    if 'strategy' not in df.columns:
        print(f"Ошибка: в файле {csv_filename} нет колонки 'strategy'")
        print(f"Доступные колонки: {list(df.columns)}")
        return

    fig = plt.figure(figsize=(16, 12))
    fig.suptitle('Сравнение алгоритмов поиска в лабиринте', fontsize=16)
    
    # Время выполнения
    ax1 = fig.add_subplot(2, 2, 1)
    for strategy in df['strategy'].unique():
        data = df[df['strategy'] == strategy]
        ax1.bar(data['maze_name'], data['avg_time_ms'], alpha=0.7, label=strategy)
    ax1.set_xlabel('Лабиринт')
    ax1.set_ylabel('Время (мс)')
    ax1.set_title('Время выполнения алгоритмов')
    ax1.legend(loc='upper left', fontsize=8)
    ax1.tick_params(axis='x', rotation=45, labelsize=8)
    
    # Посещённые клетки
    ax2 = fig.add_subplot(2, 2, 2)
    for strategy in df['strategy'].unique():
        data = df[df['strategy'] == strategy]
        ax2.bar(data['maze_name'], data['avg_visited_cells'], alpha=0.7, label=strategy)
    ax2.set_xlabel('Лабиринт')
    ax2.set_ylabel('Посещено клеток')
    ax2.set_title('Эффективность поиска')
    ax2.legend(loc='upper left', fontsize=8)
    ax2.tick_params(axis='x', rotation=45, labelsize=8)
    
    # Длина пути
    ax3 = fig.add_subplot(2, 2, 3)
    for strategy in df['strategy'].unique():
        data = df[df['strategy'] == strategy]
        ax3.bar(data['maze_name'], data['avg_path_length'], alpha=0.7, label=strategy)
    ax3.set_xlabel('Лабиринт')
    ax3.set_ylabel('Длина пути')
    ax3.set_title('Оптимальность пути')
    ax3.legend(loc='upper left', fontsize=8)
    ax3.tick_params(axis='x', rotation=45, labelsize=8)
    
    # Радар-диаграмма
    ax4 = fig.add_subplot(2, 2, 4, projection='polar')
    
    strategies = df['strategy'].unique()
    metrics = ['avg_time_ms', 'avg_visited_cells', 'avg_path_length']
    metric_labels = ['Время', 'Посещено', 'Длина пути']
    
    for strategy in strategies:
        data = df[df['strategy'] == strategy]
        values = []
        for metric in metrics:
            val = data[metric].mean()
            max_val = df[metric].max()
            normalized = 1 - (val / max_val) if max_val > 0 else 0.5
            values.append(normalized)
        values.append(values[0])
        angles = np.linspace(0, 2 * np.pi, len(metrics), endpoint=False).tolist()
        angles += angles[:1]
        ax4.plot(angles, values, 'o-', linewidth=2, label=strategy)
        ax4.fill(angles, values, alpha=0.25)
    
    ax4.set_xticks(angles[:-1])
    ax4.set_xticklabels(metric_labels)
    ax4.set_ylim(0, 1)
    ax4.set_title('Радар-диаграмма (дальше от центра = лучше)')
    ax4.legend(loc='upper right', fontsize=7, bbox_to_anchor=(1.3, 1.0))
    
    plt.tight_layout()
    plt.savefig('algorithm_comparison.png', dpi=150, bbox_inches='tight')
    plt.show()
    print("График сохранён: algorithm_comparison.png")


def plot_by_maze(csv_filename="experiment_results.csv"):
    """построение графиков для каждого лабиринта"""
    try:
        import matplotlib.pyplot as plt
        import pandas as pd
    except ImportError:
        return
    
    if not os.path.exists(csv_filename):
        return
    

    df = pd.read_csv(csv_filename, sep=';', encoding='utf-8-sig')
    print("Колонки в файле:", list(df.columns))
    
    if 'maze_name' not in df.columns:
        for col in df.columns:
            if 'maze' in col.lower() or 'name' in col.lower():
                df.rename(columns={col: 'maze_name'}, inplace=True)
                break
                
    if 'maze_name' not in df.columns:
        print("Ошибка: колонка 'maze_name' не найдена")
        return
    
    mazes = df['maze_name'].unique()
    mazes = df['maze_name'].unique()
    
    fig, axes = plt.subplots(len(mazes), 3, figsize=(15, 4 * len(mazes)))
    fig.suptitle('Детальный анализ по каждому лабиринту', fontsize=14)
    
    if len(mazes) == 1:
        axes = axes.reshape(1, -1)
    
    for i, maze_name in enumerate(mazes):
        maze_data = df[df['maze_name'] == maze_name]
        
        axes[i, 0].bar(maze_data['strategy'], maze_data['avg_time_ms'])
        axes[i, 0].set_title(f'{maze_name}\nВремя (мс)')
        axes[i, 0].tick_params(axis='x', rotation=45, labelsize=8)
        
        axes[i, 1].bar(maze_data['strategy'], maze_data['avg_visited_cells'])
        axes[i, 1].set_title('Посещено клеток')
        axes[i, 1].tick_params(axis='x', rotation=45, labelsize=8)
        
        bars = axes[i, 2].bar(maze_data['strategy'], maze_data['avg_path_length'])
        axes[i, 2].set_title('Длина пути')
        axes[i, 2].tick_params(axis='x', rotation=45, labelsize=8)
        
        for j, (bar, found) in enumerate(zip(bars, maze_data['path_found'])):
            if not found:
                bar.set_color('red')
    
    plt.tight_layout()
    plt.savefig('maze_detailed_analysis.png', dpi=150, bbox_inches='tight')
    plt.show()
    print("Детальные графики сохранены: maze_detailed_analysis.png")


def print_analysis(csv_filename="experiment_results.csv"):
    """вывод анализа эффективности алгоритмов"""
    try:
        import pandas as pd
    except ImportError:
        return
    
    if not os.path.exists(csv_filename):
        return
    
    df = pd.read_csv(csv_filename, sep=';', encoding='utf-8-sig')
    
    print("DEBUG: Колонки в файле:", list(df.columns))
    if 'strategy' not in df.columns:
        for col in df.columns:
            if 'strategy' in col.lower() or 'algo' in col.lower():
                df.rename(columns={col: 'strategy'}, inplace=True)
                break

    if 'strategy' not in df.columns:
        print("Ошибка: колонка 'strategy' не найдена в CSV")
        return
    

    print("АНАЛИЗ ЭФФЕКТИВНОСТИ АЛГОРИТМОВ")
    

    print("\nОбщая статистика по алгоритмам:")
    print(f"{'Алгоритм':<25} {'Время(мс)':<12} {'Посещено':<12} {'Длина пути':<12} {'Найден %':<10}")
    
    for strategy in df['strategy'].unique():
        data = df[df['strategy'] == strategy]
        avg_time = data['avg_time_ms'].mean()
        avg_visited = data['avg_visited_cells'].mean()
        avg_length = data['avg_path_length'].mean()
        found_rate = data['path_found'].mean() * 100
        print(f"{strategy:<25} {avg_time:<12.2f} {avg_visited:<12.0f} {avg_length:<12.1f} {found_rate:<10.1f}%")
    

    print("АНАЛИЗ ПО ТИПАМ ЛАБИРИНТОВ")
    
    for maze_name in df['maze_name'].unique():
        maze_data = df[df['maze_name'] == maze_name]
        
        print(f"\nЛабиринт: {maze_name}")
        
        if 'tiny' in maze_name:
            maze_type = "Маленький (10x10)"
        elif 'small' in maze_name:
            maze_type = "Небольшой (20x20)"
        elif 'medium' in maze_name:
            maze_type = "Средний (30x30-40x40)"
        elif 'large' in maze_name:
            maze_type = "Большой (50x50)"
        elif 'no_exit' in maze_name:
            maze_type = "Без выхода"
        else:
            maze_type = "Обычный"
        
        print(f"  Тип: {maze_type}")
        
        best_time = maze_data.loc[maze_data['avg_time_ms'].idxmin()]
        print(f"  Самый быстрый: {best_time['strategy']} ({best_time['avg_time_ms']:.2f} мс)")
        
        best_visited = maze_data.loc[maze_data['avg_visited_cells'].idxmin()]
        print(f"  Самый экономный: {best_visited['strategy']} ({best_visited['avg_visited_cells']:.0f} клеток)")
        
        path_data = maze_data[maze_data['path_found'] == True]
        if not path_data.empty:
            best_path = path_data.loc[path_data['avg_path_length'].idxmin()]
            print(f"  Самый короткий путь: {best_path['strategy']} ({best_path['avg_path_length']:.1f} шагов)")
        else:
            print("  Путь не найден ни одним алгоритмом")
    

    print("ВЫВОДЫ И РЕКОМЕНДАЦИИ")

    
    print("""
1. BFS (Поиск в ширину):
   - Гарантирует кратчайший путь
   - Медленнее на больших лабиринтах
   - Рекомендуется для маленьких лабиринтов (до 20x20)

2. DFS (Поиск в глубину):
   - Самый быстрый по времени
   - Не гарантирует кратчайший путь
   - Рекомендуется когда важна скорость, а не оптимальность

3. A* (A Star):
   - Лучший компромисс между скоростью и оптимальностью
   - Рекомендуется для больших и сложных лабиринтов (40x40+)

4. Дейкстра:
   - Гарантирует оптимальный путь
   - Работает с взвешенными графами
   - Медленнее A* на больших лабиринтах

ИТОГОВЫЕ РЕКОМЕНДАЦИИ:
- Маленькие лабиринты (до 20x20): BFS
- Средние лабиринты (20x20 - 40x40): A*
- Большие лабиринты (40x40+): A* или DFS
- Когда нужен кратчайший путь: BFS или A*
- Когда важна только скорость: DFS
- Лабиринты без выхода: BFS или A* (быстрее обнаруживают)
""")


def run_full_analysis():
    """Запуск полного анализа с построением графиков"""
    if not os.path.exists("experiment_results.csv"):
        print("Результаты не найдены. Запускаем эксперименты...")
        runner = ExperimentRunner(runs_per_experiment=5)
        runner.run_all_experiments()
        runner.save_to_csv()
        runner.print_summary()
        runner.print_conclusions()
    
    plot_experiment_results()
    plot_by_maze()
    print_analysis()



def run_experiments():
    """Запуск экспериментов с построением графиков и анализом"""
    runner = ExperimentRunner(runs_per_experiment=5)
    runner.run_all_experiments()
    runner.save_to_csv()
    runner.print_summary()
    runner.print_conclusions()
    
    run_full_analysis()



if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
        run_experiments()
    else:
        # Интерактивный режим
        sample = """###############
#S            #
# ### ####### #
#   #       # #
### # ### # # #
#   # #   #   #
# ### # ### ###
#     #     E #
###############"""
        
        with open("maze.txt", "w") as f:
            f.write(sample)
        
        builder = TextFileMazeBuilder()
        maze = builder.buildFromFile("maze.txt")
        
        print(f"Лабиринт загружен: {maze.width}x{maze.height}")
        print(f"Старт: ({maze.start.x}, {maze.start.y})")
        print(f"Выход: ({maze.exit.x}, {maze.exit.y})")
        
        game = GameController(maze)
        game.run()