Merge pull request '[2] lab2' (#238) from shekurovaa/2026-rff_mp:zad2 into develop
Reviewed-on: #238
This commit is contained in:
commit
b54e87ad1c
BIN
shekurovaa/2/docs/Report.docx
Normal file
BIN
shekurovaa/2/docs/Report.docx
Normal file
Binary file not shown.
46
shekurovaa/2/docs/data/builder.py
Normal file
46
shekurovaa/2/docs/data/builder.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
from model import Cell, Maze
|
||||
|
||||
class MazeBuilder:
|
||||
def buildFromFile(self, filename: str) -> Maze:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TextFileMazeBuilder(MazeBuilder):
|
||||
def buildFromFile(self, filename: str) -> Maze:
|
||||
with open(filename, "r", encoding="utf-8") as f:
|
||||
raw_lines = [line.rstrip("\n") for line in f if line.strip("\n") != ""]
|
||||
|
||||
width = max(len(line) for line in raw_lines)
|
||||
grid = []
|
||||
|
||||
start_count = 0
|
||||
exit_count = 0
|
||||
|
||||
for y, line in enumerate(raw_lines):
|
||||
row = []
|
||||
padded = line.ljust(width)
|
||||
for x, ch in enumerate(padded):
|
||||
if ch == "#":
|
||||
row.append(Cell(x, y, isWall=True))
|
||||
elif ch == "S":
|
||||
row.append(Cell(x, y, isStart=True))
|
||||
start_count += 1
|
||||
elif ch == "E":
|
||||
row.append(Cell(x, y, isExit=True))
|
||||
exit_count += 1
|
||||
elif ch == "1":
|
||||
row.append(Cell(x, y, weight=1))
|
||||
elif ch == "2":
|
||||
row.append(Cell(x, y, weight=2))
|
||||
elif ch == "3":
|
||||
row.append(Cell(x, y, weight=3))
|
||||
else:
|
||||
row.append(Cell(x, y))
|
||||
grid.append(row)
|
||||
|
||||
maze = Maze(grid)
|
||||
|
||||
if start_count != 1 or exit_count != 1:
|
||||
raise ValueError("В лабиринте должен быть ровно один S и один E")
|
||||
|
||||
return maze
|
||||
44
shekurovaa/2/docs/data/command.py
Normal file
44
shekurovaa/2/docs/data/command.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
class Command:
|
||||
def execute(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def undo(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Player:
|
||||
def __init__(self, position):
|
||||
self.position = position
|
||||
|
||||
|
||||
class MoveCommand(Command):
|
||||
DIRS = {
|
||||
"W": (0, -1),
|
||||
"S": (0, 1),
|
||||
"A": (-1, 0),
|
||||
"D": (1, 0),
|
||||
}
|
||||
|
||||
def __init__(self, maze, player, direction):
|
||||
self.maze = maze
|
||||
self.player = player
|
||||
self.direction = direction.upper()
|
||||
self.prev_position = None
|
||||
|
||||
def execute(self):
|
||||
if self.direction not in self.DIRS:
|
||||
return False
|
||||
dx, dy = self.DIRS[self.direction]
|
||||
current = self.player.position
|
||||
nxt = self.maze.getCell(current.x + dx, current.y + dy)
|
||||
if nxt is None or not nxt.isPassable():
|
||||
return False
|
||||
self.prev_position = current
|
||||
self.player.position = nxt
|
||||
return True
|
||||
|
||||
def undo(self):
|
||||
if self.prev_position is not None:
|
||||
self.player.position = self.prev_position
|
||||
return True
|
||||
return False
|
||||
30
shekurovaa/2/docs/data/experiments.py
Normal file
30
shekurovaa/2/docs/data/experiments.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import csv
|
||||
from statistics import mean
|
||||
|
||||
def run_experiments(maze_files, strategies, runs=5, out_csv="output/results.csv"):
|
||||
rows = []
|
||||
for maze_name, maze in maze_files.items():
|
||||
for strat_name, strat_cls in strategies.items():
|
||||
times = []
|
||||
visiteds = []
|
||||
lengths = []
|
||||
for _ in range(runs):
|
||||
solver = maze["solver_factory"](strat_cls())
|
||||
stats = solver.solve()
|
||||
times.append(stats.timeMs)
|
||||
visiteds.append(stats.visitedCells)
|
||||
lengths.append(stats.pathLength)
|
||||
rows.append({
|
||||
"maze": maze_name,
|
||||
"strategy": strat_name,
|
||||
"time_ms": round(mean(times), 3),
|
||||
"visited_cells": round(mean(visiteds), 1),
|
||||
"path_length": round(mean(lengths), 1)
|
||||
})
|
||||
|
||||
with open(out_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(rows)
|
||||
|
||||
return rows
|
||||
33
shekurovaa/2/docs/data/main.py
Normal file
33
shekurovaa/2/docs/data/main.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from builder import TextFileMazeBuilder
|
||||
from strategies import BFSStrategy, DFSStrategy, AStarStrategy
|
||||
from solver import MazeSolver
|
||||
from observer import ConsoleView
|
||||
from command import Player, MoveCommand
|
||||
|
||||
def main():
|
||||
builder = TextFileMazeBuilder()
|
||||
maze = builder.buildFromFile("mazes/small.txt")
|
||||
|
||||
console = ConsoleView()
|
||||
console.update({"type": "message", "text": "Лабиринт загружен:"})
|
||||
console.update({"type": "render", "maze": maze})
|
||||
|
||||
strategies = {
|
||||
"BFS": BFSStrategy(),
|
||||
"DFS": DFSStrategy(),
|
||||
"A*": AStarStrategy()
|
||||
}
|
||||
|
||||
for name, strat in strategies.items():
|
||||
solver = MazeSolver(maze, strat)
|
||||
stats = solver.solve()
|
||||
print(f"{name}: time={stats.timeMs:.3f} ms, visited={stats.visitedCells}, path={stats.pathLength}")
|
||||
console.update({"type": "render", "maze": maze, "path": stats.path})
|
||||
|
||||
player = Player(maze.start)
|
||||
cmd = MoveCommand(maze, player, "D")
|
||||
cmd.execute()
|
||||
console.update({"type": "render", "maze": maze, "player": player.position})
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
10
shekurovaa/2/docs/data/mazes/small.txt
Normal file
10
shekurovaa/2/docs/data/mazes/small.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
##########
|
||||
#S # #
|
||||
# ## # # #
|
||||
# ## # #
|
||||
# ### #
|
||||
### ## #
|
||||
# # #
|
||||
# # ###E #
|
||||
# #
|
||||
##########
|
||||
67
shekurovaa/2/docs/data/model.py
Normal file
67
shekurovaa/2/docs/data/model.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Cell:
|
||||
x: int
|
||||
y: int
|
||||
isWall: bool = False
|
||||
isStart: bool = False
|
||||
isExit: bool = False
|
||||
weight: int = 1
|
||||
|
||||
def isPassable(self) -> bool:
|
||||
return not self.isWall
|
||||
|
||||
|
||||
class Maze:
|
||||
def __init__(self, grid: List[List[Cell]]):
|
||||
self.grid = grid
|
||||
self.height = len(grid)
|
||||
self.width = len(grid[0]) if self.height else 0
|
||||
self.start: Optional[Cell] = None
|
||||
self.exit: Optional[Cell] = None
|
||||
|
||||
for row in grid:
|
||||
for cell in row:
|
||||
if cell.isStart:
|
||||
self.start = cell
|
||||
if cell.isExit:
|
||||
self.exit = cell
|
||||
|
||||
def getCell(self, x: int, y: int) -> Optional[Cell]:
|
||||
if 0 <= y < self.height and 0 <= x < self.width:
|
||||
return self.grid[y][x]
|
||||
return None
|
||||
|
||||
def getNeighbors(self, cell: Cell) -> List[Cell]:
|
||||
result = []
|
||||
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
|
||||
nxt = self.getCell(cell.x + dx, cell.y + dy)
|
||||
if nxt is not None and nxt.isPassable():
|
||||
result.append(nxt)
|
||||
return result
|
||||
|
||||
def render(self, path=None, player_position=None) -> str:
|
||||
path_set = {(c.x, c.y) for c in path} if path else set()
|
||||
player_xy = (player_position.x, player_position.y) if player_position else None
|
||||
|
||||
lines = []
|
||||
for y in range(self.height):
|
||||
row = []
|
||||
for x in range(self.width):
|
||||
c = self.grid[y][x]
|
||||
if player_xy == (x, y):
|
||||
row.append("P")
|
||||
elif c.isStart:
|
||||
row.append("S")
|
||||
elif c.isExit:
|
||||
row.append("E")
|
||||
elif (x, y) in path_set:
|
||||
row.append(".")
|
||||
elif c.isWall:
|
||||
row.append("#")
|
||||
else:
|
||||
row.append(" ")
|
||||
lines.append("".join(row))
|
||||
return "\n".join(lines)
|
||||
15
shekurovaa/2/docs/data/observer.py
Normal file
15
shekurovaa/2/docs/data/observer.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
class Observer:
|
||||
def update(self, event):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ConsoleView(Observer):
|
||||
def update(self, event):
|
||||
if isinstance(event, dict) and event.get("type") == "message":
|
||||
print(event["text"])
|
||||
elif isinstance(event, dict) and event.get("type") == "render":
|
||||
maze = event["maze"]
|
||||
path = event.get("path")
|
||||
player = event.get("player")
|
||||
print(maze.render(path=path, player_position=player))
|
||||
print()
|
||||
38
shekurovaa/2/docs/data/solver.py
Normal file
38
shekurovaa/2/docs/data/solver.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import time
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class SearchStats:
|
||||
timeMs: float
|
||||
visitedCells: int
|
||||
pathLength: int
|
||||
path: list
|
||||
|
||||
|
||||
class MazeSolver:
|
||||
def __init__(self, maze, strategy):
|
||||
self.maze = maze
|
||||
self.strategy = strategy
|
||||
|
||||
def setStrategy(self, strategy):
|
||||
self.strategy = strategy
|
||||
|
||||
def solve(self) -> SearchStats:
|
||||
if self.maze.start is None or self.maze.exit is None:
|
||||
raise ValueError("Лабиринт должен содержать start и exit")
|
||||
|
||||
t0 = time.perf_counter()
|
||||
result = self.strategy.findPath(self.maze, self.maze.start, self.maze.exit)
|
||||
t1 = time.perf_counter()
|
||||
|
||||
if isinstance(result, tuple):
|
||||
path, visited = result
|
||||
else:
|
||||
path, visited = result, 0
|
||||
|
||||
return SearchStats(
|
||||
timeMs=(t1 - t0) * 1000,
|
||||
visitedCells=visited,
|
||||
pathLength=len(path),
|
||||
path=path
|
||||
)
|
||||
103
shekurovaa/2/docs/data/strategies.py
Normal file
103
shekurovaa/2/docs/data/strategies.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
from collections import deque
|
||||
import heapq
|
||||
from math import inf
|
||||
|
||||
class PathFindingStrategy:
|
||||
def findPath(self, maze, start, exit):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def reconstruct_path(parent, start, goal):
|
||||
if goal not in parent and goal != start:
|
||||
return []
|
||||
path = []
|
||||
cur = goal
|
||||
while cur != start:
|
||||
path.append(cur)
|
||||
cur = parent[cur]
|
||||
path.append(start)
|
||||
path.reverse()
|
||||
return path
|
||||
|
||||
|
||||
class BFSStrategy(PathFindingStrategy):
|
||||
def findPath(self, maze, start, exit):
|
||||
queue = deque([start])
|
||||
visited = {start}
|
||||
parent = {}
|
||||
visited_count = 0
|
||||
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
visited_count += 1
|
||||
if current == exit:
|
||||
path = reconstruct_path(parent, start, exit)
|
||||
return path, visited_count
|
||||
|
||||
for nxt in maze.getNeighbors(current):
|
||||
if nxt not in visited:
|
||||
visited.add(nxt)
|
||||
parent[nxt] = current
|
||||
queue.append(nxt)
|
||||
|
||||
return [], visited_count
|
||||
|
||||
|
||||
class DFSStrategy(PathFindingStrategy):
|
||||
def findPath(self, maze, start, exit):
|
||||
stack = [start]
|
||||
visited = {start}
|
||||
parent = {}
|
||||
visited_count = 0
|
||||
|
||||
while stack:
|
||||
current = stack.pop()
|
||||
visited_count += 1
|
||||
if current == exit:
|
||||
path = reconstruct_path(parent, start, exit)
|
||||
return path, visited_count
|
||||
|
||||
for nxt in maze.getNeighbors(current):
|
||||
if nxt not in visited:
|
||||
visited.add(nxt)
|
||||
parent[nxt] = current
|
||||
stack.append(nxt)
|
||||
|
||||
return [], visited_count
|
||||
|
||||
|
||||
class AStarStrategy(PathFindingStrategy):
|
||||
def h(self, a, b):
|
||||
return abs(a.x - b.x) + abs(a.y - b.y)
|
||||
|
||||
def findPath(self, maze, start, exit):
|
||||
open_heap = []
|
||||
heapq.heappush(open_heap, (0, 0, start))
|
||||
parent = {}
|
||||
g = {start: 0}
|
||||
visited = set()
|
||||
visited_count = 0
|
||||
counter = 1
|
||||
|
||||
while open_heap:
|
||||
_, _, current = heapq.heappop(open_heap)
|
||||
if current in visited:
|
||||
continue
|
||||
|
||||
visited.add(current)
|
||||
visited_count += 1
|
||||
|
||||
if current == exit:
|
||||
path = reconstruct_path(parent, start, exit)
|
||||
return path, visited_count
|
||||
|
||||
for nxt in maze.getNeighbors(current):
|
||||
tentative_g = g[current] + nxt.weight
|
||||
if tentative_g < g.get(nxt, inf):
|
||||
g[nxt] = tentative_g
|
||||
parent[nxt] = current
|
||||
f = tentative_g + self.h(nxt, exit)
|
||||
heapq.heappush(open_heap, (f, counter, nxt))
|
||||
counter += 1
|
||||
|
||||
return [], visited_count
|
||||
BIN
shekurovaa/2/docs/~$Report.docx
Normal file
BIN
shekurovaa/2/docs/~$Report.docx
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user