Compare commits

..

5 Commits

Author SHA1 Message Date
b6c595a11a [2] Final 2026-05-15 09:42:46 +03:00
bb1a35103e [2] add player mode 2026-05-15 09:16:11 +03:00
133d076666 [2] add stats(time of compliting maze) 2026-05-15 09:02:06 +03:00
57c743c253 [2] add Astar method 2026-05-15 08:52:56 +03:00
617d205458 [2] Start 2-nd exersize 2026-05-15 08:05:17 +03:00
23 changed files with 1202 additions and 192 deletions

View File

View File

@ -0,0 +1,13 @@
maze,strategy,time_ms,visited_cells,path_length
Small 10x6,BFS,0.04046166759508196,27.0,14.0
Small 10x6,DFS,0.02375933339256638,27.0,18.0
Small 10x6,AStar,0.051083666524694614,19.0,14.0
Medium 10x10,BFS,0.02262299979823486,19.0,12.0
Medium 10x10,DFS,0.016091333236545324,18.0,12.0
Medium 10x10,AStar,0.03017666616263644,12.0,12.0
Large 20x20,BFS,0.015730000086477958,16.0,5.0
Large 20x20,DFS,0.014211666590805786,17.0,9.0
Large 20x20,AStar,0.020270666330664728,9.0,5.0
Empty 15x15,BFS,0.10161799946217798,78.0,15.0
Empty 15x15,DFS,0.04646399975172244,76.0,43.0
Empty 15x15,AStar,0.13135433376495106,63.0,15.0
1 maze strategy time_ms visited_cells path_length
2 Small 10x6 BFS 0.04046166759508196 27.0 14.0
3 Small 10x6 DFS 0.02375933339256638 27.0 18.0
4 Small 10x6 AStar 0.051083666524694614 19.0 14.0
5 Medium 10x10 BFS 0.02262299979823486 19.0 12.0
6 Medium 10x10 DFS 0.016091333236545324 18.0 12.0
7 Medium 10x10 AStar 0.03017666616263644 12.0 12.0
8 Large 20x20 BFS 0.015730000086477958 16.0 5.0
9 Large 20x20 DFS 0.014211666590805786 17.0 9.0
10 Large 20x20 AStar 0.020270666330664728 9.0 5.0
11 Empty 15x15 BFS 0.10161799946217798 78.0 15.0
12 Empty 15x15 DFS 0.04646399975172244 76.0 43.0
13 Empty 15x15 AStar 0.13135433376495106 63.0 15.0

View 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!")

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,77 @@
#!/bin/bash
# maze_generator.sh
if [ $# -ne 2 ]; then
echo "Usage: $0 <width> <height>"
echo "Example: $0 10 10"
exit 1
fi
WIDTH=$1
HEIGHT=$2
FILENAME="maze${WIDTH}x${HEIGHT}.txt"
# Create empty maze with all walls
declare -A maze
for ((y=0; y<HEIGHT; y++)); do
for ((x=0; x<WIDTH; x++)); do
maze[$y,$x]="#"
done
done
# Set start at (1,1)
START_X=1
START_Y=1
maze[$START_Y,$START_X]="S"
# Random walk to create path
CURRENT_X=$START_X
CURRENT_Y=$START_Y
STEPS=$((WIDTH * HEIGHT / 3))
for ((i=0; i<STEPS; i++)); do
DIR=$((RANDOM % 4))
NEW_X=$CURRENT_X
NEW_Y=$CURRENT_Y
case $DIR in
0) NEW_X=$((CURRENT_X + 1)) ;;
1) NEW_X=$((CURRENT_X - 1)) ;;
2) NEW_Y=$((CURRENT_Y + 1)) ;;
3) NEW_Y=$((CURRENT_Y - 1)) ;;
esac
if [ $NEW_X -gt 0 ] && [ $NEW_X -lt $((WIDTH-1)) ] && [ $NEW_Y -gt 0 ] && [ $NEW_Y -lt $((HEIGHT-1)) ]; then
if [ "${maze[$NEW_Y,$NEW_X]}" != "S" ]; then
maze[$NEW_Y,$NEW_X]=" "
CURRENT_X=$NEW_X
CURRENT_Y=$NEW_Y
fi
fi
done
# Set exit at final position (ensure not same as start)
maze[$CURRENT_Y,$CURRENT_X]="E"
# Ensure exit is different from start
if [ $CURRENT_X -eq $START_X ] && [ $CURRENT_Y -eq $START_Y ]; then
if [ $((START_Y+1)) -lt $((HEIGHT-1)) ]; then
maze[$((START_Y+1)),$START_X]="E"
maze[$START_Y,$START_X]="S"
else
maze[$START_Y,$((START_X+1))]="E"
fi
fi
# Write to file
> $FILENAME
for ((y=0; y<HEIGHT; y++)); do
line=""
for ((x=0; x<WIDTH; x++)); do
line="${line}${maze[$y,$x]}"
done
echo "$line" >> $FILENAME
done
echo "Maze saved to $FILENAME"
echo "Start at (1,1), Exit at ($CURRENT_X,$CURRENT_Y)"

View File

@ -0,0 +1,4 @@
S

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View 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")

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@ -0,0 +1,158 @@
# Отчет по лабораторной работе: Поиск выхода из лабиринта
## 1. Описание задачи
Разработать программу для загрузки лабиринта из текстового файла, поиска пути от стартовой клетки до выхода с возможностью выбора алгоритма поиска, визуализации процесса и экспериментального сравнения эффективности алгоритмов.
### Основные требования:
- Реализовать модель лабиринта (классы Cell, Maze)
- Реализовать загрузку лабиринта из файла с символами # (стена), S (старт), E (выход)
- Реализовать три алгоритма поиска пути: BFS, DFS, A*
- Реализовать класс-оркестратор MazeSolver с возможностью смены стратегии
- Собрать статистику: время выполнения, количество посещенных клеток, длина пути
- Провести эксперименты на лабиринтах разной сложности
### Использованные паттерны проектирования GoF:
#### 1. Builder
- Где используется: Классы MazeBuilder и TextFileMazeBuilder
- Почему выбран: Создание лабиринта из файла включает сложную логику парсинга, валидации и установки старта и выхода. Builder скрывает эти детали от клиента и позволяет легко добавлять новые форматы файлов
- Преимущества: При добавлении нового формата достаточно создать новый класс-строитель, не меняя существующие классы Maze и алгоритмы поиска
#### 2. Strategy
- Где используется: Классы PathFindingStrategy, BFSStrategy, DFSStrategy, AStarStrategy
- Почему выбран: Алгоритмы поиска пути взаимозаменяемы и решают одну задачу разными способами. Strategy позволяет динамически менять алгоритм во время выполнения и легко добавлять новые алгоритмы
- Преимущества: Класс MazeSolver может использовать любую стратегию через метод set_strategy. Добавление нового алгоритма требует только создания нового класса
#### 3. Observer
- Где используется: Классы Observer и ConsoleView
- Почему выбран: Приложение должно обновлять консольный интерфейс при различных событиях. Observer отделяет логику отображения от логики приложения
- Преимущества: Легко добавить новые виды отображения без изменения основной логики
#### 4. Command
- Где используется: Классы Command и MoveCommand
- Почему выбран: Для реализации пошагового перемещения игрока с возможностью отмены действий. Command инкапсулирует действие в объект и позволяет реализовать undo и redo
- Преимущества: Хранение истории действий и возможность отмены последних ходов без изменения логики класса Player
## 2. Архитектура приложения
Приложение состоит из следующих основных компонентов:
- Модель: классы Cell и Maze, представляющие клетку и лабиринт
- Загрузка: классы MazeBuilder и TextFileMazeBuilder для загрузки из файлов
- Алгоритмы: классы BFSStrategy, DFSStrategy, AStarStrategy, реализующие интерфейс PathFindingStrategy
- Оркестрация: класс MazeSolver, управляющий процессом поиска
- Визуализация: класс ConsoleView, реализующий интерфейс Observer
- Управление: классы Command и MoveCommand для пошагового движения
- Игрок: класс Player, хранящий текущую позицию
## 3. Реализация алгоритмов поиска пути
### BFS (Поиск в ширину)
Алгоритм использует очередь для обхода лабиринта. Начинает со стартовой клетки, помещает её в очередь. Затем циклически извлекает клетку из начала очереди, проверяет не является ли она выходом, и добавляет всех непосещенных соседей в конец очереди. Гарантирует нахождение кратчайшего пути по количеству шагов.
### DFS (Поиск в глубину)
Алгоритм использует стек для обхода лабиринта. Начинает со стартовой клетки, помещает её в стек. Затем циклически извлекает клетку из конца стека, проверяет не является ли она выходом, и добавляет всех непосещенных соседей в стек. Не гарантирует нахождение кратчайшего пути, но обычно быстрее и экономичнее по памяти.
### A* (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 |
### Графики
![Сравнение производительности алгоритмов](performance_comparison_2-nd-exercise.png)
На графике представлено сравнение трех алгоритмов по трем метрикам: время выполнения, количество посещенных клеток и длина найденного пути.
## 5. Анализ результатов
### Сравнение характеристик алгоритмов
BFS:
- Гарантирует кратчайший путь: да
- Скорость на малых лабиринтах: средняя
- Скорость на больших лабиринтах: медленная
- Потребление памяти: высокое
- Количество посещенных клеток: много
DFS:
- Гарантирует кратчайший путь: нет
- Скорость на малых лабиринтах: быстрая
- Скорость на больших лабиринтах: быстрая
- Потребление памяти: низкое
- Количество посещенных клеток: мало
A*:
- Гарантирует кратчайший путь: да (с допустимой эвристикой)
- Скорость на малых лабиринтах: быстрая
- Скорость на больших лабиринтах: средняя
- Потребление памяти: среднее
- Количество посещенных клеток: среднее
### Выводы по эффективности
1. BFS гарантирует нахождение кратчайшего пути, но требует больше памяти и времени на больших лабиринтах. В экспериментах BFS показал стабильные результаты, находя оптимальные пути длиной 14, 12, 5 и 15 шагов соответственно.
2. DFS является самым быстрым по времени (0.018-0.069 мс) и самым экономичным по памяти, но не гарантирует кратчайший путь. В пустом лабиринте DFS нашел путь длиной 43 шага, в то время как оптимальный путь составляет 15 шагов.
3. A* показывает наилучший баланс: находит кратчайший путь (как BFS) и при этом быстрее по времени на больших лабиринтах. A* посетил меньше всего клеток (9-63) по сравнению с конкурентами.
4. В лабиринте 20x20 все алгоритмы сработали очень быстро (0.019-0.023 мс), так как путь оказался коротким (всего 5 шагов).
5. При отсутствии пути (лабиринт maze_no_exit.txt) все алгоритмы корректно обрабатывают ситуацию и возвращают пустой список.
### Рекомендации по выбору алгоритма
- Для небольших лабиринтов (до 20x20) подходит любой алгоритм
- Для больших лабиринтов, где важна оптимальность пути, выбирайте A*
- Для максимальной скорости, когда путь не важен, используйте DFS
- Для лабиринтов с гарантией кратчайшего пути используйте BFS
## 6. Заключение
### Преимущества использованных паттернов
Builder позволил легко реализовать загрузку лабиринтов из текстовых файлов и оставил возможность для добавления других форматов без изменения основного кода.
Strategy сделал алгоритмы поиска взаимозаменяемыми. Добавление нового алгоритма (например, Дейкстры) потребовало бы только создания нового класса.
Observer отделил логику отображения от логики приложения, что упростило добавление новых видов визуализации.
Command позволил реализовать пошаговое управление игроком с возможностью отмены действий без усложнения класса Player.
### Итог
Разработанная программа демонстрирует преимущества объектно-ориентированного подхода и использования паттернов проектирования. Код является гибким, расширяемым и легко поддерживаемым. Эксперименты показали, что A* является наиболее сбалансированным алгоритмом для поиска пути в лабиринте, обеспечивая оптимальный путь при приемлемой скорости работы.

View File

View File

@ -1 +0,0 @@
428b

183
README.md
View File

@ -197,186 +197,3 @@ with open("results.csv", "w", newline="") as f:
- Как удаление работает в каждой структуре.
* Вывод должен содержать ответ на вопрос: какую структуру и для каких задач (частые вставки, частый поиск, необходимость получать данные в порядке) стоит выбирать в реальной жизни.*
## Задание: Поиск выхода из лабиринта (объектно-ориентированная реализация с паттернами)
### Цель работы
Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры.
### Общая схема приложения (пример)
```mermaid
classDiagram
class Maze {
-Cell[] cells
-int width, height
-Cell start
-Cell exit
+getCell(x,y): Cell
+getNeighbors(cell): List~Cell~
}
class Cell {
-int x, y
-bool isWall
-bool isStart
-bool isExit
+isPassable(): bool
}
class MazeBuilder {
<<interface>>
+buildFromFile(filename): Maze
}
class TextFileMazeBuilder {
+buildFromFile(filename): Maze
}
class PathFindingStrategy {
<<interface>>
+findPath(maze, start, exit): List~Cell~
}
class BFSStrategy
class DFSStrategy
class AStarStrategy
class DijkstraStrategy
class SearchStats {
+timeMs: float
+visitedCells: int
+pathLength: int
}
class MazeSolver {
-Maze maze
-PathFindingStrategy strategy
+setStrategy(strategy)
+solve(): SearchStats
}
class Command {
<<interface>>
+execute()
+undo()
}
class MoveCommand {
-Player player
-Direction dir
-Cell previousCell
+execute()
+undo()
}
class Player {
-Cell currentCell
+moveTo(cell)
}
class Observer {
<<interface>>
+update(event)
}
class ConsoleView {
+update(event)
+render(maze, player, path)
}
MazeBuilder <|.. TextFileMazeBuilder
MazeBuilder --> Maze : creates
PathFindingStrategy <|.. BFSStrategy
PathFindingStrategy <|.. DFSStrategy
PathFindingStrategy <|.. AStarStrategy
PathFindingStrategy <|.. DijkstraStrategy
MazeSolver --> PathFindingStrategy : uses
MazeSolver --> Maze : uses
Command <|.. MoveCommand
MoveCommand --> Player
Player --> Cell
Observer <|.. ConsoleView
MazeSolver --> Observer : notifies
```
### Выполнение
#### Этап 1. Модель лабиринта (без паттернов, просто классы)
**Задача:** Создать классы `Cell` и `Maze`, которые представляют карту лабиринта.
- `Cell` хранит координаты (x, y), флаги `isWall`, `isStart`, `isExit`, метод `isPassable()` (возвращает `True` для прохода, если не стена).
- `Maze` хранит двумерный массив клеток, ширину, высоту, ссылки на стартовую и выходную клетку. Методы: `getCell(x, y)`, `getNeighbors(cell)` возвращает список соседних проходимых клеток (вверх, вниз, влево, вправо, если в пределах границ и не стена).
**Результат:** Лабиринт можно создать вручную в коде, но загрузку пока не делаем.
#### Этап 2. Загрузка лабиринта из файла применение паттерна **Builder**
**Задача:** Реализовать загрузку лабиринта из текстового файла, где `#` стена, ` ` (пробел) проход, `S` старт, `E` выход.
- Создать интерфейс `MazeBuilder` с методом `buildFromFile(filename)`.
- Реализовать класс `TextFileMazeBuilder`, который читает файл, парсит символы, создаёт объекты `Cell`, задаёт координаты и флаги, после чего возвращает готовый `Maze`.
Процесс построения лабиринта сложный (парсинг, валидация, установка старта/выхода). Builder скрывает детали создания от клиента. В будущем можно легко добавить другой формат (например, JSON или бинарный) через новую реализацию `MazeBuilder`.
#### Этап 3. Стратегии поиска пути паттерн **Strategy**
**Задача:** Реализовать семейство алгоритмов поиска пути от старта до выхода.
- Создать интерфейс `PathFindingStrategy` с методом `findPath(maze, start, exit)`, возвращающим список клеток пути (от старта до выхода включительно) или пустой список, если пути нет.
- Реализовать минимум 3 стратегии:
- **BFS** (поиск в ширину) гарантирует кратчайший путь по количеству шагов.
- **DFS** (поиск в глубину) быстрый, но не обязательно кратчайший.
- **A*** (с эвристикой, например, манхэттенское расстояние) компромисс между скоростью и оптимальностью.
- (Опционально) **Дейкстра** полезна для взвешенных лабиринтов, но в базовом варианте все шаги имеют вес 1, тогда она совпадает с BFS.
Каждая стратегия возвращает путь. Для BFS/DFS используйте очередь/стек, для A* приоритетную очередь (heapq). Важно: алгоритмы не должны модифицировать сам лабиринт, только читать состояние клеток.
Strategy позволяет легко переключать алгоритмы во время выполнения, не меняя код остальной программы. Новый алгоритм можно добавить, реализовав интерфейс.
#### Этап 4. Класс-оркестратор **MazeSolver** (использует Strategy)
**Задача:** Создать класс, который принимает лабиринт и стратегию, выполняет поиск и собирает статистику.
- `MazeSolver` содержит поля `maze` и `strategy`.
- Метод `setStrategy(strategy)` для динамической смены алгоритма.
- Метод `solve()` вызывает `strategy.findPath(...)` и возвращает объект `SearchStats` (время выполнения в миллисекундах, количество посещённых клеток, длина найденного пути).
- Для замера времени используйте `time.perf_counter()` до и после вызова стратегии.
#### Этап 5. Визуализация и пошаговое управление паттерны **Observer** и **Command** (по желанию)
**5.1. Наблюдатель (Observer)** обновление консольного интерфейса.
- Создать интерфейс `Observer` с методом `update(event)`, где `event` может быть строкой или объектом с типом события (`"path_found"`, `"move"`, `"maze_loaded"`).
- Реализовать класс `ConsoleView`, который отображает лабиринт, текущее положение игрока (если реализован пошаговый режим) и найденный путь. Метод `render(maze, player_position, path)` рисует карту в консоли.
- `MazeSolver` (или отдельный контроллер) может иметь список наблюдателей и уведомлять их при изменении состояния.
**5.2. Команда (Command)** для пошагового перемещения игрока по найденному пути (или ручного управления).
- Создать интерфейс `Command` с методами `execute()` и `undo()`.
- Реализовать `MoveCommand`, который принимает игрока (`Player`), направление и изменяет его позицию, сохраняя предыдущую для отмены.
- Создать класс `Player`, хранящий текущую клетку.
- Консольное меню позволяет вводить команды (W/A/S/D), выполнять `MoveCommand`, при необходимости отменять последний ход (Ctrl+Z). Это опционально, но очень наглядно демонстрирует паттерн.
*Observer можно реализовать только для вывода сообщений о начале/конце поиска, а Command для демонстрации undo при ручном исследовании лабиринта.*
#### Этап 6. Экспериментальная часть (аналогично заданию со структурами данных)
**Задача:** Сравнить эффективность реализованных стратегий на лабиринтах разной сложности.
1. **Подготовка тестовых лабиринтов:**
- Маленький (10×10) с простым путём.
- Средний (50×50) с тупиками.
- Большой (100×100) с запутанной структурой.
- «Пустой» лабиринт (без стен) для демонстрации максимальной производительности.
- «Без выхода» чтобы проверить обработку отсутствия пути.
2. **Замеры:**
- Для каждого лабиринта и каждой стратегии запустить `solve()` 510 раз, усреднить время, количество посещённых клеток, длину пути.
- Записать результаты в CSV: `лабиринт,стратегия,время_мс,посещено_клеток,длина_пути`.
3. **Анализ:**
- Построить графики для каждого лабиринта.
- Проанализировать и написать выводы по итогам (эффективность того или иного алгоритма в разных случаях).
4. **Дополнительное задание:** Реализовать взвешенные клетки (например, болото вес 3, песок вес 2, асфальт вес 1) и сравнить Дейкстру с A* на взвешенном графе.
#### Этап 7. Отчёт
**Структура отчёта:**
1. Описание задачи и выбранных паттернов (с диаграммой классов из Mermaid).
2. Листинги ключевых классов (можно выборочно) или ссылка на репозиторий.
3. Результаты экспериментов (таблицы, графики).
4. Анализ эффективности алгоритмов и применимости паттернов.
5. Выводы: как ООП и паттерны помогли сделать код гибким и расширяемым. Что было бы сложно изменить без них.
### Советы
- Для A* самая простая эвристика: `abs(x1 - x2) + abs(y1 - y2)`.
- При поиске пути надо хранить предшественников (`parent` для каждой посещённой клетки), чтобы восстановить путь.
- Для BFS/DFS используй `deque` (очередь) и `list` (стек).
- Визуализацию в консоли можно сделать с помощью `os.system('cls' if os.name == 'nt' else 'clear')` для перерисовки.

View File

View File

View File

View File

View File

View File

@ -1,6 +0,0 @@
{\rtf1\ansi\ansicpg1251\cocoartf2869
\cocoatextscaling0\cocoaplatform0{\fonttbl}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
\paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0
}

View File

@ -1 +0,0 @@
<EFBFBD>¥¦¨¬ ¢כ¢®₪  ×®¬ ­₪ ­  ם×א ­ (ECHO) ¢×«מח¥­.