from pathlib import Path from statistics import mean import csv import random import matplotlib.pyplot as plt from core.cell import Cell from core.maze import Maze from solver.maze_solver import MazeSolver from strategies.astar_strategy import AStarStrategy from strategies.bfs_strategy import BFSStrategy from strategies.dfs_strategy import DFSStrategy from strategies.dijkstra_strategy import DijkstraStrategy BASE_DIR = Path(__file__).resolve().parent OUT_DIR = BASE_DIR / "experiment_results" def build_maze_from_symbols(lines): height = len(lines) width = max(len(line) for line in lines) cells = [] start = None exit_cell = None for y, line in enumerate(lines): row = [] for x in range(width): ch = line[x] if x < len(line) else "#" if ch == "#": cell = Cell(x, y, isWall=True) elif ch == "S": cell = Cell(x, y, isWall=False, isStart=True) start = cell elif ch == "E": cell = Cell(x, y, isWall=False, isExit=True) exit_cell = cell elif ch == " " or ch == ".": cell = Cell(x, y, isWall=False) elif ch.isdigit(): cell = Cell(x, y, isWall=False, weight=int(ch)) else: raise ValueError(f"Unknown symbol '{ch}' at {x},{y}") row.append(cell) cells.append(row) return Maze(cells, width, height, start, exit_cell) def generate_empty_maze(width, height): lines = [" " * width for _ in range(height)] lines = [list(row) for row in lines] lines[1][1] = "S" lines[height - 2][width - 2] = "E" return build_maze_from_symbols(["".join(row) for row in lines]) def generate_simple_maze(width, height): grid = [["#" for _ in range(width)] for _ in range(height)] for x in range(1, width - 1): grid[1][x] = " " for y in range(1, height - 1): grid[y][width - 2] = " " grid[1][1] = "S" grid[height - 2][width - 2] = "E" return build_maze_from_symbols(["".join(row) for row in grid]) def generate_branching_maze(width, height, seed=42, wall_density=0.30): rng = random.Random(seed) grid = [["#" for _ in range(width)] for _ in range(height)] x, y = 1, 1 grid[y][x] = "S" while (x, y) != (width - 2, height - 2): candidates = [] for dx, dy in [(1, 0), (0, 1)]: nx, ny = x + dx, y + dy if 1 <= nx < width - 1 and 1 <= ny < height - 1: candidates.append((nx, ny)) if not candidates: break x, y = rng.choice(candidates) grid[y][x] = " " grid[height - 2][width - 2] = "E" # carve extra corridors and dead ends for yy in range(1, height - 1): for xx in range(1, width - 1): if grid[yy][xx] == "#" and rng.random() > wall_density: grid[yy][xx] = " " grid[1][1] = "S" grid[height - 2][width - 2] = "E" return build_maze_from_symbols(["".join(row) for row in grid]) def generate_no_path_maze(width, height): grid = [[" " for _ in range(width)] for _ in range(height)] for x in range(width): grid[height // 2][x] = "#" grid[1][1] = "S" grid[height - 2][width - 2] = "E" return build_maze_from_symbols(["".join(row) for row in grid]) def generate_weighted_maze(width, height, seed=123): rng = random.Random(seed) grid = [[" " for _ in range(width)] for _ in range(height)] for y in range(height): for x in range(width): r = rng.random() if r < 0.12: grid[y][x] = "#" elif r < 0.25: grid[y][x] = "3" elif r < 0.40: grid[y][x] = "2" else: grid[y][x] = "1" # ensure path-ish for x in range(width): grid[1][x] = "1" for y in range(1, height): grid[y][width - 2] = "1" grid[1][1] = "S" grid[height - 2][width - 2] = "E" return build_maze_from_symbols(["".join(row) for row in grid]) def bench_one_maze(maze_name, maze, strategies, repeats=5): summary_rows = [] raw_rows = [] for strategy_name, strategy_factory in strategies: times, visiteds, lengths = [], [], [] for run in range(1, repeats + 1): solver = MazeSolver(maze) solver.setStrategy(strategy_factory()) stats = solver.solve() raw_rows.append([maze_name, strategy_name, run, f"{stats.timeMs:.6f}", stats.visitedCells, stats.pathLength]) times.append(stats.timeMs) visiteds.append(stats.visitedCells) lengths.append(stats.pathLength) summary_rows.append([maze_name, strategy_name, f"{mean(times):.6f}", f"{mean(visiteds):.2f}", f"{mean(lengths):.2f}", repeats]) return summary_rows, raw_rows def save_csv(path, rows): with open(path, "w", newline="", encoding="utf-8") as f: csv.writer(f).writerows(rows) def plot_summary(summary_rows): by_maze = {} for row in summary_rows[1:]: maze_name, strategy, avg_time, avg_visited, avg_len, runs = row by_maze.setdefault(maze_name, []).append((strategy, float(avg_time), float(avg_visited), float(avg_len))) for maze_name, items in by_maze.items(): items.sort(key=lambda t: t[0]) strategies = [i[0] for i in items] x = list(range(len(strategies))) plt.figure(figsize=(8, 4)) plt.bar(x, [i[1] for i in items]) plt.xticks(x, strategies) plt.ylabel("ms") plt.title(f"{maze_name} — avg time") plt.tight_layout() plt.savefig(OUT_DIR / f"{maze_name}_time.png", dpi=150) plt.close() plt.figure(figsize=(8, 4)) plt.bar(x, [i[2] for i in items]) plt.xticks(x, strategies) plt.ylabel("cells") plt.title(f"{maze_name} — visited cells") plt.tight_layout() plt.savefig(OUT_DIR / f"{maze_name}_visited.png", dpi=150) plt.close() plt.figure(figsize=(8, 4)) plt.bar(x, [i[3] for i in items]) plt.xticks(x, strategies) plt.ylabel("cells") plt.title(f"{maze_name} — path length") plt.tight_layout() plt.savefig(OUT_DIR / f"{maze_name}_length.png", dpi=150) plt.close() def main(): OUT_DIR.mkdir(exist_ok=True) strategies = [ ("BFS", BFSStrategy), ("DFS", DFSStrategy), ("A*", AStarStrategy), ("Dijkstra", DijkstraStrategy), ] mazes = [ ("small_10x10", generate_simple_maze(10, 10)), ("medium_50x50", generate_branching_maze(50, 50)), ("large_100x100", generate_branching_maze(100, 100, seed=99, wall_density=0.35)), ("empty_30x30", generate_empty_maze(30, 30)), ("no_path_30x30", generate_no_path_maze(30, 30)), ("weighted_30x30", generate_weighted_maze(30, 30)), ] summary = [["maze", "strategy", "avg_time_ms", "avg_visited_cells", "avg_path_length", "runs"]] raw = [["maze", "strategy", "run", "time_ms", "visited_cells", "path_length"]] for maze_name, maze in mazes: s_rows, r_rows = bench_one_maze(maze_name, maze, strategies, repeats=5) summary.extend(s_rows) raw.extend(r_rows) save_csv(OUT_DIR / "summary.csv", summary) save_csv(OUT_DIR / "raw.csv", raw) plot_summary(summary) print("Saved to", OUT_DIR.resolve()) if __name__ == "__main__": main()