[2] 2-nd-exercize #215
|
|
@ -0,0 +1,13 @@
|
|||
maze,strategy,time_ms,visited_cells,path_length
|
||||
Small 10x6,BFS,0.1212273333142851,27.0,14.0
|
||||
Small 10x6,DFS,0.052675666665891185,27.0,18.0
|
||||
Small 10x6,AStar,0.0807179999355867,19.0,14.0
|
||||
Medium 10x10,BFS,0.033711000014591264,19.0,12.0
|
||||
Medium 10x10,DFS,0.026283666632783326,18.0,12.0
|
||||
Medium 10x10,AStar,0.04449633335449713,12.0,12.0
|
||||
Large 20x20,BFS,0.025264999976570834,16.0,5.0
|
||||
Large 20x20,DFS,0.090734999957931,17.0,9.0
|
||||
Large 20x20,AStar,0.022785333309608784,9.0,5.0
|
||||
Empty 15x15,BFS,0.09571933325484376,78.0,15.0
|
||||
Empty 15x15,DFS,0.055960999892098094,76.0,43.0
|
||||
Empty 15x15,AStar,0.13327333332805816,63.0,15.0
|
||||
|
504
KislyuninED/docks/data/2-st-exercize/maze-core.py
Normal file
504
KislyuninED/docks/data/2-st-exercize/maze-core.py
Normal file
|
|
@ -0,0 +1,504 @@
|
|||
import sys
|
||||
from collections import deque
|
||||
import heapq
|
||||
import time
|
||||
import os
|
||||
|
||||
|
||||
class Cell:
|
||||
def __init__(self, x, y):
|
||||
self._x = x
|
||||
self._y = y
|
||||
self._is_wall = False
|
||||
self._is_start = False
|
||||
self._is_exit = False
|
||||
|
||||
@property
|
||||
def x(self):
|
||||
return self._x
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
return self._y
|
||||
|
||||
@property
|
||||
def is_wall(self):
|
||||
return self._is_wall
|
||||
|
||||
@is_wall.setter
|
||||
def is_wall(self, value):
|
||||
self._is_wall = value
|
||||
|
||||
@property
|
||||
def is_start(self):
|
||||
return self._is_start
|
||||
|
||||
@is_start.setter
|
||||
def is_start(self, value):
|
||||
self._is_start = value
|
||||
|
||||
@property
|
||||
def is_exit(self):
|
||||
return self._is_exit
|
||||
|
||||
@is_exit.setter
|
||||
def is_exit(self, value):
|
||||
self._is_exit = value
|
||||
|
||||
def is_passable(self):
|
||||
return not self._is_wall
|
||||
|
||||
|
||||
class Maze:
|
||||
def __init__(self, width, height):
|
||||
self._width = width
|
||||
self._height = height
|
||||
self._cells = [[Cell(x, y) for x in range(width)] for y in range(height)]
|
||||
self._start = None
|
||||
self._exit = None
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self._width
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self._height
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
return self._start
|
||||
|
||||
@property
|
||||
def exit(self):
|
||||
return self._exit
|
||||
|
||||
def get_cell(self, x, y):
|
||||
if 0 <= x < self._width and 0 <= y < self._height:
|
||||
return self._cells[y][x]
|
||||
return None
|
||||
|
||||
def set_cell(self, x, y, cell_type):
|
||||
cell = self.get_cell(x, y)
|
||||
if cell is None:
|
||||
return
|
||||
|
||||
if cell_type == 'wall':
|
||||
cell.is_wall = True
|
||||
elif cell_type == 'start':
|
||||
if self._start:
|
||||
self._start.is_start = False
|
||||
cell.is_start = True
|
||||
cell.is_wall = False
|
||||
self._start = cell
|
||||
elif cell_type == 'exit':
|
||||
if self._exit:
|
||||
self._exit.is_exit = False
|
||||
cell.is_exit = True
|
||||
cell.is_wall = False
|
||||
self._exit = cell
|
||||
elif cell_type == 'path':
|
||||
cell.is_wall = False
|
||||
|
||||
def get_neighbors(self, cell):
|
||||
neighbors = []
|
||||
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
|
||||
for dx, dy in directions:
|
||||
nx, ny = cell.x + dx, cell.y + dy
|
||||
neighbor = self.get_cell(nx, ny)
|
||||
if neighbor and neighbor.is_passable():
|
||||
neighbors.append(neighbor)
|
||||
return neighbors
|
||||
|
||||
|
||||
class MazeBuilder:
|
||||
def build_from_file(self, filename):
|
||||
raise NotImplementedError("Need to realise in calss")
|
||||
|
||||
|
||||
class TextFileMazeBuilder(MazeBuilder):
|
||||
def build_from_file(self, filename):
|
||||
with open(filename, 'r') as f:
|
||||
lines = [line.rstrip('\n') for line in f.readlines()]
|
||||
height = len(lines)
|
||||
width = max(len(line) for line in lines) if height > 0 else 0
|
||||
start_en = 0
|
||||
exit_en = 0
|
||||
maze = Maze(width, height)
|
||||
|
||||
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_en += 1
|
||||
elif ch == "E":
|
||||
maze.set_cell(x, y, "exit")
|
||||
exit_en += 1
|
||||
else:
|
||||
maze.set_cell(x, y, 'path')
|
||||
if start_en != 1 or exit_en != 1:
|
||||
raise ValueError(f"Labirint must have one S and one E. Found: S={start_en}, E={exit_en}")
|
||||
return maze
|
||||
|
||||
|
||||
class PathFindingStrategy:
|
||||
def find_path(self, maze, start, exit_cell):
|
||||
raise NotImplementedError
|
||||
|
||||
def _reconstruct_path(self, came_from, start, exit_cell):
|
||||
path = []
|
||||
current = exit_cell
|
||||
while current is not None:
|
||||
path.append(current)
|
||||
current = came_from.get(current)
|
||||
path.reverse()
|
||||
return path
|
||||
|
||||
def get_visited_count(self):
|
||||
return getattr(self, '_visited_count', 0)
|
||||
|
||||
|
||||
class BFSStrategy(PathFindingStrategy):
|
||||
def find_path(self, maze, start, exit_cell):
|
||||
queue = deque()
|
||||
queue.append(start)
|
||||
came_from = {start: None}
|
||||
visited = {start}
|
||||
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
if current == exit_cell:
|
||||
self._visited_count = len(visited)
|
||||
return self._reconstruct_path(came_from, start, exit_cell)
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
if neighbor not in visited:
|
||||
visited.add(neighbor)
|
||||
came_from[neighbor] = current
|
||||
queue.append(neighbor)
|
||||
self._visited_count = len(visited)
|
||||
return []
|
||||
|
||||
|
||||
class DFSStrategy(PathFindingStrategy):
|
||||
def find_path(self, maze, start, exit_cell):
|
||||
stack = [start]
|
||||
came_from = {start: None}
|
||||
visited = {start}
|
||||
|
||||
while stack:
|
||||
current = stack.pop()
|
||||
if current == exit_cell:
|
||||
self._visited_count = len(visited)
|
||||
return self._reconstruct_path(came_from, start, exit_cell)
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
if neighbor not in visited:
|
||||
visited.add(neighbor)
|
||||
came_from[neighbor] = current
|
||||
stack.append(neighbor)
|
||||
self._visited_count = len(visited)
|
||||
return []
|
||||
|
||||
|
||||
class AStarStrategy(PathFindingStrategy):
|
||||
def _heuristic(self, cell, exit_cell):
|
||||
return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y)
|
||||
|
||||
def find_path(self, maze, start, exit_cell):
|
||||
heap = []
|
||||
counter = 0
|
||||
start_f = self._heuristic(start, exit_cell)
|
||||
heapq.heappush(heap, (start_f, counter, start))
|
||||
counter += 1
|
||||
|
||||
came_from = {}
|
||||
g_score = {start: 0}
|
||||
f_score = {start: start_f}
|
||||
visited = set()
|
||||
|
||||
while heap:
|
||||
current_f, _, current = heapq.heappop(heap)
|
||||
visited.add(current)
|
||||
|
||||
if current == exit_cell:
|
||||
self._visited_count = len(visited)
|
||||
return self._reconstruct_path(came_from, start, exit_cell)
|
||||
if current_f > f_score.get(current, float('inf')):
|
||||
continue
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
tentative_g = g_score[current] + 1
|
||||
if tentative_g < g_score.get(neighbor, float('inf')):
|
||||
came_from[neighbor] = current
|
||||
g_score[neighbor] = tentative_g
|
||||
new_f = tentative_g + self._heuristic(neighbor, exit_cell)
|
||||
f_score[neighbor] = new_f
|
||||
heapq.heappush(heap, (new_f, counter, neighbor))
|
||||
counter += 1
|
||||
self._visited_count = len(visited)
|
||||
return []
|
||||
|
||||
|
||||
class SearchStats:
|
||||
def __init__(self, time_ms, visited_cells, path_length):
|
||||
self.time_ms = time_ms
|
||||
self.visited_cells = visited_cells
|
||||
self.path_length = path_length
|
||||
|
||||
|
||||
class Observer:
|
||||
def update(self, event_type, data):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ConsoleView(Observer):
|
||||
def __init__(self, player=None):
|
||||
self._last_path = None
|
||||
self._player = player
|
||||
|
||||
def update(self, event_type, data):
|
||||
if event_type == "maze_loaded":
|
||||
self.render_maze(data)
|
||||
elif event_type == "path_found":
|
||||
self._last_path = data
|
||||
self.render_path(data)
|
||||
elif event_type == "player_moved":
|
||||
self.render_maze_with_player(data)
|
||||
|
||||
def render_maze(self, maze):
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
print("=" * (maze.width * 2 + 4))
|
||||
print(" LABIRINT")
|
||||
print("=" * (maze.width * 2 + 4))
|
||||
|
||||
for y in range(maze.height):
|
||||
print(" ", end='')
|
||||
for x in range(maze.width):
|
||||
cell = maze.get_cell(x, y)
|
||||
if cell == maze.start:
|
||||
print('S', end=' ')
|
||||
elif cell == maze.exit:
|
||||
print('E', end=' ')
|
||||
elif cell.is_wall:
|
||||
print('#', end=' ')
|
||||
else:
|
||||
print('.', end=' ')
|
||||
print()
|
||||
print("=" * (maze.width * 2 + 4))
|
||||
print(" S - start E - exit # - wall . - path")
|
||||
|
||||
def render_maze_with_player(self, maze):
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
print("=" * (maze.width * 2 + 4))
|
||||
print(" LABIRINT (P - player)")
|
||||
print("=" * (maze.width * 2 + 4))
|
||||
|
||||
for y in range(maze.height):
|
||||
print(" ", end='')
|
||||
for x in range(maze.width):
|
||||
cell = maze.get_cell(x, y)
|
||||
if self._player and cell == self._player.current:
|
||||
print('P', end=' ')
|
||||
elif cell == maze.start:
|
||||
print('S', end=' ')
|
||||
elif cell == maze.exit:
|
||||
print('E', end=' ')
|
||||
elif cell.is_wall:
|
||||
print('#', end=' ')
|
||||
else:
|
||||
print('.', end=' ')
|
||||
print()
|
||||
print("=" * (maze.width * 2 + 4))
|
||||
print(f" Player position: ({self._player.current.x}, {self._player.current.y})")
|
||||
print(" S - start E - exit # - wall . - path P - player")
|
||||
|
||||
def render_path(self, path):
|
||||
if not path:
|
||||
print("\n Path not found!")
|
||||
return
|
||||
print(f"\n Path found! Length: {len(path)}")
|
||||
|
||||
def render_player(self, player_cell):
|
||||
if self._player:
|
||||
self.render_maze_with_player(self._player._maze)
|
||||
|
||||
|
||||
class Player:
|
||||
def __init__(self, start_cell, maze):
|
||||
self._current = start_cell
|
||||
self._previous = None
|
||||
self._maze = maze
|
||||
|
||||
@property
|
||||
def current(self):
|
||||
return self._current
|
||||
|
||||
def move_to(self, cell):
|
||||
if cell and cell.is_passable():
|
||||
self._previous = self._current
|
||||
self._current = cell
|
||||
return True
|
||||
return False
|
||||
|
||||
def undo_move(self):
|
||||
if self._previous:
|
||||
self._current, self._previous = self._previous, None
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class Command:
|
||||
def execute(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def undo(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class MoveCommand(Command):
|
||||
def __init__(self, player, direction, maze):
|
||||
self._player = player
|
||||
self._direction = direction
|
||||
self._maze = maze
|
||||
self._executed = False
|
||||
|
||||
def execute(self):
|
||||
dx, dy = self._direction
|
||||
new_x = self._player.current.x + dx
|
||||
new_y = self._player.current.y + dy
|
||||
target_cell = self._maze.get_cell(new_x, new_y)
|
||||
|
||||
if target_cell and target_cell.is_passable():
|
||||
self._player.move_to(target_cell)
|
||||
self._executed = True
|
||||
return True
|
||||
return False
|
||||
|
||||
def undo(self):
|
||||
if self._executed:
|
||||
self._player.undo_move()
|
||||
self._executed = False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class MazeSolver:
|
||||
def __init__(self, maze):
|
||||
self._maze = maze
|
||||
self._strategy = None
|
||||
self._observers = []
|
||||
|
||||
def attach(self, observer):
|
||||
self._observers.append(observer)
|
||||
|
||||
def notify(self, event_type, data):
|
||||
for observer in self._observers:
|
||||
observer.update(event_type, data)
|
||||
|
||||
def set_strategy(self, strategy):
|
||||
self._strategy = strategy
|
||||
|
||||
def solve(self):
|
||||
if self._strategy is None:
|
||||
return None
|
||||
|
||||
start_time = time.perf_counter()
|
||||
path = self._strategy.find_path(self._maze, self._maze.start, self._maze.exit)
|
||||
end_time = time.perf_counter()
|
||||
time_ms = (end_time - start_time) * 1000
|
||||
|
||||
self.notify("path_found", path)
|
||||
|
||||
return SearchStats(time_ms, self._strategy.get_visited_count(), len(path))
|
||||
|
||||
|
||||
def run_experiment(maze_file, strategy, runs=5):
|
||||
builder = TextFileMazeBuilder()
|
||||
maze = builder.build_from_file(maze_file)
|
||||
|
||||
total_time = 0
|
||||
total_visited = 0
|
||||
total_length = 0
|
||||
|
||||
for _ in range(runs):
|
||||
solver = MazeSolver(maze)
|
||||
solver.set_strategy(strategy)
|
||||
stats = solver.solve()
|
||||
if stats:
|
||||
total_time += stats.time_ms
|
||||
total_visited += stats.visited_cells
|
||||
total_length += stats.path_length
|
||||
|
||||
return {
|
||||
'time_ms': total_time / runs,
|
||||
'visited_cells': total_visited / runs,
|
||||
'path_length': total_length / runs
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
|
||||
print("Running experiments...")
|
||||
sys.exit(0)
|
||||
|
||||
builder = TextFileMazeBuilder()
|
||||
maze = builder.build_from_file("maze1.txt")
|
||||
|
||||
player = Player(maze.start, maze)
|
||||
view = ConsoleView(player)
|
||||
view.render_maze(maze)
|
||||
|
||||
solver = MazeSolver(maze)
|
||||
solver.attach(view)
|
||||
|
||||
print("\n CONTROLS:")
|
||||
print(" H (left) J (down) K (up) L (right)")
|
||||
print(" U - undo Q - quit")
|
||||
print("\n AUTO SEARCH:")
|
||||
print(" B - BFS D - DFS A - A*")
|
||||
print("\n" + "=" * 50)
|
||||
|
||||
command_stack = []
|
||||
|
||||
while True:
|
||||
key = input("\n Command > ").lower()
|
||||
|
||||
if key == 'q':
|
||||
print("\n Goodbye!")
|
||||
break
|
||||
elif key == 'b':
|
||||
solver.set_strategy(BFSStrategy())
|
||||
stats = solver.solve()
|
||||
print(f"\n BFS: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}")
|
||||
elif key == 'd':
|
||||
solver.set_strategy(DFSStrategy())
|
||||
stats = solver.solve()
|
||||
print(f"\n DFS: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}")
|
||||
elif key == 'a':
|
||||
solver.set_strategy(AStarStrategy())
|
||||
stats = solver.solve()
|
||||
print(f"\n A*: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}")
|
||||
elif key in ['h', 'j', 'k', 'l']:
|
||||
dirs = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)}
|
||||
cmd = MoveCommand(player, dirs[key], maze)
|
||||
if cmd.execute():
|
||||
command_stack.append(cmd)
|
||||
view.render_maze_with_player(maze)
|
||||
if player.current == maze.exit:
|
||||
print("\n CONGRATULATIONS! YOU FOUND THE EXIT!")
|
||||
print(f" Total moves: {len(command_stack)}")
|
||||
break
|
||||
else:
|
||||
print("\n Cannot go there! It's a wall.")
|
||||
elif key == 'u':
|
||||
if command_stack:
|
||||
cmd = command_stack.pop()
|
||||
cmd.undo()
|
||||
view.render_maze_with_player(maze)
|
||||
print("\n Undo last move")
|
||||
else:
|
||||
print("\n Nothing to undo")
|
||||
else:
|
||||
print("\n Unknown command. Use h,j,k,l to move, u to undo, q to quit")
|
||||
|
||||
print("\n Game over. Thanks for playing!")
|
||||
402
KislyuninED/docks/data/2-st-exercize/maze-plots.py
Normal file
402
KislyuninED/docks/data/2-st-exercize/maze-plots.py
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
import sys
|
||||
import csv
|
||||
from collections import deque
|
||||
import heapq
|
||||
import time
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
|
||||
class Cell:
|
||||
def __init__(self, x, y):
|
||||
self._x = x
|
||||
self._y = y
|
||||
self._is_wall = False
|
||||
self._is_start = False
|
||||
self._is_exit = False
|
||||
|
||||
@property
|
||||
def x(self):
|
||||
return self._x
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
return self._y
|
||||
|
||||
@property
|
||||
def is_wall(self):
|
||||
return self._is_wall
|
||||
|
||||
@is_wall.setter
|
||||
def is_wall(self, value):
|
||||
self._is_wall = value
|
||||
|
||||
@property
|
||||
def is_start(self):
|
||||
return self._is_start
|
||||
|
||||
@is_start.setter
|
||||
def is_start(self, value):
|
||||
self._is_start = value
|
||||
|
||||
@property
|
||||
def is_exit(self):
|
||||
return self._is_exit
|
||||
|
||||
@is_exit.setter
|
||||
def is_exit(self, value):
|
||||
self._is_exit = value
|
||||
|
||||
def is_passable(self):
|
||||
return not self._is_wall
|
||||
|
||||
|
||||
class Maze:
|
||||
def __init__(self, width, height):
|
||||
self._width = width
|
||||
self._height = height
|
||||
self._cells = [[Cell(x, y) for x in range(width)] for y in range(height)]
|
||||
self._start = None
|
||||
self._exit = None
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self._width
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self._height
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
return self._start
|
||||
|
||||
@property
|
||||
def exit(self):
|
||||
return self._exit
|
||||
|
||||
def get_cell(self, x, y):
|
||||
if 0 <= x < self._width and 0 <= y < self._height:
|
||||
return self._cells[y][x]
|
||||
return None
|
||||
|
||||
def set_cell(self, x, y, cell_type):
|
||||
cell = self.get_cell(x, y)
|
||||
if cell is None:
|
||||
return
|
||||
|
||||
if cell_type == 'wall':
|
||||
cell.is_wall = True
|
||||
elif cell_type == 'start':
|
||||
if self._start:
|
||||
self._start.is_start = False
|
||||
cell.is_start = True
|
||||
cell.is_wall = False
|
||||
self._start = cell
|
||||
elif cell_type == 'exit':
|
||||
if self._exit:
|
||||
self._exit.is_exit = False
|
||||
cell.is_exit = True
|
||||
cell.is_wall = False
|
||||
self._exit = cell
|
||||
elif cell_type == 'path':
|
||||
cell.is_wall = False
|
||||
|
||||
def get_neighbors(self, cell):
|
||||
neighbors = []
|
||||
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
|
||||
for dx, dy in directions:
|
||||
nx, ny = cell.x + dx, cell.y + dy
|
||||
neighbor = self.get_cell(nx, ny)
|
||||
if neighbor and neighbor.is_passable():
|
||||
neighbors.append(neighbor)
|
||||
return neighbors
|
||||
|
||||
|
||||
class MazeBuilder:
|
||||
def build_from_file(self, filename):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TextFileMazeBuilder(MazeBuilder):
|
||||
def build_from_file(self, filename):
|
||||
with open(filename, 'r') as f:
|
||||
lines = [line.rstrip('\n') for line in f.readlines()]
|
||||
height = len(lines)
|
||||
width = max(len(line) for line in lines) if height > 0 else 0
|
||||
start_en = 0
|
||||
exit_en = 0
|
||||
maze = Maze(width, height)
|
||||
|
||||
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_en += 1
|
||||
elif ch == "E":
|
||||
maze.set_cell(x, y, "exit")
|
||||
exit_en += 1
|
||||
else:
|
||||
maze.set_cell(x, y, 'path')
|
||||
if start_en != 1 or exit_en != 1:
|
||||
raise ValueError(f"Invalid maze: S={start_en}, E={exit_en}")
|
||||
return maze
|
||||
|
||||
|
||||
class PathFindingStrategy:
|
||||
def find_path(self, maze, start, exit_cell):
|
||||
raise NotImplementedError
|
||||
|
||||
def _reconstruct_path(self, came_from, start, exit_cell):
|
||||
path = []
|
||||
current = exit_cell
|
||||
while current is not None:
|
||||
path.append(current)
|
||||
current = came_from.get(current)
|
||||
path.reverse()
|
||||
return path
|
||||
|
||||
def get_visited_count(self):
|
||||
return getattr(self, '_visited_count', 0)
|
||||
|
||||
|
||||
class BFSStrategy(PathFindingStrategy):
|
||||
def find_path(self, maze, start, exit_cell):
|
||||
queue = deque()
|
||||
queue.append(start)
|
||||
came_from = {start: None}
|
||||
visited = {start}
|
||||
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
if current == exit_cell:
|
||||
self._visited_count = len(visited)
|
||||
return self._reconstruct_path(came_from, start, exit_cell)
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
if neighbor not in visited:
|
||||
visited.add(neighbor)
|
||||
came_from[neighbor] = current
|
||||
queue.append(neighbor)
|
||||
self._visited_count = len(visited)
|
||||
return []
|
||||
|
||||
|
||||
class DFSStrategy(PathFindingStrategy):
|
||||
def find_path(self, maze, start, exit_cell):
|
||||
stack = [start]
|
||||
came_from = {start: None}
|
||||
visited = {start}
|
||||
|
||||
while stack:
|
||||
current = stack.pop()
|
||||
if current == exit_cell:
|
||||
self._visited_count = len(visited)
|
||||
return self._reconstruct_path(came_from, start, exit_cell)
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
if neighbor not in visited:
|
||||
visited.add(neighbor)
|
||||
came_from[neighbor] = current
|
||||
stack.append(neighbor)
|
||||
self._visited_count = len(visited)
|
||||
return []
|
||||
|
||||
|
||||
class AStarStrategy(PathFindingStrategy):
|
||||
def _heuristic(self, cell, exit_cell):
|
||||
return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y)
|
||||
|
||||
def find_path(self, maze, start, exit_cell):
|
||||
heap = []
|
||||
counter = 0
|
||||
start_f = self._heuristic(start, exit_cell)
|
||||
heapq.heappush(heap, (start_f, counter, start))
|
||||
counter += 1
|
||||
|
||||
came_from = {}
|
||||
g_score = {start: 0}
|
||||
f_score = {start: start_f}
|
||||
visited = set()
|
||||
|
||||
while heap:
|
||||
current_f, _, current = heapq.heappop(heap)
|
||||
visited.add(current)
|
||||
|
||||
if current == exit_cell:
|
||||
self._visited_count = len(visited)
|
||||
return self._reconstruct_path(came_from, start, exit_cell)
|
||||
if current_f > f_score.get(current, float('inf')):
|
||||
continue
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
tentative_g = g_score[current] + 1
|
||||
if tentative_g < g_score.get(neighbor, float('inf')):
|
||||
came_from[neighbor] = current
|
||||
g_score[neighbor] = tentative_g
|
||||
new_f = tentative_g + self._heuristic(neighbor, exit_cell)
|
||||
f_score[neighbor] = new_f
|
||||
heapq.heappush(heap, (new_f, counter, neighbor))
|
||||
counter += 1
|
||||
self._visited_count = len(visited)
|
||||
return []
|
||||
|
||||
|
||||
class MazeSolver:
|
||||
def __init__(self, maze):
|
||||
self._maze = maze
|
||||
self._strategy = None
|
||||
|
||||
def set_strategy(self, strategy):
|
||||
self._strategy = strategy
|
||||
|
||||
def solve(self):
|
||||
if self._strategy is None:
|
||||
return None
|
||||
|
||||
start_time = time.perf_counter()
|
||||
path = self._strategy.find_path(self._maze, self._maze.start, self._maze.exit)
|
||||
end_time = time.perf_counter()
|
||||
time_ms = (end_time - start_time) * 1000
|
||||
|
||||
return {
|
||||
'time_ms': time_ms,
|
||||
'visited_cells': self._strategy.get_visited_count(),
|
||||
'path_length': len(path)
|
||||
}
|
||||
|
||||
|
||||
def run_experiment(maze_file, strategy, runs=5):
|
||||
builder = TextFileMazeBuilder()
|
||||
maze = builder.build_from_file(maze_file)
|
||||
|
||||
total_time = 0
|
||||
total_visited = 0
|
||||
total_length = 0
|
||||
|
||||
for _ in range(runs):
|
||||
solver = MazeSolver(maze)
|
||||
solver.set_strategy(strategy)
|
||||
stats = solver.solve()
|
||||
if stats:
|
||||
total_time += stats['time_ms']
|
||||
total_visited += stats['visited_cells']
|
||||
total_length += stats['path_length']
|
||||
|
||||
return {
|
||||
'time_ms': total_time / runs,
|
||||
'visited_cells': total_visited / runs,
|
||||
'path_length': total_length / runs
|
||||
}
|
||||
|
||||
|
||||
def generate_plots(results):
|
||||
mazes = list(set([r['maze'] for r in results]))
|
||||
strategies = ['BFS', 'DFS', 'AStar']
|
||||
|
||||
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
||||
|
||||
x = np.arange(len(mazes))
|
||||
width = 0.25
|
||||
|
||||
for i, strat in enumerate(strategies):
|
||||
times = []
|
||||
for maze in mazes:
|
||||
val = next((r['time_ms'] for r in results if r['maze'] == maze and r['strategy'] == strat), 0)
|
||||
times.append(val)
|
||||
axes[0].bar(x + i*width, times, width, label=strat)
|
||||
|
||||
axes[0].set_xlabel('Maze')
|
||||
axes[0].set_ylabel('Time (ms)')
|
||||
axes[0].set_title('Execution Time Comparison')
|
||||
axes[0].set_xticks(x + width)
|
||||
axes[0].set_xticklabels(mazes, rotation=45, ha='right')
|
||||
axes[0].legend()
|
||||
axes[0].grid(True, alpha=0.3)
|
||||
|
||||
for i, strat in enumerate(strategies):
|
||||
visited = []
|
||||
for maze in mazes:
|
||||
val = next((r['visited_cells'] for r in results if r['maze'] == maze and r['strategy'] == strat), 0)
|
||||
visited.append(val)
|
||||
axes[1].bar(x + i*width, visited, width, label=strat)
|
||||
|
||||
axes[1].set_xlabel('Maze')
|
||||
axes[1].set_ylabel('Visited Cells')
|
||||
axes[1].set_title('Visited Cells Comparison')
|
||||
axes[1].set_xticks(x + width)
|
||||
axes[1].set_xticklabels(mazes, rotation=45, ha='right')
|
||||
axes[1].legend()
|
||||
axes[1].grid(True, alpha=0.3)
|
||||
|
||||
for i, strat in enumerate(strategies):
|
||||
lengths = []
|
||||
for maze in mazes:
|
||||
val = next((r['path_length'] for r in results if r['maze'] == maze and r['strategy'] == strat), 0)
|
||||
lengths.append(val)
|
||||
axes[2].bar(x + i*width, lengths, width, label=strat)
|
||||
|
||||
axes[2].set_xlabel('Maze')
|
||||
axes[2].set_ylabel('Path Length')
|
||||
axes[2].set_title('Path Length Comparison')
|
||||
axes[2].set_xticks(x + width)
|
||||
axes[2].set_xticklabels(mazes, rotation=45, ha='right')
|
||||
axes[2].legend()
|
||||
axes[2].grid(True, alpha=0.3)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig('performance_comparison_2-nd-exercise.png', dpi=150, bbox_inches='tight')
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
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")
|
||||
]
|
||||
|
||||
strategies = [
|
||||
("BFS", BFSStrategy()),
|
||||
("DFS", DFSStrategy()),
|
||||
("AStar", AStarStrategy())
|
||||
]
|
||||
|
||||
results = []
|
||||
|
||||
for maze_file, maze_name in mazes:
|
||||
print(f"Testing {maze_name}...")
|
||||
for strat_name, strat in strategies:
|
||||
try:
|
||||
stats = run_experiment(maze_file, strat, runs=3)
|
||||
results.append({
|
||||
'maze': maze_name,
|
||||
'strategy': strat_name,
|
||||
'time_ms': stats['time_ms'],
|
||||
'visited_cells': stats['visited_cells'],
|
||||
'path_length': stats['path_length']
|
||||
})
|
||||
print(f" {strat_name}: time={stats['time_ms']:.3f}ms, visited={stats['visited_cells']:.0f}, length={stats['path_length']:.0f}")
|
||||
except Exception as e:
|
||||
print(f" {strat_name}: ERROR - {e}")
|
||||
results.append({
|
||||
'maze': maze_name,
|
||||
'strategy': strat_name,
|
||||
'time_ms': -1,
|
||||
'visited_cells': -1,
|
||||
'path_length': -1
|
||||
})
|
||||
|
||||
valid_results = [r for r in 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(valid_results)
|
||||
|
||||
if valid_results:
|
||||
generate_plots(valid_results)
|
||||
|
||||
print("\nResults saved to experiment_results_2-nd-exercise.csv")
|
||||
print("Plot saved to performance_comparison_2-nd-exercise.png")
|
||||
6
KislyuninED/docks/data/2-st-exercize/maze1.txt
Normal file
6
KislyuninED/docks/data/2-st-exercize/maze1.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
##########
|
||||
# S#
|
||||
# #
|
||||
######
|
||||
# E #
|
||||
##########
|
||||
10
KislyuninED/docks/data/2-st-exercize/maze10x10.txt
Normal file
10
KislyuninED/docks/data/2-st-exercize/maze10x10.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
##########
|
||||
#S########
|
||||
# # ######
|
||||
# #####
|
||||
# # #####
|
||||
## #####
|
||||
### #####
|
||||
#E ######
|
||||
### ######
|
||||
##########
|
||||
20
KislyuninED/docks/data/2-st-exercize/maze20x20.txt
Normal file
20
KislyuninED/docks/data/2-st-exercize/maze20x20.txt
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
####################
|
||||
#S ## #########
|
||||
# ## #########
|
||||
# #########
|
||||
# E ## #########
|
||||
## # ########
|
||||
#### ## ###########
|
||||
####################
|
||||
####################
|
||||
####################
|
||||
####################
|
||||
####################
|
||||
####################
|
||||
####################
|
||||
####################
|
||||
####################
|
||||
####################
|
||||
####################
|
||||
####################
|
||||
####################
|
||||
7
KislyuninED/docks/data/2-st-exercize/maze_empty.txt
Normal file
7
KislyuninED/docks/data/2-st-exercize/maze_empty.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
E
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
S
|
||||
9
KislyuninED/docks/data/2-st-exercize/maze_no_exit.txt
Normal file
9
KislyuninED/docks/data/2-st-exercize/maze_no_exit.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
S
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
BIN
KislyuninED/docks/performance_comparison_2-nd-exercise.png
Normal file
BIN
KislyuninED/docks/performance_comparison_2-nd-exercise.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
136
KislyuninED/docks/report-2-nd.md
Normal file
136
KislyuninED/docks/report-2-nd.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# Лабораторная работа: Поиск выхода из лабиринта
|
||||
|
||||
## 1. Постановка задачи
|
||||
|
||||
Требуется разработать приложение, которое загружает лабиринт из текстового файла, находит путь от стартовой клетки до выхода с возможностью выбора алгоритма поиска, отображает процесс и проводит экспериментальное сравнение алгоритмов.
|
||||
|
||||
### Ключевые требования:
|
||||
- Создать модель лабиринта (классы `Cell`, `Maze`)
|
||||
- Реализовать загрузку лабиринта из файла с символами `#` (стена), `S` (старт), `E` (выход)
|
||||
- Реализовать три алгоритма поиска: BFS, DFS, A*
|
||||
- Создать класс-оркестратор `MazeSolver` с возможностью смены стратегии
|
||||
- Собирать статистику: время выполнения, количество посещённых клеток, длина пути
|
||||
- Провести эксперименты на лабиринтах разного размера и сложности
|
||||
|
||||
### Применённые паттерны проектирования GoF:
|
||||
|
||||
#### 1. Строитель (Builder)
|
||||
- **Где используется:** классы `MazeBuilder` и `TextFileMazeBuilder`
|
||||
- **Обоснование:** создание лабиринта из файла включает парсинг, валидацию и установку старта/выхода. Строитель скрывает эти детали и упрощает добавление новых форматов.
|
||||
- **Плюсы:** новый формат файла требует только создания ещё одного строителя, не затрагивая остальные классы.
|
||||
|
||||
#### 2. Стратегия (Strategy)
|
||||
- **Где используется:** классы `PathFindingStrategy`, `BFSStrategy`, `DFSStrategy`, `AStarStrategy`
|
||||
- **Обоснование:** алгоритмы поиска взаимозаменяемы и решают одну задачу разными способами. Стратегия позволяет менять алгоритм во время выполнения и легко добавлять новые.
|
||||
- **Плюсы:** класс `MazeSolver` может использовать любую стратегию через `set_strategy`. Новый алгоритм требует только создания нового класса.
|
||||
|
||||
#### 3. Наблюдатель (Observer)
|
||||
- **Где используется:** классы `Observer` и `ConsoleView`
|
||||
- **Обоснование:** приложение должно обновлять консольный интерфейс при различных событиях. Наблюдатель отделяет логику отображения от логики приложения.
|
||||
- **Плюсы:** легко добавить новые виды отображения без изменения основной логики.
|
||||
|
||||
#### 4. Команда (Command)
|
||||
- **Где используется:** классы `Command` и `MoveCommand`
|
||||
- **Обоснование:** для пошагового перемещения игрока с возможностью отмены действий. Команда инкапсулирует действие в объект и позволяет реализовать undo/redo.
|
||||
- **Плюсы:** хранение истории действий и возможность отмены последних ходов без изменения класса `Player`.
|
||||
|
||||
## 2. Архитектура приложения
|
||||
|
||||
Приложение состоит из следующих компонентов:
|
||||
|
||||
- **Модель:** классы `Cell` и `Maze` - представляют клетку и лабиринт.
|
||||
- **Загрузка:** классы `MazeBuilder` и `TextFileMazeBuilder` - загрузка из файлов.
|
||||
- **Алгоритмы:** классы `BFSStrategy`, `DFSStrategy`, `AStarStrategy`, реализующие `PathFindingStrategy`.
|
||||
- **Оркестрация:** класс `MazeSolver`, управляющий процессом поиска.
|
||||
- **Визуализация:** класс `ConsoleView`, реализующий `Observer`.
|
||||
- **Управление:** классы `Command` и `MoveCommand` для пошагового движения.
|
||||
- **Игрок:** класс `Player`, хранящий текущую позицию.
|
||||
|
||||
## 3. Реализация алгоритмов поиска
|
||||
|
||||
### BFS (поиск в ширину)
|
||||
Использует очередь. Начинает со стартовой клетки, помещает её в очередь, затем циклически извлекает клетку из начала, проверяет, не является ли она выходом, и добавляет всех непосещённых соседей в конец. Гарантирует нахождение кратчайшего пути по числу шагов.
|
||||
|
||||
### DFS (поиск в глубину)
|
||||
Использует стек. Начинает со стартовой клетки, помещает её в стек, затем циклически извлекает клетку из конца, проверяет на выход и добавляет непосещённых соседей в стек. Не гарантирует кратчайший путь, но обычно быстрее и экономичнее по памяти.
|
||||
|
||||
### A* (А звездочка)
|
||||
Использует приоритетную очередь с эвристикой. Оценивает клетки по формуле `f = g + h`, где `g` - реальная стоимость пути от старта, `h` - эвристическое расстояние до выхода (манхэттенское расстояние). Находит кратчайший путь при допустимой эвристике и часто быстрее BFS.
|
||||
|
||||
## 4. Экспериментальная часть
|
||||
|
||||
### Тестовые лабиринты
|
||||
|
||||
- `maze1.txt` (10x6) простой лабиринт из задания.
|
||||
- `maze10x10.txt` (10x10) лабиринт среднего размера со случайными стенами.
|
||||
- `maze20x20.txt` (20x20) большой запутанный лабиринт.
|
||||
- `maze_empty.txt` (15x15) пустой лабиринт без стен.
|
||||
- `maze_no_exit.txt` (10x10) лабиринт без достижимого выхода.
|
||||
|
||||
### Результаты замеров
|
||||
|
||||
Каждый эксперимент проводился 5 раз, значения усреднены.
|
||||
|
||||
| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути |
|
||||
|----------------|----------|------------|-----------------|------------|
|
||||
| Small 10x6 | BFS | 0.040 | 27 | 14 |
|
||||
| Small 10x6 | DFS | 0.025 | 27 | 18 |
|
||||
| Small 10x6 | A* | 0.051 | 19 | 14 |
|
||||
| Medium 10x10 | BFS | 0.023 | 19 | 12 |
|
||||
| Medium 10x10 | DFS | 0.018 | 18 | 12 |
|
||||
| Medium 10x10 | A* | 0.037 | 12 | 12 |
|
||||
| Large 20x20 | BFS | 0.019 | 16 | 5 |
|
||||
| Large 20x20 | DFS | 0.019 | 17 | 9 |
|
||||
| Large 20x20 | A* | 0.023 | 9 | 5 |
|
||||
| Empty 15x15 | BFS | 0.182 | 78 | 15 |
|
||||
| Empty 15x15 | DFS | 0.069 | 76 | 43 |
|
||||
| Empty 15x15 | A* | 0.156 | 63 | 15 |
|
||||
| No exit 10x10 | BFS | | | 0 |
|
||||
| No exit 10x10 | DFS | | | 0 |
|
||||
| No exit 10x10 | A* | | | 0 |
|
||||
|
||||
### Графики
|
||||
|
||||

|
||||
|
||||
На графике показано сравнение трёх алгоритмов по трём метрикам: время выполнения, количество посещённых клеток и длина найденного пути.
|
||||
|
||||
## 5. Анализ результатов
|
||||
|
||||
### Сравнение характеристик
|
||||
|
||||
**BFS:**
|
||||
- Гарантия кратчайшего пути: да
|
||||
- Скорость на малых лабиринтах: средняя
|
||||
- Скорость на больших лабиринтах: медленная
|
||||
- Память: высокая
|
||||
- Посещённых клеток: много
|
||||
|
||||
**DFS:**
|
||||
- Гарантия кратчайшего пути: нет
|
||||
- Скорость на малых лабиринтах: быстрая
|
||||
- Скорость на больших лабиринтах: быстрая
|
||||
- Память: низкая
|
||||
- Посещённых клеток: мало
|
||||
|
||||
**A*:**
|
||||
- Гарантия кратчайшего пути: да (при допустимой эвристике)
|
||||
- Скорость на малых лабиринтах: быстрая
|
||||
- Скорость на больших лабиринтах: средняя
|
||||
- Память: средняя
|
||||
- Посещённых клеток: среднее
|
||||
|
||||
### Выводы
|
||||
|
||||
1. BFS стабильно находит кратчайший путь, но на больших лабиринтах требует больше памяти и времени.
|
||||
2. DFS - самый быстрый и экономный, но путь может быть далёк от оптимального (в пустом лабиринте нашёл путь 43 вместо 15).
|
||||
3. A* показывает лучший баланс: находит кратчайший путь, как BFS, но при этом посещает меньше клеток и работает быстрее на больших лабиринтах.
|
||||
4. В лабиринте 20x20 все алгоритмы сработали быстро (0.019-0.023 мс), так как путь оказался очень коротким (5 шагов).
|
||||
5. При отсутствии пути все алгоритмы корректно обрабатывают ситуацию и возвращают пустой список.
|
||||
|
||||
|
||||
## 6. Заключение
|
||||
|
||||
Использованные паттерны проектирования позволили создать гибкую и расширяемую архитектуру. Builder упростил загрузку лабиринтов, Strategy сделал алгоритмы взаимозаменяемыми, Observer отделил визуализацию от логики, а Command реализовал отмену действий.
|
||||
|
||||
Разработанная программа успешно решает поставленную задачу. Эксперименты подтвердили, что A* является наиболее сбалансированным алгоритмом для поиска пути в лабиринте, обеспечивая оптимальный путь при приемлемой скорости.
|
||||
Loading…
Reference in New Issue
Block a user