This commit is contained in:
Yaroslav 2026-05-24 19:43:30 +03:00
parent b4b8bf522d
commit a66cac23d8
70 changed files with 1006 additions and 0 deletions

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -0,0 +1,24 @@
# Maze Solver Project
ООП-проект для поиска выхода из лабиринта с паттернами:
- Builder
- Strategy
- Observer
- Command
## Запуск
```bash
python main.py
```
## Эксперименты
```bash
python experiment.py
```
Результаты сохраняются в папку `experiment_results/`.
## Требования
```bash
pip install -r requirements.txt
```

View File

View File

@ -0,0 +1,7 @@
from abc import ABC, abstractmethod
class MazeBuilder(ABC):
@abstractmethod
def buildFromFile(self, filename):
raise NotImplementedError

View File

@ -0,0 +1,52 @@
from core.cell import Cell
from core.maze import Maze
from builders.maze_builder import MazeBuilder
class TextFileMazeBuilder(MazeBuilder):
def buildFromFile(self, filename):
with open(filename, "r", encoding="utf-8") as f:
lines = [line.rstrip("\n") for line in f]
if not lines:
raise ValueError("Maze file is empty")
width = max(len(line) for line in lines)
height = len(lines)
cells = []
startCell = None
exitCell = 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":
if startCell is not None:
raise ValueError("Multiple start cells found")
cell = Cell(x, y, isWall=False, isStart=True)
startCell = cell
elif ch == "E":
if exitCell is not None:
raise ValueError("Multiple exit cells found")
cell = Cell(x, y, isWall=False, isExit=True)
exitCell = cell
elif ch in (" ", "."):
cell = Cell(x, y, isWall=False)
elif ch.isdigit():
cell = Cell(x, y, isWall=False, weight=max(1, int(ch)))
else:
raise ValueError(f"Unsupported symbol '{ch}' at ({x}, {y})")
row.append(cell)
cells.append(row)
if startCell is None:
raise ValueError("Start cell 'S' not found")
if exitCell is None:
raise ValueError("Exit cell 'E' not found")
return Maze(cells, width, height, startCell, exitCell)

View File

View File

@ -0,0 +1,11 @@
from abc import ABC, abstractmethod
class Command(ABC):
@abstractmethod
def execute(self):
raise NotImplementedError
@abstractmethod
def undo(self):
raise NotImplementedError

View File

@ -0,0 +1,37 @@
from commands.command import Command
class MoveCommand(Command):
DIRECTION_TO_DELTA = {
"W": (0, -1),
"A": (-1, 0),
"S": (0, 1),
"D": (1, 0),
}
def __init__(self, player, maze, direction):
self.player = player
self.maze = maze
self.direction = direction.upper()
self.previousCell = None
def execute(self):
if self.direction not in self.DIRECTION_TO_DELTA:
return False
dx, dy = self.DIRECTION_TO_DELTA[self.direction]
current = self.player.currentCell
new_cell = self.maze.getCell(current.x + dx, current.y + dy)
if new_cell is None or not new_cell.isPassable():
return False
self.previousCell = current
self.player.setCell(new_cell)
return True
def undo(self):
if self.previousCell is None:
return False
self.player.setCell(self.previousCell)
return True

View File

@ -0,0 +1,30 @@
from commands.move_command import MoveCommand
class GameController:
def __init__(self, maze, player, view):
self.maze = maze
self.player = player
self.view = view
self.history = []
def move(self, direction):
command = MoveCommand(self.player, self.maze, direction)
if command.execute():
self.history.append(command)
self.view.update({"type": "move", "direction": direction})
self.view.render(self.maze, player_position=self.player.currentCell)
return True
print("Cannot move there")
return False
def undo(self):
if not self.history:
print("Nothing to undo")
return False
command = self.history.pop()
if command.undo():
self.view.update({"type": "undo"})
self.view.render(self.maze, player_position=self.player.currentCell)
return True
return False

View File

View File

@ -0,0 +1,26 @@
from dataclasses import dataclass
@dataclass
class Cell:
x: int
y: int
isWall: bool = False
isStart: bool = False
isExit: bool = False
weight: int = 1
def isPassable(self):
return not self.isWall
def __repr__(self):
parts = [f"Cell({self.x}, {self.y}"]
if self.isWall:
parts.append("WALL")
if self.isStart:
parts.append("START")
if self.isExit:
parts.append("EXIT")
if self.weight != 1:
parts.append(f"w={self.weight}")
return ", ".join(parts) + ")"

View File

@ -0,0 +1,49 @@
class Maze:
def __init__(self, cells, width, height, startCell=None, exitCell=None):
self.cells = cells
self.width = width
self.height = height
self.startCell = startCell
self.exitCell = exitCell
def getCell(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[y][x]
return None
def getNeighbors(self, cell):
neighbors = []
for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)):
nx, ny = cell.x + dx, cell.y + dy
neighbor = self.getCell(nx, ny)
if neighbor is not None and neighbor.isPassable():
neighbors.append(neighbor)
return neighbors
def render_lines(self, player_position=None, path=None):
path_set = {(c.x, c.y) for c in path} if path else set()
player_pos = None if player_position is None else (player_position.x, player_position.y)
lines = []
for y in range(self.height):
row = []
for x in range(self.width):
cell = self.cells[y][x]
if player_pos == (x, y):
row.append("P")
elif cell.isStart:
row.append("S")
elif cell.isExit:
row.append("E")
elif cell.isWall:
row.append("#")
elif (x, y) in path_set:
row.append("*")
elif cell.weight > 1:
row.append(str(cell.weight))
else:
row.append(" ")
lines.append("".join(row))
return lines
def render(self, player_position=None, path=None):
return "\n".join(self.render_lines(player_position=player_position, path=path))

View File

@ -0,0 +1,6 @@
class Player:
def __init__(self, currentCell):
self.currentCell = currentCell
def setCell(self, cell):
self.currentCell = cell

View File

@ -0,0 +1,11 @@
from dataclasses import dataclass, field
@dataclass
class SearchStats:
timeMs: float
visitedCells: int
pathLength: int
path: list = field(default_factory=list)
found: bool = False
algorithm: str = ""

Binary file not shown.

View File

@ -0,0 +1,225 @@
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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,121 @@
maze,strategy,run,time_ms,visited_cells,path_length
small_10x10,BFS,1,0.086300,15,15
small_10x10,BFS,2,0.061100,15,15
small_10x10,BFS,3,0.059300,15,15
small_10x10,BFS,4,0.058400,15,15
small_10x10,BFS,5,0.058500,15,15
small_10x10,DFS,1,0.073400,15,15
small_10x10,DFS,2,0.063500,15,15
small_10x10,DFS,3,0.062500,15,15
small_10x10,DFS,4,0.062700,15,15
small_10x10,DFS,5,0.070900,15,15
small_10x10,A*,1,0.110100,15,15
small_10x10,A*,2,0.089200,15,15
small_10x10,A*,3,0.087800,15,15
small_10x10,A*,4,0.087600,15,15
small_10x10,A*,5,0.087000,15,15
small_10x10,Dijkstra,1,0.290000,15,15
small_10x10,Dijkstra,2,0.083300,15,15
small_10x10,Dijkstra,3,0.091500,15,15
small_10x10,Dijkstra,4,0.081000,15,15
small_10x10,Dijkstra,5,0.080400,15,15
medium_50x50,BFS,1,6.799200,1579,95
medium_50x50,BFS,2,6.960100,1579,95
medium_50x50,BFS,3,6.337000,1579,95
medium_50x50,BFS,4,7.431700,1579,95
medium_50x50,BFS,5,6.517900,1579,95
medium_50x50,DFS,1,6.463000,1277,647
medium_50x50,DFS,2,6.815500,1277,647
medium_50x50,DFS,3,5.816100,1277,647
medium_50x50,DFS,4,6.492400,1277,647
medium_50x50,DFS,5,6.532500,1277,647
medium_50x50,A*,1,6.940500,927,95
medium_50x50,A*,2,7.275400,927,95
medium_50x50,A*,3,7.062500,927,95
medium_50x50,A*,4,7.727600,927,95
medium_50x50,A*,5,7.321000,927,95
medium_50x50,Dijkstra,1,11.483200,1579,95
medium_50x50,Dijkstra,2,11.194200,1579,95
medium_50x50,Dijkstra,3,11.255200,1579,95
medium_50x50,Dijkstra,4,10.512500,1579,95
medium_50x50,Dijkstra,5,10.696400,1579,95
large_100x100,BFS,1,25.623500,5566,195
large_100x100,BFS,2,24.348800,5566,195
large_100x100,BFS,3,25.452600,5566,195
large_100x100,BFS,4,30.516900,5566,195
large_100x100,BFS,5,33.694700,5566,195
large_100x100,DFS,1,19.415200,3543,1531
large_100x100,DFS,2,19.919000,3543,1531
large_100x100,DFS,3,19.104600,3543,1531
large_100x100,DFS,4,20.000600,3543,1531
large_100x100,DFS,5,17.840200,3543,1531
large_100x100,A*,1,7.509300,853,195
large_100x100,A*,2,7.221200,853,195
large_100x100,A*,3,6.486700,853,195
large_100x100,A*,4,6.357600,853,195
large_100x100,A*,5,6.723800,853,195
large_100x100,Dijkstra,1,40.782300,5571,195
large_100x100,Dijkstra,2,41.155000,5571,195
large_100x100,Dijkstra,3,39.456200,5571,195
large_100x100,Dijkstra,4,41.388700,5571,195
large_100x100,Dijkstra,5,40.962500,5571,195
empty_30x30,BFS,1,4.143200,896,55
empty_30x30,BFS,2,3.987000,896,55
empty_30x30,BFS,3,3.777100,896,55
empty_30x30,BFS,4,3.682300,896,55
empty_30x30,BFS,5,3.737900,896,55
empty_30x30,DFS,1,4.024200,842,815
empty_30x30,DFS,2,4.333900,842,815
empty_30x30,DFS,3,5.411000,842,815
empty_30x30,DFS,4,4.677200,842,815
empty_30x30,DFS,5,5.177400,842,815
empty_30x30,A*,1,6.603700,784,55
empty_30x30,A*,2,6.200600,784,55
empty_30x30,A*,3,6.798400,784,55
empty_30x30,A*,4,7.178500,784,55
empty_30x30,A*,5,6.660800,784,55
empty_30x30,Dijkstra,1,6.396000,896,55
empty_30x30,Dijkstra,2,6.275200,896,55
empty_30x30,Dijkstra,3,6.845700,896,55
empty_30x30,Dijkstra,4,6.531200,896,55
empty_30x30,Dijkstra,5,6.783400,896,55
no_path_30x30,BFS,1,2.000100,450,0
no_path_30x30,BFS,2,1.797900,450,0
no_path_30x30,BFS,3,1.796200,450,0
no_path_30x30,BFS,4,1.774100,450,0
no_path_30x30,BFS,5,1.775200,450,0
no_path_30x30,DFS,1,2.090400,450,0
no_path_30x30,DFS,2,2.222600,450,0
no_path_30x30,DFS,3,2.454300,450,0
no_path_30x30,DFS,4,2.476200,450,0
no_path_30x30,DFS,5,2.073700,450,0
no_path_30x30,A*,1,3.651700,450,0
no_path_30x30,A*,2,3.495200,450,0
no_path_30x30,A*,3,3.754200,450,0
no_path_30x30,A*,4,3.286800,450,0
no_path_30x30,A*,5,3.335200,450,0
no_path_30x30,Dijkstra,1,3.050900,450,0
no_path_30x30,Dijkstra,2,3.109900,450,0
no_path_30x30,Dijkstra,3,3.292500,450,0
no_path_30x30,Dijkstra,4,3.418600,450,0
no_path_30x30,Dijkstra,5,3.212100,450,0
weighted_30x30,BFS,1,3.418900,788,55
weighted_30x30,BFS,2,3.368200,788,55
weighted_30x30,BFS,3,3.516400,788,55
weighted_30x30,BFS,4,3.224300,788,55
weighted_30x30,BFS,5,3.131100,788,55
weighted_30x30,DFS,1,3.291200,693,479
weighted_30x30,DFS,2,3.362300,693,479
weighted_30x30,DFS,3,3.523200,693,479
weighted_30x30,DFS,4,3.521400,693,479
weighted_30x30,DFS,5,3.332300,693,479
weighted_30x30,A*,1,1.181000,126,55
weighted_30x30,A*,2,1.080200,126,55
weighted_30x30,A*,3,1.368400,126,55
weighted_30x30,A*,4,1.109800,126,55
weighted_30x30,A*,5,1.079300,126,55
weighted_30x30,Dijkstra,1,6.112700,781,55
weighted_30x30,Dijkstra,2,5.464800,781,55
weighted_30x30,Dijkstra,3,5.794500,781,55
weighted_30x30,Dijkstra,4,6.171700,781,55
weighted_30x30,Dijkstra,5,6.640500,781,55
1 maze strategy run time_ms visited_cells path_length
2 small_10x10 BFS 1 0.086300 15 15
3 small_10x10 BFS 2 0.061100 15 15
4 small_10x10 BFS 3 0.059300 15 15
5 small_10x10 BFS 4 0.058400 15 15
6 small_10x10 BFS 5 0.058500 15 15
7 small_10x10 DFS 1 0.073400 15 15
8 small_10x10 DFS 2 0.063500 15 15
9 small_10x10 DFS 3 0.062500 15 15
10 small_10x10 DFS 4 0.062700 15 15
11 small_10x10 DFS 5 0.070900 15 15
12 small_10x10 A* 1 0.110100 15 15
13 small_10x10 A* 2 0.089200 15 15
14 small_10x10 A* 3 0.087800 15 15
15 small_10x10 A* 4 0.087600 15 15
16 small_10x10 A* 5 0.087000 15 15
17 small_10x10 Dijkstra 1 0.290000 15 15
18 small_10x10 Dijkstra 2 0.083300 15 15
19 small_10x10 Dijkstra 3 0.091500 15 15
20 small_10x10 Dijkstra 4 0.081000 15 15
21 small_10x10 Dijkstra 5 0.080400 15 15
22 medium_50x50 BFS 1 6.799200 1579 95
23 medium_50x50 BFS 2 6.960100 1579 95
24 medium_50x50 BFS 3 6.337000 1579 95
25 medium_50x50 BFS 4 7.431700 1579 95
26 medium_50x50 BFS 5 6.517900 1579 95
27 medium_50x50 DFS 1 6.463000 1277 647
28 medium_50x50 DFS 2 6.815500 1277 647
29 medium_50x50 DFS 3 5.816100 1277 647
30 medium_50x50 DFS 4 6.492400 1277 647
31 medium_50x50 DFS 5 6.532500 1277 647
32 medium_50x50 A* 1 6.940500 927 95
33 medium_50x50 A* 2 7.275400 927 95
34 medium_50x50 A* 3 7.062500 927 95
35 medium_50x50 A* 4 7.727600 927 95
36 medium_50x50 A* 5 7.321000 927 95
37 medium_50x50 Dijkstra 1 11.483200 1579 95
38 medium_50x50 Dijkstra 2 11.194200 1579 95
39 medium_50x50 Dijkstra 3 11.255200 1579 95
40 medium_50x50 Dijkstra 4 10.512500 1579 95
41 medium_50x50 Dijkstra 5 10.696400 1579 95
42 large_100x100 BFS 1 25.623500 5566 195
43 large_100x100 BFS 2 24.348800 5566 195
44 large_100x100 BFS 3 25.452600 5566 195
45 large_100x100 BFS 4 30.516900 5566 195
46 large_100x100 BFS 5 33.694700 5566 195
47 large_100x100 DFS 1 19.415200 3543 1531
48 large_100x100 DFS 2 19.919000 3543 1531
49 large_100x100 DFS 3 19.104600 3543 1531
50 large_100x100 DFS 4 20.000600 3543 1531
51 large_100x100 DFS 5 17.840200 3543 1531
52 large_100x100 A* 1 7.509300 853 195
53 large_100x100 A* 2 7.221200 853 195
54 large_100x100 A* 3 6.486700 853 195
55 large_100x100 A* 4 6.357600 853 195
56 large_100x100 A* 5 6.723800 853 195
57 large_100x100 Dijkstra 1 40.782300 5571 195
58 large_100x100 Dijkstra 2 41.155000 5571 195
59 large_100x100 Dijkstra 3 39.456200 5571 195
60 large_100x100 Dijkstra 4 41.388700 5571 195
61 large_100x100 Dijkstra 5 40.962500 5571 195
62 empty_30x30 BFS 1 4.143200 896 55
63 empty_30x30 BFS 2 3.987000 896 55
64 empty_30x30 BFS 3 3.777100 896 55
65 empty_30x30 BFS 4 3.682300 896 55
66 empty_30x30 BFS 5 3.737900 896 55
67 empty_30x30 DFS 1 4.024200 842 815
68 empty_30x30 DFS 2 4.333900 842 815
69 empty_30x30 DFS 3 5.411000 842 815
70 empty_30x30 DFS 4 4.677200 842 815
71 empty_30x30 DFS 5 5.177400 842 815
72 empty_30x30 A* 1 6.603700 784 55
73 empty_30x30 A* 2 6.200600 784 55
74 empty_30x30 A* 3 6.798400 784 55
75 empty_30x30 A* 4 7.178500 784 55
76 empty_30x30 A* 5 6.660800 784 55
77 empty_30x30 Dijkstra 1 6.396000 896 55
78 empty_30x30 Dijkstra 2 6.275200 896 55
79 empty_30x30 Dijkstra 3 6.845700 896 55
80 empty_30x30 Dijkstra 4 6.531200 896 55
81 empty_30x30 Dijkstra 5 6.783400 896 55
82 no_path_30x30 BFS 1 2.000100 450 0
83 no_path_30x30 BFS 2 1.797900 450 0
84 no_path_30x30 BFS 3 1.796200 450 0
85 no_path_30x30 BFS 4 1.774100 450 0
86 no_path_30x30 BFS 5 1.775200 450 0
87 no_path_30x30 DFS 1 2.090400 450 0
88 no_path_30x30 DFS 2 2.222600 450 0
89 no_path_30x30 DFS 3 2.454300 450 0
90 no_path_30x30 DFS 4 2.476200 450 0
91 no_path_30x30 DFS 5 2.073700 450 0
92 no_path_30x30 A* 1 3.651700 450 0
93 no_path_30x30 A* 2 3.495200 450 0
94 no_path_30x30 A* 3 3.754200 450 0
95 no_path_30x30 A* 4 3.286800 450 0
96 no_path_30x30 A* 5 3.335200 450 0
97 no_path_30x30 Dijkstra 1 3.050900 450 0
98 no_path_30x30 Dijkstra 2 3.109900 450 0
99 no_path_30x30 Dijkstra 3 3.292500 450 0
100 no_path_30x30 Dijkstra 4 3.418600 450 0
101 no_path_30x30 Dijkstra 5 3.212100 450 0
102 weighted_30x30 BFS 1 3.418900 788 55
103 weighted_30x30 BFS 2 3.368200 788 55
104 weighted_30x30 BFS 3 3.516400 788 55
105 weighted_30x30 BFS 4 3.224300 788 55
106 weighted_30x30 BFS 5 3.131100 788 55
107 weighted_30x30 DFS 1 3.291200 693 479
108 weighted_30x30 DFS 2 3.362300 693 479
109 weighted_30x30 DFS 3 3.523200 693 479
110 weighted_30x30 DFS 4 3.521400 693 479
111 weighted_30x30 DFS 5 3.332300 693 479
112 weighted_30x30 A* 1 1.181000 126 55
113 weighted_30x30 A* 2 1.080200 126 55
114 weighted_30x30 A* 3 1.368400 126 55
115 weighted_30x30 A* 4 1.109800 126 55
116 weighted_30x30 A* 5 1.079300 126 55
117 weighted_30x30 Dijkstra 1 6.112700 781 55
118 weighted_30x30 Dijkstra 2 5.464800 781 55
119 weighted_30x30 Dijkstra 3 5.794500 781 55
120 weighted_30x30 Dijkstra 4 6.171700 781 55
121 weighted_30x30 Dijkstra 5 6.640500 781 55

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,25 @@
maze,strategy,avg_time_ms,avg_visited_cells,avg_path_length,runs
small_10x10,BFS,0.064720,15.00,15.00,5
small_10x10,DFS,0.066600,15.00,15.00,5
small_10x10,A*,0.092340,15.00,15.00,5
small_10x10,Dijkstra,0.125240,15.00,15.00,5
medium_50x50,BFS,6.809180,1579.00,95.00,5
medium_50x50,DFS,6.423900,1277.00,647.00,5
medium_50x50,A*,7.265400,927.00,95.00,5
medium_50x50,Dijkstra,11.028300,1579.00,95.00,5
large_100x100,BFS,27.927300,5566.00,195.00,5
large_100x100,DFS,19.255920,3543.00,1531.00,5
large_100x100,A*,6.859720,853.00,195.00,5
large_100x100,Dijkstra,40.748940,5571.00,195.00,5
empty_30x30,BFS,3.865500,896.00,55.00,5
empty_30x30,DFS,4.724740,842.00,815.00,5
empty_30x30,A*,6.688400,784.00,55.00,5
empty_30x30,Dijkstra,6.566300,896.00,55.00,5
no_path_30x30,BFS,1.828700,450.00,0.00,5
no_path_30x30,DFS,2.263440,450.00,0.00,5
no_path_30x30,A*,3.504620,450.00,0.00,5
no_path_30x30,Dijkstra,3.216800,450.00,0.00,5
weighted_30x30,BFS,3.331780,788.00,55.00,5
weighted_30x30,DFS,3.406080,693.00,479.00,5
weighted_30x30,A*,1.163740,126.00,55.00,5
weighted_30x30,Dijkstra,6.036840,781.00,55.00,5
1 maze strategy avg_time_ms avg_visited_cells avg_path_length runs
2 small_10x10 BFS 0.064720 15.00 15.00 5
3 small_10x10 DFS 0.066600 15.00 15.00 5
4 small_10x10 A* 0.092340 15.00 15.00 5
5 small_10x10 Dijkstra 0.125240 15.00 15.00 5
6 medium_50x50 BFS 6.809180 1579.00 95.00 5
7 medium_50x50 DFS 6.423900 1277.00 647.00 5
8 medium_50x50 A* 7.265400 927.00 95.00 5
9 medium_50x50 Dijkstra 11.028300 1579.00 95.00 5
10 large_100x100 BFS 27.927300 5566.00 195.00 5
11 large_100x100 DFS 19.255920 3543.00 1531.00 5
12 large_100x100 A* 6.859720 853.00 195.00 5
13 large_100x100 Dijkstra 40.748940 5571.00 195.00 5
14 empty_30x30 BFS 3.865500 896.00 55.00 5
15 empty_30x30 DFS 4.724740 842.00 815.00 5
16 empty_30x30 A* 6.688400 784.00 55.00 5
17 empty_30x30 Dijkstra 6.566300 896.00 55.00 5
18 no_path_30x30 BFS 1.828700 450.00 0.00 5
19 no_path_30x30 DFS 2.263440 450.00 0.00 5
20 no_path_30x30 A* 3.504620 450.00 0.00 5
21 no_path_30x30 Dijkstra 3.216800 450.00 0.00 5
22 weighted_30x30 BFS 3.331780 788.00 55.00 5
23 weighted_30x30 DFS 3.406080 693.00 479.00 5
24 weighted_30x30 A* 1.163740 126.00 55.00 5
25 weighted_30x30 Dijkstra 6.036840 781.00 55.00 5

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,59 @@
from builders.text_file_maze_builder import TextFileMazeBuilder
from core.player import Player
from observer.console_view import ConsoleView
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 controller.game_controller import GameController
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
def run_demo():
builder = TextFileMazeBuilder()
maze = builder.buildFromFile(str(BASE_DIR / "mazes" / "maze_small.txt"))
view = ConsoleView()
view.update({"type": "maze_loaded", "message": "Maze loaded"})
view.render(maze)
solver = MazeSolver(maze)
solver.addObserver(view)
for strategy in (BFSStrategy(), DFSStrategy(), AStarStrategy()):
solver.setStrategy(strategy)
stats = solver.solve()
print()
print(f"=== {strategy.name} ===")
print(f"Time: {stats.timeMs:.3f} ms")
print(f"Visited cells: {stats.visitedCells}")
print(f"Path length: {stats.pathLength}")
print(f"Path found: {'yes' if stats.found else 'no'}")
view.render(maze, path=stats.path)
player = Player(maze.startCell)
controller = GameController(maze, player, view)
print("Manual mode: W/A/S/D move, Z undo, Q quit")
view.render(maze, player_position=player.currentCell)
while True:
cmd = input("Command: ").strip().upper()
if cmd == "Q":
break
if cmd == "Z":
controller.undo()
elif cmd in {"W", "A", "S", "D"}:
controller.move(cmd)
else:
print("Unknown command")
if __name__ == "__main__":
run_demo()

View File

@ -0,0 +1,9 @@
S
E

View File

@ -0,0 +1,11 @@
####################################################################################################
#S # # # # # # # # # # # # # # # E#
# # ### ### # ###### # ### # ## # #### # ####### # #### # # ### ## # ## # # ## # ## # ##### ### ##
# # # # # # # # # # # # # # # # # # # # # # # # # # #
# ##### # ######## # ### # ## # #### # ####### ## ### # # #### ####### ## ####### ####### # ### ##
# # # # # # # # # # # # # # # # # # # # #
### # # ###### # ########### ########### ### ####### # ####### ### # # ###### # ### ### # ### ####
# # # # # # # # # # # # # # # # # # # # # #
# ### ###### # ##### # ### # ####### # ### ### ## # ###### # ### # ### ###### # ### # ### ### ## #
# # # # # # # # #
####################################################################################################

View File

@ -0,0 +1,11 @@
##################################################
#S # # # # # # E#
# # ### ### # ###### # ### # ## # #### # ####### ##
# # # # # # # # # # # # # #
# ##### # ######## # ### # ## # #### # ####### ## #
# # # # # # # # # #
### # # ###### # ########### ########### ### ######
# # # # # # # # # # #
# ### ###### # ##### # ### # ####### # ### ### ## #
# # # # #
##################################################

View File

@ -0,0 +1,9 @@
##########
#S #
# ###### #
# # #
##########
# #E#
# ###### #
# #
##########

View File

@ -0,0 +1,7 @@
##########
#S #E#
# ## # # ##
# # #
# #### # #
# # #
##########

View File

@ -0,0 +1,10 @@
1111111111111111111111111111
1S11111111111111111111111111
1111111111111111111111111111
1111111111111111111111111111
1111111111111222222222222111
1111111111111222222222222111
1111111111111333333333333111
1111111111111333333333333111
111111111111111111111111111E
1111111111111111111111111111

View File

View File

@ -0,0 +1,26 @@
import os
from observer.observer import Observer
class ConsoleView(Observer):
def update(self, event):
if isinstance(event, str):
print(f"[EVENT] {event}")
elif isinstance(event, dict):
event_type = event.get("type", "unknown")
if event_type == "search_finished":
stats = event.get("stats")
print(f"[EVENT] search finished: {stats}")
else:
print(f"[EVENT] {event_type}: {event}")
else:
print("[EVENT] unknown")
def clear(self):
os.system("cls" if os.name == "nt" else "clear")
def render(self, maze, player_position=None, path=None, clear_screen=False):
if clear_screen:
self.clear()
print(maze.render(player_position=player_position, path=path))
print()

View File

@ -0,0 +1,7 @@
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, event):
raise NotImplementedError

View File

@ -0,0 +1 @@
matplotlib

View File

View File

@ -0,0 +1,50 @@
import time
from core.search_stats import SearchStats
class MazeSolver:
def __init__(self, maze, strategy=None):
self.maze = maze
self.strategy = strategy
self.observers = []
def setStrategy(self, strategy):
self.strategy = strategy
def addObserver(self, observer):
if observer not in self.observers:
self.observers.append(observer)
def removeObserver(self, observer):
if observer in self.observers:
self.observers.remove(observer)
def notify(self, event):
for observer in self.observers:
observer.update(event)
def solve(self):
if self.strategy is None:
raise ValueError("Strategy is not set")
self.notify({"type": "search_started", "strategy": self.strategy.name})
start_time = time.perf_counter()
path = self.strategy.findPath(self.maze, self.maze.startCell, self.maze.exitCell)
end_time = time.perf_counter()
stats = SearchStats(
timeMs=(end_time - start_time) * 1000.0,
visitedCells=getattr(self.strategy, "visitedCount", 0),
pathLength=len(path),
path=path,
found=bool(path),
algorithm=getattr(self.strategy, "name", "")
)
if stats.found:
self.notify({"type": "path_found", "strategy": stats.algorithm, "length": stats.pathLength})
else:
self.notify({"type": "path_not_found", "strategy": stats.algorithm})
self.notify({"type": "search_finished", "stats": stats})
return stats

View File

@ -0,0 +1,45 @@
import heapq
from strategies.pathfinding_strategy import PathFindingStrategy
class AStarStrategy(PathFindingStrategy):
name = "A*"
def heuristic(self, cell, exitCell):
return abs(cell.x - exitCell.x) + abs(cell.y - exitCell.y)
def findPath(self, maze, start, exitCell):
self.visitedCount = 0
if start is None or exitCell is None:
return []
open_set = []
heapq.heappush(open_set, (0, 0, start.x, start.y, start))
parent = {}
g_score = {(start.x, start.y): 0}
closed = set()
while open_set:
f_score, current_g, _, _, current = heapq.heappop(open_set)
pos = (current.x, current.y)
if pos in closed:
continue
closed.add(pos)
self.visitedCount += 1
if current.x == exitCell.x and current.y == exitCell.y:
return self._restore_path(parent, start, exitCell)
for neighbor in maze.getNeighbors(current):
npos = (neighbor.x, neighbor.y)
tentative_g = current_g + getattr(neighbor, "weight", 1)
if tentative_g < g_score.get(npos, float("inf")):
g_score[npos] = tentative_g
parent[npos] = current
new_f = tentative_g + self.heuristic(neighbor, exitCell)
heapq.heappush(open_set, (new_f, tentative_g, neighbor.x, neighbor.y, neighbor))
return []

View File

@ -0,0 +1,31 @@
from collections import deque
from strategies.pathfinding_strategy import PathFindingStrategy
class BFSStrategy(PathFindingStrategy):
name = "BFS"
def findPath(self, maze, start, exitCell):
self.visitedCount = 0
if start is None or exitCell is None:
return []
queue = deque([start])
visited = {(start.x, start.y)}
parent = {}
while queue:
current = queue.popleft()
self.visitedCount += 1
if current.x == exitCell.x and current.y == exitCell.y:
return self._restore_path(parent, start, exitCell)
for neighbor in maze.getNeighbors(current):
pos = (neighbor.x, neighbor.y)
if pos not in visited:
visited.add(pos)
parent[pos] = current
queue.append(neighbor)
return []

View File

@ -0,0 +1,35 @@
from strategies.pathfinding_strategy import PathFindingStrategy
class DFSStrategy(PathFindingStrategy):
name = "DFS"
def findPath(self, maze, start, exitCell):
self.visitedCount = 0
if start is None or exitCell is None:
return []
stack = [start]
visited = set()
parent = {}
while stack:
current = stack.pop()
pos = (current.x, current.y)
if pos in visited:
continue
visited.add(pos)
self.visitedCount += 1
if current.x == exitCell.x and current.y == exitCell.y:
return self._restore_path(parent, start, exitCell)
neighbors = maze.getNeighbors(current)
for neighbor in reversed(neighbors):
npos = (neighbor.x, neighbor.y)
if npos not in visited:
parent[npos] = current
stack.append(neighbor)
return []

View File

@ -0,0 +1,41 @@
import heapq
from strategies.pathfinding_strategy import PathFindingStrategy
class DijkstraStrategy(PathFindingStrategy):
name = "Dijkstra"
def findPath(self, maze, start, exitCell):
self.visitedCount = 0
if start is None or exitCell is None:
return []
pq = [(0, start.x, start.y, start)]
dist = {(start.x, start.y): 0}
parent = {}
closed = set()
while pq:
current_cost, _, _, current = heapq.heappop(pq)
pos = (current.x, current.y)
if pos in closed:
continue
closed.add(pos)
self.visitedCount += 1
if current.x == exitCell.x and current.y == exitCell.y:
return self._restore_path(parent, start, exitCell)
for neighbor in maze.getNeighbors(current):
npos = (neighbor.x, neighbor.y)
step_cost = getattr(neighbor, "weight", 1)
new_cost = current_cost + step_cost
if new_cost < dist.get(npos, float("inf")):
dist[npos] = new_cost
parent[npos] = current
heapq.heappush(pq, (new_cost, neighbor.x, neighbor.y, neighbor))
return []

View File

@ -0,0 +1,30 @@
from abc import ABC, abstractmethod
class PathFindingStrategy(ABC):
name = "Base"
def __init__(self):
self.visitedCount = 0
@abstractmethod
def findPath(self, maze, start, exitCell):
raise NotImplementedError
def _restore_path(self, parent, start, exitCell):
if exitCell is None or start is None:
return []
path = []
current = exitCell
while True:
path.append(current)
if current.x == start.x and current.y == start.y:
break
current = parent.get((current.x, current.y))
if current is None:
return []
path.reverse()
return path