580 lines
17 KiB
Python
580 lines
17 KiB
Python
|
|
import csv
|
||
|
|
import time
|
||
|
|
import os
|
||
|
|
import matplotlib.pyplot as plt
|
||
|
|
import numpy as np
|
||
|
|
from collections import deque
|
||
|
|
import heapq
|
||
|
|
|
||
|
|
from maze import DATA_PATH
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
class Tile:
|
||
|
|
def __init__(self, x: int, y: int):
|
||
|
|
self._x = x
|
||
|
|
self._y = y
|
||
|
|
self._wall = False
|
||
|
|
self._start = False
|
||
|
|
self._exit = False
|
||
|
|
|
||
|
|
@property
|
||
|
|
def x(self) -> int:
|
||
|
|
return self._x
|
||
|
|
|
||
|
|
@property
|
||
|
|
def y(self) -> int:
|
||
|
|
return self._y
|
||
|
|
|
||
|
|
@property
|
||
|
|
def is_wall(self) -> bool:
|
||
|
|
return self._wall
|
||
|
|
|
||
|
|
@is_wall.setter
|
||
|
|
def is_wall(self, v: bool):
|
||
|
|
self._wall = v
|
||
|
|
|
||
|
|
@property
|
||
|
|
def is_start(self) -> bool:
|
||
|
|
return self._start
|
||
|
|
|
||
|
|
@is_start.setter
|
||
|
|
def is_start(self, v: bool):
|
||
|
|
self._start = v
|
||
|
|
|
||
|
|
@property
|
||
|
|
def is_exit(self) -> bool:
|
||
|
|
return self._exit
|
||
|
|
|
||
|
|
@is_exit.setter
|
||
|
|
def is_exit(self, v: bool):
|
||
|
|
self._exit = v
|
||
|
|
|
||
|
|
def passable(self) -> bool:
|
||
|
|
return not self._wall
|
||
|
|
|
||
|
|
def __hash__(self):
|
||
|
|
return hash((self._x, self._y))
|
||
|
|
|
||
|
|
def __eq__(self, other):
|
||
|
|
if not isinstance(other, Tile):
|
||
|
|
return False
|
||
|
|
return self._x == other._x and self._y == other._y
|
||
|
|
|
||
|
|
|
||
|
|
class Maze:
|
||
|
|
def __init__(self, w: int, h: int):
|
||
|
|
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) -> int:
|
||
|
|
return self._w
|
||
|
|
|
||
|
|
@property
|
||
|
|
def height(self) -> int:
|
||
|
|
return self._h
|
||
|
|
|
||
|
|
@property
|
||
|
|
def start(self):
|
||
|
|
return self._start
|
||
|
|
|
||
|
|
@property
|
||
|
|
def exit(self):
|
||
|
|
return self._exit
|
||
|
|
|
||
|
|
def get_cell(self, x: int, y: int):
|
||
|
|
if 0 <= x < self._w and 0 <= y < self._h:
|
||
|
|
return self._cells[y][x]
|
||
|
|
return None
|
||
|
|
|
||
|
|
def set_cell(self, x: int, y: int, kind: str):
|
||
|
|
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):
|
||
|
|
result = []
|
||
|
|
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():
|
||
|
|
result.append(nb)
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
class TextMazeLoader:
|
||
|
|
def load(self, filename: str):
|
||
|
|
with open(filename, 'r', encoding='utf-8') as f:
|
||
|
|
lines = [line.rstrip('\n') for line in f.readlines()]
|
||
|
|
|
||
|
|
h = len(lines)
|
||
|
|
w = max(len(line) for line in lines) if h else 0
|
||
|
|
|
||
|
|
start_count = 0
|
||
|
|
exit_count = 0
|
||
|
|
maze = Maze(w, h)
|
||
|
|
|
||
|
|
for y, line in enumerate(lines):
|
||
|
|
for x, ch in enumerate(line):
|
||
|
|
if ch == '#':
|
||
|
|
maze.set_cell(x, y, 'wall')
|
||
|
|
elif ch == 'S':
|
||
|
|
maze.set_cell(x, y, 'start')
|
||
|
|
start_count += 1
|
||
|
|
elif ch == 'E':
|
||
|
|
maze.set_cell(x, y, 'exit')
|
||
|
|
exit_count += 1
|
||
|
|
else:
|
||
|
|
maze.set_cell(x, y, 'path')
|
||
|
|
|
||
|
|
if start_count != 1 or exit_count != 1:
|
||
|
|
raise ValueError(f"Maze must have one S and one E. Found: S={start_count}, E={exit_count}")
|
||
|
|
|
||
|
|
return maze
|
||
|
|
|
||
|
|
|
||
|
|
class BFS:
|
||
|
|
def __init__(self):
|
||
|
|
self._visited = 0
|
||
|
|
|
||
|
|
def find(self, maze, start, goal):
|
||
|
|
from collections import deque
|
||
|
|
queue = deque([start])
|
||
|
|
parent = {start: None}
|
||
|
|
visited = {start}
|
||
|
|
|
||
|
|
while queue:
|
||
|
|
current = queue.popleft()
|
||
|
|
|
||
|
|
if current == goal:
|
||
|
|
self._visited = len(visited)
|
||
|
|
return self._reconstruct(parent, start, goal)
|
||
|
|
|
||
|
|
for neighbor in maze.neighbours(current):
|
||
|
|
if neighbor not in visited:
|
||
|
|
visited.add(neighbor)
|
||
|
|
parent[neighbor] = current
|
||
|
|
queue.append(neighbor)
|
||
|
|
|
||
|
|
self._visited = len(visited)
|
||
|
|
return []
|
||
|
|
|
||
|
|
def _reconstruct(self, parent, start, goal):
|
||
|
|
path = []
|
||
|
|
current = goal
|
||
|
|
while current is not None:
|
||
|
|
path.append(current)
|
||
|
|
current = parent.get(current)
|
||
|
|
path.reverse()
|
||
|
|
return path if path and path[0] == start else []
|
||
|
|
|
||
|
|
@property
|
||
|
|
def visited_count(self):
|
||
|
|
return self._visited
|
||
|
|
|
||
|
|
|
||
|
|
class DFS:
|
||
|
|
def __init__(self):
|
||
|
|
self._visited = 0
|
||
|
|
|
||
|
|
def find(self, maze, start, goal):
|
||
|
|
stack = [start]
|
||
|
|
parent = {start: None}
|
||
|
|
visited = {start}
|
||
|
|
|
||
|
|
while stack:
|
||
|
|
current = stack.pop()
|
||
|
|
|
||
|
|
if current == goal:
|
||
|
|
self._visited = len(visited)
|
||
|
|
return self._reconstruct(parent, start, goal)
|
||
|
|
|
||
|
|
for neighbor in maze.neighbours(current):
|
||
|
|
if neighbor not in visited:
|
||
|
|
visited.add(neighbor)
|
||
|
|
parent[neighbor] = current
|
||
|
|
stack.append(neighbor)
|
||
|
|
|
||
|
|
self._visited = len(visited)
|
||
|
|
return []
|
||
|
|
|
||
|
|
def _reconstruct(self, parent, start, goal):
|
||
|
|
path = []
|
||
|
|
current = goal
|
||
|
|
while current is not None:
|
||
|
|
path.append(current)
|
||
|
|
current = parent.get(current)
|
||
|
|
path.reverse()
|
||
|
|
return path if path and path[0] == start else []
|
||
|
|
|
||
|
|
@property
|
||
|
|
def visited_count(self):
|
||
|
|
return self._visited
|
||
|
|
|
||
|
|
|
||
|
|
class AStar:
|
||
|
|
def __init__(self):
|
||
|
|
self._visited = 0
|
||
|
|
|
||
|
|
def _heuristic(self, cell, goal):
|
||
|
|
return abs(cell.x - goal.x) + abs(cell.y - goal.y)
|
||
|
|
|
||
|
|
def find(self, maze, start, goal):
|
||
|
|
import heapq
|
||
|
|
heap = []
|
||
|
|
counter = 0
|
||
|
|
start_f = self._heuristic(start, goal)
|
||
|
|
heapq.heappush(heap, (start_f, counter, start))
|
||
|
|
counter += 1
|
||
|
|
|
||
|
|
parent = {}
|
||
|
|
g_score = {start: 0}
|
||
|
|
f_score = {start: start_f}
|
||
|
|
visited = set()
|
||
|
|
|
||
|
|
while heap:
|
||
|
|
current_f, _, current = heapq.heappop(heap)
|
||
|
|
visited.add(current)
|
||
|
|
|
||
|
|
if current == goal:
|
||
|
|
self._visited = len(visited)
|
||
|
|
return self._reconstruct(parent, start, goal)
|
||
|
|
|
||
|
|
if current_f > f_score.get(current, float('inf')):
|
||
|
|
continue
|
||
|
|
|
||
|
|
for neighbor in maze.neighbours(current):
|
||
|
|
tentative_g = g_score[current] + 1
|
||
|
|
|
||
|
|
if tentative_g < g_score.get(neighbor, float('inf')):
|
||
|
|
parent[neighbor] = current
|
||
|
|
g_score[neighbor] = tentative_g
|
||
|
|
new_f = tentative_g + self._heuristic(neighbor, goal)
|
||
|
|
f_score[neighbor] = new_f
|
||
|
|
heapq.heappush(heap, (new_f, counter, neighbor))
|
||
|
|
counter += 1
|
||
|
|
|
||
|
|
self._visited = len(visited)
|
||
|
|
return []
|
||
|
|
|
||
|
|
def _reconstruct(self, parent, start, goal):
|
||
|
|
path = []
|
||
|
|
current = goal
|
||
|
|
while current is not None:
|
||
|
|
path.append(current)
|
||
|
|
current = parent.get(current)
|
||
|
|
path.reverse()
|
||
|
|
return path if path and path[0] == start else []
|
||
|
|
|
||
|
|
@property
|
||
|
|
def visited_count(self):
|
||
|
|
return self._visited
|
||
|
|
|
||
|
|
|
||
|
|
class MazeSolver:
|
||
|
|
def __init__(self, maze):
|
||
|
|
self._maze = maze
|
||
|
|
self._algorithm = None
|
||
|
|
|
||
|
|
def set_algorithm(self, algorithm):
|
||
|
|
self._algorithm = algorithm
|
||
|
|
|
||
|
|
def solve(self):
|
||
|
|
if not self._algorithm:
|
||
|
|
raise ValueError("Algorithm not set")
|
||
|
|
|
||
|
|
start_time = time.perf_counter()
|
||
|
|
path = self._algorithm.find(self._maze, self._maze.start, self._maze.exit)
|
||
|
|
end_time = time.perf_counter()
|
||
|
|
|
||
|
|
elapsed_ms = (end_time - start_time) * 1000
|
||
|
|
|
||
|
|
return {
|
||
|
|
'time_ms': elapsed_ms,
|
||
|
|
'visited': self._algorithm.visited_count,
|
||
|
|
'path_length': len(path),
|
||
|
|
'path': path
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
DATA_PATH = r"C:\Users\Kirill\2026-rff_mp\fomichevks\docs\data"
|
||
|
|
|
||
|
|
|
||
|
|
class ExperimentRunner:
|
||
|
|
def __init__(self):
|
||
|
|
self.algorithms = {
|
||
|
|
"BFS": BFS(),
|
||
|
|
"DFS": DFS(),
|
||
|
|
"A*": AStar()
|
||
|
|
}
|
||
|
|
self.loader = TextMazeLoader()
|
||
|
|
|
||
|
|
def run_benchmark(self, maze_file: str, algorithm: str, runs: int = 5):
|
||
|
|
try:
|
||
|
|
maze = self.loader.load(maze_file)
|
||
|
|
except Exception as e:
|
||
|
|
return None
|
||
|
|
|
||
|
|
total_time = 0.0
|
||
|
|
total_visited = 0
|
||
|
|
total_length = 0
|
||
|
|
successes = 0
|
||
|
|
|
||
|
|
for _ in range(runs):
|
||
|
|
solver = MazeSolver(maze)
|
||
|
|
solver.set_algorithm(self.algorithms[algorithm])
|
||
|
|
result = solver.solve()
|
||
|
|
|
||
|
|
if result and result['path_length'] > 0:
|
||
|
|
total_time += result['time_ms']
|
||
|
|
total_visited += result['visited']
|
||
|
|
total_length += result['path_length']
|
||
|
|
successes += 1
|
||
|
|
|
||
|
|
if successes == 0:
|
||
|
|
return None
|
||
|
|
|
||
|
|
return {
|
||
|
|
'time_ms': total_time / successes,
|
||
|
|
'visited_cells': total_visited / successes,
|
||
|
|
'path_length': total_length / successes,
|
||
|
|
'success_rate': successes / runs
|
||
|
|
}
|
||
|
|
|
||
|
|
def run_all_experiments(self, runs: int = 5):
|
||
|
|
mazes_list = [
|
||
|
|
(os.path.join(DATA_PATH, "small.txt"), "Small (10x10)"),
|
||
|
|
(os.path.join(DATA_PATH, "medium.txt"), "Medium (50x50)"),
|
||
|
|
(os.path.join(DATA_PATH, "large.txt"), "Large (100x100)"),
|
||
|
|
(os.path.join(DATA_PATH, "empty.txt"), "Empty"),
|
||
|
|
(os.path.join(DATA_PATH, "no_exit.txt"), "No exit")
|
||
|
|
]
|
||
|
|
|
||
|
|
results = []
|
||
|
|
|
||
|
|
|
||
|
|
print("running experiments")
|
||
|
|
|
||
|
|
print(f"Data path: {DATA_PATH}")
|
||
|
|
|
||
|
|
|
||
|
|
for maze_file, maze_name in mazes_list:
|
||
|
|
if not os.path.exists(maze_file):
|
||
|
|
print(f"\n[warn] File not found: {maze_file}")
|
||
|
|
continue
|
||
|
|
|
||
|
|
print(f"\nTesting: {maze_name}")
|
||
|
|
|
||
|
|
for algo_name in self.algorithms.keys():
|
||
|
|
stats = self.run_benchmark(maze_file, algo_name, runs)
|
||
|
|
|
||
|
|
if stats:
|
||
|
|
print(
|
||
|
|
f" {algo_name}: time={stats['time_ms']:.3f}ms, visited={stats['visited_cells']:.0f}, length={stats['path_length']:.0f}")
|
||
|
|
results.append({
|
||
|
|
'maze': maze_name,
|
||
|
|
'strategy': algo_name,
|
||
|
|
'time_ms': stats['time_ms'],
|
||
|
|
'visited_cells': stats['visited_cells'],
|
||
|
|
'path_length': stats['path_length'],
|
||
|
|
'success_rate': stats['success_rate']
|
||
|
|
})
|
||
|
|
else:
|
||
|
|
print(f" {algo_name}: no path found")
|
||
|
|
results.append({
|
||
|
|
'maze': maze_name,
|
||
|
|
'strategy': algo_name,
|
||
|
|
'time_ms': -1,
|
||
|
|
'visited_cells': -1,
|
||
|
|
'path_length': -1,
|
||
|
|
'success_rate': 0
|
||
|
|
})
|
||
|
|
|
||
|
|
return results
|
||
|
|
|
||
|
|
|
||
|
|
def create_visualizations(results):
|
||
|
|
valid_results = [r for r in results if r['time_ms'] > 0]
|
||
|
|
if not valid_results:
|
||
|
|
print("no valid results for visualization")
|
||
|
|
return
|
||
|
|
|
||
|
|
mazes = sorted(set(r['maze'] for r in valid_results))
|
||
|
|
algorithms = ['BFS', 'DFS', 'A*']
|
||
|
|
|
||
|
|
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
||
|
|
fig.suptitle('pathfinding algorithms comparison', fontsize=14)
|
||
|
|
|
||
|
|
x = np.arange(len(mazes))
|
||
|
|
width = 0.25
|
||
|
|
|
||
|
|
# Time chart
|
||
|
|
for i, algo in enumerate(algorithms):
|
||
|
|
times = []
|
||
|
|
for maze in mazes:
|
||
|
|
val = next((r['time_ms'] for r in valid_results
|
||
|
|
if r['maze'] == maze and r['strategy'] == algo), 0)
|
||
|
|
times.append(val)
|
||
|
|
bars = axes[0].bar(x + i * width, times, width, label=algo, alpha=0.8)
|
||
|
|
for bar, val in zip(bars, times):
|
||
|
|
if val > 0:
|
||
|
|
axes[0].text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.5,
|
||
|
|
f'{val:.1f}', ha='center', va='bottom', fontsize=7)
|
||
|
|
|
||
|
|
axes[0].set_title('execution Time (ms)')
|
||
|
|
axes[0].set_ylabel('time (ms)')
|
||
|
|
axes[0].set_xticks(x + width)
|
||
|
|
axes[0].set_xticklabels(mazes, rotation=45, ha='right', fontsize=8)
|
||
|
|
axes[0].legend()
|
||
|
|
axes[0].grid(alpha=0.3, axis='y')
|
||
|
|
|
||
|
|
# Visited cells chart
|
||
|
|
for i, algo in enumerate(algorithms):
|
||
|
|
visited = []
|
||
|
|
for maze in mazes:
|
||
|
|
val = next((r['visited_cells'] for r in valid_results
|
||
|
|
if r['maze'] == maze and r['strategy'] == algo), 0)
|
||
|
|
visited.append(val)
|
||
|
|
bars = axes[1].bar(x + i * width, visited, width, label=algo, alpha=0.8)
|
||
|
|
for bar, val in zip(bars, visited):
|
||
|
|
if val > 0:
|
||
|
|
axes[1].text(bar.get_x() + bar.get_width() / 2, bar.get_height(),
|
||
|
|
f'{val:.0f}', ha='center', va='bottom', fontsize=7)
|
||
|
|
|
||
|
|
axes[1].set_title('visited Cells')
|
||
|
|
axes[1].set_ylabel('count')
|
||
|
|
axes[1].set_xticks(x + width)
|
||
|
|
axes[1].set_xticklabels(mazes, rotation=45, ha='right', fontsize=8)
|
||
|
|
axes[1].legend()
|
||
|
|
axes[1].grid(alpha=0.3, axis='y')
|
||
|
|
|
||
|
|
# Path length chart
|
||
|
|
for i, algo in enumerate(algorithms):
|
||
|
|
lengths = []
|
||
|
|
for maze in mazes:
|
||
|
|
val = next((r['path_length'] for r in valid_results
|
||
|
|
if r['maze'] == maze and r['strategy'] == algo), 0)
|
||
|
|
lengths.append(val)
|
||
|
|
bars = axes[2].bar(x + i * width, lengths, width, label=algo, alpha=0.8)
|
||
|
|
for bar, val in zip(bars, lengths):
|
||
|
|
if val > 0:
|
||
|
|
axes[2].text(bar.get_x() + bar.get_width() / 2, bar.get_height(),
|
||
|
|
f'{val:.0f}', ha='center', va='bottom', fontsize=7)
|
||
|
|
|
||
|
|
axes[2].set_title('path Length')
|
||
|
|
axes[2].set_ylabel('steps')
|
||
|
|
axes[2].set_xticks(x + width)
|
||
|
|
axes[2].set_xticklabels(mazes, rotation=45, ha='right', fontsize=8)
|
||
|
|
axes[2].legend()
|
||
|
|
axes[2].grid(alpha=0.3, axis='y')
|
||
|
|
|
||
|
|
plt.tight_layout()
|
||
|
|
|
||
|
|
output_path = os.path.join(DATA_PATH, 'experiment_results.png')
|
||
|
|
plt.savefig(output_path, dpi=150, bbox_inches='tight')
|
||
|
|
print(f"\nPlot saved to: {output_path}")
|
||
|
|
plt.show()
|
||
|
|
|
||
|
|
|
||
|
|
def save_results_to_csv(results, filename='experiment_results.csv'):
|
||
|
|
if not results:
|
||
|
|
return
|
||
|
|
|
||
|
|
filepath = os.path.join(DATA_PATH, filename)
|
||
|
|
with open(filepath, 'w', newline='', encoding='utf-8') as f:
|
||
|
|
fieldnames = ['maze', 'strategy', 'time_ms', 'visited_cells', 'path_length', 'success_rate']
|
||
|
|
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||
|
|
writer.writeheader()
|
||
|
|
writer.writerows(results)
|
||
|
|
|
||
|
|
print(f"Results saved to: {filepath}")
|
||
|
|
|
||
|
|
|
||
|
|
def analyze_efficiency(results):
|
||
|
|
valid_results = [r for r in results if r['time_ms'] > 0]
|
||
|
|
if not valid_results:
|
||
|
|
print("no valid results for analysis")
|
||
|
|
return
|
||
|
|
|
||
|
|
algo_stats = {}
|
||
|
|
for algo in ['BFS', 'DFS', 'A*']:
|
||
|
|
algo_data = [r for r in valid_results if r['strategy'] == algo]
|
||
|
|
if algo_data:
|
||
|
|
algo_stats[algo] = {
|
||
|
|
'avg_time': sum(r['time_ms'] for r in algo_data) / len(algo_data),
|
||
|
|
'avg_visited': sum(r['visited_cells'] for r in algo_data) / len(algo_data),
|
||
|
|
'avg_length': sum(r['path_length'] for r in algo_data) / len(algo_data)
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
print("average values across all mazes")
|
||
|
|
print(f"{'Algorithm':<12} {'Time (ms)':<15} {'Visited':<15} {'Path length':<15}")
|
||
|
|
|
||
|
|
for algo, stats in algo_stats.items():
|
||
|
|
print(f"{algo:<12} {stats['avg_time']:<15.3f} {stats['avg_visited']:<15.1f} {stats['avg_length']:<15.1f}")
|
||
|
|
|
||
|
|
fastest = min(algo_stats.items(), key=lambda x: x[1]['avg_time'])
|
||
|
|
optimal = min(algo_stats.items(), key=lambda x: x[1]['avg_length'])
|
||
|
|
efficient = min(algo_stats.items(), key=lambda x: x[1]['avg_visited'])
|
||
|
|
|
||
|
|
print("conclusions:")
|
||
|
|
print(f" fastest algorithm: {fastest[0]} ({fastest[1]['avg_time']:.3f} ms avg)")
|
||
|
|
print(f" optimal path: {optimal[0]} ({optimal[1]['avg_length']:.1f} steps avg)")
|
||
|
|
print(f" most efficient (fewest visits): {efficient[0]} ({efficient[1]['avg_visited']:.0f} cells avg)")
|
||
|
|
print("=" * 70)
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
|
||
|
|
|
||
|
|
if not os.path.exists(DATA_PATH):
|
||
|
|
print(f"\nerr: directory not found: {DATA_PATH}")
|
||
|
|
print("please create the directory and place maze files there.")
|
||
|
|
print("\nexpected structure:")
|
||
|
|
print(f" {DATA_PATH}/")
|
||
|
|
print(" ├── small.txt")
|
||
|
|
print(" ├── medium.txt")
|
||
|
|
print(" ├── large.txt")
|
||
|
|
print(" ├── empty.txt")
|
||
|
|
print(" └── no_exit.txt")
|
||
|
|
return
|
||
|
|
|
||
|
|
runner = ExperimentRunner()
|
||
|
|
results = runner.run_all_experiments(runs=5)
|
||
|
|
|
||
|
|
if not results:
|
||
|
|
print("\nNo results. Check if maze files exist in:", DATA_PATH)
|
||
|
|
return
|
||
|
|
|
||
|
|
save_results_to_csv(results)
|
||
|
|
analyze_efficiency(results)
|
||
|
|
create_visualizations(results)
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|