forked from UNN/2026-rff_mp
226 lines
7.3 KiB
Python
226 lines
7.3 KiB
Python
|
|
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()
|