diff --git a/meosyam/docs/data/2/main.py b/meosyam/docs/data/2/main.py index 5f7442c..9e7c12c 100644 --- a/meosyam/docs/data/2/main.py +++ b/meosyam/docs/data/2/main.py @@ -1,9 +1,11 @@ +import os import sys import time +import csv from collections import deque import heapq from dataclasses import dataclass - +from abc import ABC, abstractmethod class Cell: def __init__(self, x, y, is_wall=False, is_start=False, is_exit=False): @@ -75,14 +77,16 @@ class MazeBuilder: return Maze(width, height, cells, start, exit_cell) -class PathFindingStrategy: - def find_path(self, maze, start, exit): - raise NotImplementedError +class PathFindingStrategy(ABC): + @abstractmethod + def find_path(self, maze, start, exit, visit_callback=None): + pass class BFSStrategy(PathFindingStrategy): - def find_path(self, maze, start, exit): + def find_path(self, maze, start, exit, visit_callback=None): if start == exit: + if visit_callback: visit_callback(start) return [start], 1 queue = deque([start]) visited = {start} @@ -90,6 +94,8 @@ class BFSStrategy(PathFindingStrategy): visited_count = 1 while queue: current = queue.popleft() + if visit_callback: + visit_callback(current) if current == exit: path = [] while current: @@ -107,8 +113,9 @@ class BFSStrategy(PathFindingStrategy): class DFSStrategy(PathFindingStrategy): - def find_path(self, maze, start, exit): + def find_path(self, maze, start, exit, visit_callback=None): if start == exit: + if visit_callback: visit_callback(start) return [start], 1 stack = [start] visited = {start} @@ -116,6 +123,8 @@ class DFSStrategy(PathFindingStrategy): visited_count = 1 while stack: current = stack.pop() + if visit_callback: + visit_callback(current) if current == exit: path = [] while current: @@ -137,8 +146,9 @@ class AStarStrategy(PathFindingStrategy): def manhattan(cell, target): return abs(cell.x - target.x) + abs(cell.y - target.y) - def find_path(self, maze, start, exit): + def find_path(self, maze, start, exit, visit_callback=None): if start == exit: + if visit_callback: visit_callback(start) return [start], 1 open_set = [] counter = 0 @@ -154,6 +164,8 @@ class AStarStrategy(PathFindingStrategy): continue visited.add(current) visited_count += 1 + if visit_callback: + visit_callback(current) if current == exit: path = [] while current: @@ -188,69 +200,291 @@ class MazeSolver: def set_strategy(self, strategy): self.strategy = strategy - def solve(self): + def solve(self, visit_callback=None): if self.strategy is None: raise ValueError("Strategy not set") start_time = time.perf_counter() - path, visited = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) + path, visited = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit, visit_callback) end_time = time.perf_counter() time_ms = (end_time - start_time) * 1000 return path, SearchStats(time_ms, visited, len(path) if path else 0) -def print_maze(maze, path=None): - path_set = set(path) if path else set() - for y in range(maze.height): - row = [] - for x in range(maze.width): - cell = maze.get_cell(x, y) - if cell in path_set and not cell.is_start and not cell.is_exit: - row.append('*') - elif cell.is_start: - row.append('S') - elif cell.is_exit: - row.append('E') - elif cell.is_wall: - row.append('#') +class Observer(ABC): + @abstractmethod + def update(self, event_type, data): + pass + + +class ConsoleView(Observer): + def __init__(self, maze, player=None, path=None, show_steps=False): + self.maze = maze + self.player = player + self.path = path or [] + self.show_steps = show_steps + self.visited = set() + self._clear_screen() + + def _clear_screen(self): + os.system('cls' if os.name == 'nt' else 'clear') + + def update(self, event_type, data): + if event_type == 'player_moved': + self.player = data['player'] + self.render() + elif event_type == 'path_found': + self.path = data['path'] + self.visited.clear() + self.render() + elif event_type == 'search_step': + if self.show_steps: + cell = data['cell'] + self.visited.add(cell) + self.render() + elif event_type == 'clear_visited': + self.visited.clear() + self.render() + elif event_type == 'clear': + self._clear_screen() + + def render(self): + self._clear_screen() + player_pos = self.player.current_cell if self.player else None + path_set = set(self.path) if self.path else set() + for y in range(self.maze.height): + row = [] + for x in range(self.maze.width): + cell = self.maze.get_cell(x, y) + if player_pos and cell == player_pos: + row.append('@') + elif cell.is_start: + row.append('S') + elif cell.is_exit: + row.append('E') + elif cell in path_set and not cell.is_start and not cell.is_exit: + row.append('*') + elif cell in self.visited and not cell.is_start and not cell.is_exit and not cell.is_wall: + row.append('.') + elif cell.is_wall: + row.append('#') + else: + row.append(' ') + print(''.join(row)) + if player_pos: + print(f"Player at ({player_pos.x},{player_pos.y})") + if self.path: + print(f"Path length: {len(self.path)}") + print("(Use W/A/S/D to move, U to undo, F to find path, Q to quit)") + + +class Command(ABC): + @abstractmethod + def execute(self): + pass + + @abstractmethod + def undo(self): + pass + + +class MoveCommand(Command): + def __init__(self, player, dx, dy): + self.player = player + self.dx = dx + self.dy = dy + self.previous_cell = None + + def execute(self): + self.previous_cell = self.player.current_cell + nx = self.player.current_cell.x + self.dx + ny = self.player.current_cell.y + self.dy + target = self.player.maze.get_cell(nx, ny) + if target and target.is_passable(): + self.player.move_to(target) + return True + return False + + def undo(self): + if self.previous_cell: + self.player.move_to(self.previous_cell) + return True + return False + + +class Player: + def __init__(self, maze, start_cell): + self.maze = maze + self.current_cell = start_cell + + def move_to(self, cell): + self.current_cell = cell + + + +def run_experiments(): + test_files = ['maze1.txt', 'maze10x10.txt', 'maze20x20.txt', 'maze_empty.txt', 'maze_no_exit.txt'] + strategies = { + 'BFS': BFSStrategy(), + 'DFS': DFSStrategy(), + 'AStar': AStarStrategy() + } + results = [] + runs = 5 + for fname in test_files: + if not os.path.exists(fname): + print(f"File {fname} not found, skipping.") + continue + try: + maze = MazeBuilder.build_from_file(fname) + except Exception as e: + print(f"Error loading {fname}: {e}") + continue + print(f"Testing on {fname} ({maze.width}x{maze.height})") + for name, strategy in strategies.items(): + total_time = 0.0 + total_visited = 0 + total_length = 0 + success = True + for _ in range(runs): + solver = MazeSolver(maze, strategy) + path, stats = solver.solve() + if not path: + success = False + total_time += stats.time_ms + total_visited += stats.visited_cells + total_length += 0 + else: + total_time += stats.time_ms + total_visited += stats.visited_cells + total_length += len(path) + avg_time = total_time / runs + avg_visited = total_visited / runs + avg_length = total_length / runs if success else 0 + results.append({ + 'maze': fname, + 'strategy': name, + 'avg_time_ms': avg_time, + 'avg_visited': avg_visited, + 'avg_path_length': avg_length, + 'path_found': success + }) + print(f" {name}: time={avg_time:.3f}ms, visited={avg_visited:.1f}, length={avg_length:.1f}") + csv_file = 'experiment_results_2-nd-exercise.csv' + with open(csv_file, 'w', newline='') as csvfile: + fieldnames = ['maze', 'strategy', 'avg_time_ms', 'avg_visited', 'avg_path_length', 'path_found'] + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(results) + print(f"Results saved to {csv_file}") + print("\nSummary Table:") + print(f"{'Maze':<15} {'Strategy':<10} {'Time(ms)':<12} {'Visited':<10} {'Length':<10} {'Found'}") + for r in results: + print(f"{r['maze']:<15} {r['strategy']:<10} {r['avg_time_ms']:<12.3f} {r['avg_visited']:<10.1f} {r['avg_path_length']:<10.1f} {r['path_found']}") + + +def manual_mode(maze): + player = Player(maze, maze.start) + view = ConsoleView(maze, player, show_steps=True) + command_history = [] + view.render() + while True: + cmd = input().strip().lower() + if cmd == 'q': + break + elif cmd == 'u': + if command_history: + cmd_obj = command_history.pop() + cmd_obj.undo() + view.update('player_moved', {'player': player}) else: - row.append(' ') - print(''.join(row)) + print("Nothing to undo") + elif cmd == 'f': + print("Finding path from start to exit...") + strategy = BFSStrategy() + solver = MazeSolver(maze, strategy) + path, stats = solver.solve(visit_callback=lambda cell: view.update('search_step', {'cell': cell})) + view.update('clear_visited', {}) + if path: + view.update('path_found', {'path': path}) + print(f"Path found! Length: {len(path)}") + else: + print("No path found.") + elif cmd in ('w', 'a', 's', 'd'): + dx, dy = 0, 0 + if cmd == 'w': + dy = -1 + elif cmd == 's': + dy = 1 + elif cmd == 'a': + dx = -1 + elif cmd == 'd': + dx = 1 + move_cmd = MoveCommand(player, dx, dy) + if move_cmd.execute(): + command_history.append(move_cmd) + view.update('player_moved', {'player': player}) + else: + print("Can't move there") + else: + print("Unknown command") -def main(): - if len(sys.argv) > 1: - filename = sys.argv[1] - else: - filename = 'maze1.txt' - - try: - maze = MazeBuilder.build_from_file(filename) - print(f"Maze loaded ({maze.width}x{maze.height})") - print("Select algorithm: (1) BFS, (2) DFS, (3) A*") - choice = input("Choice: ").strip() +def interactive_menu(): + while True: + print("\n==== Maze Explorer ====") + print("1. Load maze and solve (auto)") + print("2. Manual control") + print("3. Run experiments") + print("4. Quit") + choice = input("Choose option: ").strip() if choice == '1': - strategy = BFSStrategy() + filename = input("Enter maze filename (default maze1.txt): ").strip() + if not filename: + filename = 'maze1.txt' + try: + maze = MazeBuilder.build_from_file(filename) + print("Maze loaded.") + print("Select algorithm: (1) BFS, (2) DFS, (3) A*") + algo = input("Choice: ").strip() + if algo == '1': + strategy = BFSStrategy() + elif algo == '2': + strategy = DFSStrategy() + elif algo == '3': + strategy = AStarStrategy() + else: + print("Invalid, using BFS") + strategy = BFSStrategy() + solver = MazeSolver(maze, strategy) + view = ConsoleView(maze, show_steps=True) + path, stats = solver.solve(visit_callback=lambda cell: view.update('search_step', {'cell': cell})) + view.update('clear_visited', {}) + if path: + view.update('path_found', {'path': path}) + print(f"Path found! Length: {len(path)}, Visited: {stats.visited_cells}, Time: {stats.time_ms:.4f} ms") + else: + print("No path found.") + print(f"Visited: {stats.visited_cells}, Time: {stats.time_ms:.4f} ms") + input("Press Enter to continue...") + except Exception as e: + print(f"Error: {e}") elif choice == '2': - strategy = DFSStrategy() + filename = input("Enter maze filename (default maze1.txt): ").strip() + if not filename: + filename = 'maze1.txt' + try: + maze = MazeBuilder.build_from_file(filename) + manual_mode(maze) + except Exception as e: + print(f"Error: {e}") elif choice == '3': - strategy = AStarStrategy() + run_experiments() + input("Press Enter to continue...") + elif choice == '4': + break else: - print("Invalid, using BFS") - strategy = BFSStrategy() + print("Invalid choice") - solver = MazeSolver(maze, strategy) - path, stats = solver.solve() - if path: - print(f"Path found! Length: {len(path)}") - print(f"Visited cells: {stats.visited_cells}") - print(f"Time: {stats.time_ms:.4f} ms") - print_maze(maze, path) - else: - print("No path found.") - print(f"Visited cells: {stats.visited_cells}") - print(f"Time: {stats.time_ms:.4f} ms") - except Exception as e: - print(f"Error: {e}") if __name__ == '__main__': - main() \ No newline at end of file + interactive_menu() \ No newline at end of file