import sys import csv from collections import deque import heapq import time import matplotlib.pyplot as plt import numpy as np class Tile: def __init__(self, x, y): self._x = x self._y = y self._wall = False self._start = False self._exit = False @property def x(self): return self._x @property def y(self): return self._y @property def is_wall(self): return self._wall @is_wall.setter def is_wall(self, v): self._wall = v @property def is_start(self): return self._start @is_start.setter def is_start(self, v): self._start = v @property def is_exit(self): return self._exit @is_exit.setter def is_exit(self, v): self._exit = v def passable(self): return not self._wall class Maze: def __init__(self, w, h): 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): return self._w @property def height(self): return self._h @property def start(self): return self._start @property def exit(self): return self._exit def get_cell(self, x, y): if 0 <= x < self._w and 0 <= y < self._h: return self._cells[y][x] return None def set_cell(self, x, y, kind): 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): res = [] 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(): res.append(nb) return res class MazeLoader: def load(self, fname): raise NotImplementedError class TextMazeLoader(MazeLoader): def load(self, fname): with open(fname, 'r') as f: lines = [ln.rstrip('\n') for ln in f.readlines()] h = len(lines) w = max(len(ln) for ln in lines) if h else 0 cntS = 0 cntE = 0 m = Maze(w, h) for y, ln in enumerate(lines): for x, ch in enumerate(ln): if ch == '#': m.set_cell(x, y, 'wall') elif ch == 'S': m.set_cell(x, y, 'start') cntS += 1 elif ch == 'E': m.set_cell(x, y, 'exit') cntE += 1 else: m.set_cell(x, y, 'path') if cntS != 1 or cntE != 1: raise ValueError(f"Bad maze: S={cntS}, E={cntE}") return m class PathFinder: def find(self, maze, start, goal): raise NotImplementedError def _reconstruct(self, parent, start, goal): path = [] cur = goal while cur: path.append(cur) cur = parent.get(cur) path.reverse() return path def visited_count(self): return getattr(self, '_vis', 0) class BFS(PathFinder): def find(self, maze, start, goal): q = deque([start]) parent = {start: None} visited = {start} while q: cur = q.popleft() if cur == goal: self._vis = len(visited) return self._reconstruct(parent, start, goal) for nb in maze.neighbours(cur): if nb not in visited: visited.add(nb) parent[nb] = cur q.append(nb) self._vis = len(visited) return [] class DFS(PathFinder): def find(self, maze, start, goal): stack = [start] parent = {start: None} visited = {start} while stack: cur = stack.pop() if cur == goal: self._vis = len(visited) return self._reconstruct(parent, start, goal) for nb in maze.neighbours(cur): if nb not in visited: visited.add(nb) parent[nb] = cur stack.append(nb) self._vis = len(visited) return [] class AStar(PathFinder): def _h(self, cell, goal): return abs(cell.x - goal.x) + abs(cell.y - goal.y) def find(self, maze, start, goal): heap = [] idx = 0 start_f = self._h(start, goal) heapq.heappush(heap, (start_f, idx, start)) idx += 1 parent = {} g = {start: 0} f = {start: start_f} visited = set() while heap: cur_f, _, cur = heapq.heappop(heap) visited.add(cur) if cur == goal: self._vis = len(visited) return self._reconstruct(parent, start, goal) if cur_f > f.get(cur, float('inf')): continue for nb in maze.neighbours(cur): new_g = g[cur] + 1 if new_g < g.get(nb, float('inf')): parent[nb] = cur g[nb] = new_g new_f = new_g + self._h(nb, goal) f[nb] = new_f heapq.heappush(heap, (new_f, idx, nb)) idx += 1 self._vis = len(visited) return [] class Solver: def __init__(self, maze): self._maze = maze self._algo = None def set_algo(self, algo): self._algo = algo def run(self): if not self._algo: return None t0 = time.perf_counter() path = self._algo.find(self._maze, self._maze.start, self._maze.exit) t1 = time.perf_counter() return { 'time_ms': (t1 - t0) * 1000, 'visited': self._algo.visited_count(), 'path_len': len(path) } def benchmark(maze_file, algorithm, runs=5): loader = TextMazeLoader() maze = loader.load(maze_file) total_t = 0.0 total_v = 0 total_l = 0 for _ in range(runs): s = Solver(maze) s.set_algo(algorithm) stats = s.run() if stats: total_t += stats['time_ms'] total_v += stats['visited'] total_l += stats['path_len'] return { 'time_ms': total_t / runs, 'visited_cells': total_v / runs, 'path_length': total_l / runs } def create_plots(results): mazes = sorted(set(r['maze'] for r in results)) algos = ['BFS', 'DFS', 'AStar'] fig, axes = plt.subplots(1, 3, figsize=(15,5)) x = np.arange(len(mazes)) width = 0.25 for i, algo in enumerate(algos): times = [] for m in mazes: val = next((r['time_ms'] for r in results if r['maze'] == m and r['strategy'] == algo), 0) times.append(val) axes[0].bar(x + i*width, times, width, label=algo) axes[0].set_title('Execution time (ms)') axes[0].set_xticks(x + width) axes[0].set_xticklabels(mazes, rotation=45, ha='right') axes[0].legend() axes[0].grid(alpha=0.3) for i, algo in enumerate(algos): visited = [] for m in mazes: val = next((r['visited_cells'] for r in results if r['maze'] == m and r['strategy'] == algo), 0) visited.append(val) axes[1].bar(x + i*width, visited, width, label=algo) axes[1].set_title('Visited cells') axes[1].set_xticks(x + width) axes[1].set_xticklabels(mazes, rotation=45, ha='right') axes[1].legend() axes[1].grid(alpha=0.3) for i, algo in enumerate(algos): lengths = [] for m in mazes: val = next((r['path_length'] for r in results if r['maze'] == m and r['strategy'] == algo), 0) lengths.append(val) axes[2].bar(x + i*width, lengths, width, label=algo) axes[2].set_title('Path length') axes[2].set_xticks(x + width) axes[2].set_xticklabels(mazes, rotation=45, ha='right') axes[2].legend() axes[2].grid(alpha=0.3) plt.tight_layout() plt.savefig('performance_comparison_2-nd-exercise.png', dpi=150, bbox_inches='tight') plt.show() if __name__ == "__main__": test_mazes = [ ("maze1.txt", "Small 10x6"), ("maze10x10.txt", "Medium 10x10"), ("maze20x20.txt", "Large 20x20"), ("maze_empty.txt", "Empty 15x15"), ("maze_no_exit.txt", "No exit 10x10") ] algorithms = [ ("BFS", BFS()), ("DFS", DFS()), ("AStar", AStar()) ] all_results = [] for fname, label in test_mazes: print(f"Testing {label}...") for name, algo in algorithms: try: stat = benchmark(fname, algo, runs=3) all_results.append({ 'maze': label, 'strategy': name, 'time_ms': stat['time_ms'], 'visited_cells': stat['visited_cells'], 'path_length': stat['path_length'] }) print(f" {name}: time={stat['time_ms']:.3f}ms, visited={stat['visited_cells']:.0f}, length={stat['path_length']:.0f}") except Exception as e: print(f" {name}: ERROR - {e}") all_results.append({ 'maze': label, 'strategy': name, 'time_ms': -1, 'visited_cells': -1, 'path_length': -1 }) good = [r for r in all_results if r['time_ms'] >= 0] with open('experiment_results_2-nd-exercise.csv', 'w', newline='', encoding='utf-8') as f: writer = csv.DictWriter(f, fieldnames=['maze', 'strategy', 'time_ms', 'visited_cells', 'path_length']) writer.writeheader() writer.writerows(good) if good: create_plots(good) print("\nResults saved to experiment_results_2-nd-exercise.csv") print("Plot saved to performance_comparison_2-nd-exercise.png")