solver, observer and movement

This commit is contained in:
gutovvm 2026-05-26 22:18:10 +03:00
parent 7e723606cb
commit 2360b42a88

View File

@ -2,6 +2,9 @@ import numpy as np
import abc
from collections import deque
import heapq
import time
import os
import keyboard
#Классы клетки и лабиринта
@ -51,33 +54,33 @@ class Maze:
#Тестирование классов клетки и лабиринта
cell1 = Cell((1,2), isExit = True, isWall = True, isStart = False)
# cell1 = Cell((1,2), isExit = True, isWall = True, isStart = False)
print(cell1.isPassable())
print(cell1.isStart)
print(cell1.coords)
# print(cell1.isPassable())
# print(cell1.isStart)
# print(cell1.coords)
width, height = 3,3
# width, height = 3,3
cells = np.full((width,height), None, dtype=object)
# cells = np.full((width,height), None, dtype=object)
for x in range(width):
for y in range(height):
if x != 0 and x != width-1 and y != 0 and y != height-1:
cells[x][y] = Cell((x,y), isWall = False)
else:
cells[x][y] = Cell((x,y), isWall = True)
# for x in range(width):
# for y in range(height):
# if x != 0 and x != width-1 and y != 0 and y != height-1:
# cells[x][y] = Cell((x,y), isWall = False)
# else:
# cells[x][y] = Cell((x,y), isWall = True)
print(cells)
# print(cells)
maze1 = Maze(cells, width, height, cells[0], cells[-1])
# maze1 = Maze(cells, width, height, cells[0], cells[-1])
for column in cells:
for cell in column:
print(cell.coords)
print(maze1.getNeighbors(cell))
# for column in cells:
# for cell in column:
# print(cell.coords)
# print(maze1.getNeighbors(cell))
print('\n')
# print('\n')
#Интерфейс постройки лабиринта
@ -95,7 +98,7 @@ class TextFileMazeBuilder(MazeBuilder):
rows = file.read().splitlines()
print(rows)
#print(rows)
width = 0
height = 0
@ -104,13 +107,15 @@ class TextFileMazeBuilder(MazeBuilder):
if len(row) > width:
width = len(row)
print(width, height)
#print(width, height)
st = (0,0)
ex = (width,height)
cells = np.full((width,height), None, dtype=object)
flagst = False
flagex = False
for y in range(height):
for x in range(width):
isWall = False
@ -122,24 +127,30 @@ class TextFileMazeBuilder(MazeBuilder):
elif rows[-(y+1)][x] == 'S':
isStart = True
st = (x,y)
print('Старт в',x,y)
flagst = True
#print('Старт в',x,y)
elif rows[-(y+1)][x] == 'E':
isExit = True
ex = (x,y)
print('Выход в',x,y)
flagex = True
#print('Выход в',x,y)
elif rows[-(y+1)][x] != ' ':
raise ValueError("Неверный формат лабиринта! Пожалуйста, используйте только символы #,S,E и пробелы")
cells[x][y] = Cell((x,y), isWall, isStart, isExit)
return Maze(cells, width, height, cells[st[0]][st[1]], cells[ex[0]][ex[1]])
if flagst and flagex:
return Maze(cells, width, height, cells[st[0]][st[1]], cells[ex[0]][ex[1]])
raise ValueError('В лабаиринте должны быть вход и выход (S и E)')
builder = TextFileMazeBuilder
# builder = TextFileMazeBuilder
maze = builder.buildFromFile('maze1.txt')
# maze = builder.buildFromFile('maze1.txt')
print(maze)
# print(maze)
#Интерфейс поиска пути
@ -147,17 +158,17 @@ print(maze)
class PathFindingStrategy(abc.ABC):
@abc.abstractmethod
def findPath(maze, st, ex): pass
def findPath(self, maze, st, ex): pass
#Поиск в глубину
class DFS(PathFindingStrategy):
def findPath(maze,st,ex):
def findPath(self,maze,st,ex):
stack = [st]
visited = {st.coords} #по координатам надёжнее, а то вдруг адрес изменится
self.visited = {st.coords} #по координатам надёжнее, а то вдруг адрес изменится
pathmap = {}
@ -176,27 +187,27 @@ class DFS(PathFindingStrategy):
return path
for n in maze.getNeighbors(cell):
if n != None and n.coords not in visited:
visited.add(n.coords)
if n != None and n.coords not in self.visited:
self.visited.add(n.coords)
pathmap[n.coords] = cell
stack.append(n)
return None
path = DFS.findPath(maze,maze.st,maze.ex)
# path = DFS().findPath(maze,maze.st,maze.ex)
print('путь поиском в глубину:')
for cell in path:
print(cell.coords)
# print('путь поиском в глубину:')
# for cell in path:
# print(cell.coords)
class BFS(PathFindingStrategy):
def findPath(maze,st,ex):
def findPath(self,maze,st,ex):
queue = deque([st])
visited = {st.coords} #по координатам надёжнее, а то вдруг адрес изменится
self.visited = {st.coords} #по координатам надёжнее, а то вдруг адрес изменится
pathmap = {}
@ -215,27 +226,27 @@ class BFS(PathFindingStrategy):
return path
for n in maze.getNeighbors(cell):
if n != None and n.coords not in visited:
visited.add(n.coords)
if n != None and n.coords not in self.visited:
self.visited.add(n.coords)
pathmap[n.coords] = cell
queue.append(n)
return None
path = BFS.findPath(maze,maze.st,maze.ex)
# path = BFS().findPath(maze,maze.st,maze.ex)
print('путь поиском в ширину:')
for cell in path:
print(cell.coords)
# print('путь поиском в ширину:')
# for cell in path:
# print(cell.coords)
class Astar(PathFindingStrategy):
def findPath(maze,st,ex):
def findPath(self,maze,st,ex):
c = 0
hp_queue = [(0,c,st)]
g_score = {st.coords: 0}
self.g_score = {st.coords: 0}
pathmap = {}
@ -254,14 +265,15 @@ class Astar(PathFindingStrategy):
cell = pathmap[cell.coords]
path.append(st)
path = path[::-1]
self.visited = self.g_score #экий костыль
return path
for n in maze.getNeighbors(cell):
new_g_score = g_score[cell.coords] + 1
new_g_score = self.g_score[cell.coords] + 1
if n is not None and new_g_score < g_score.get(n.coords, float('inf')):
if n is not None and new_g_score < self.g_score.get(n.coords, float('inf')):
pathmap[n.coords] = cell
g_score[n.coords] = new_g_score
self.g_score[n.coords] = new_g_score
h_score = abs(n.coords[0]-ex.coords[0]) + abs(n.coords[1]-ex.coords[1])
@ -274,10 +286,268 @@ class Astar(PathFindingStrategy):
heapq.heappush(hp_queue, (full_score, c, n))
hp_queue_coords.add(n.coords)
self.visited = self.g_score #экий костыль 2: возвращение ситхов
return None
path = Astar.findPath(maze,maze.st,maze.ex)
# path = Astar().findPath(maze,maze.st,maze.ex)
# print('путь с A*:')
# for cell in path:
# print(cell.coords)
#Класс статистики поиска пути и класс оркестратор
class SearchStats():
def __init__(self, timeMs, visitedCells, pathLength):
self.timeMs = timeMs
self.visitedCells = visitedCells
self.pathLength = pathLength
class MazeSolver():
def __init__(self, maze, strategy):
self.maze = maze
self.strategy = strategy
self.observers = [ConsoleView(maze)]
for observer in self.observers:
observer.update(MazeEvent('maze_loaded',maze,maze.st.coords))
def setStrategy(self,strategy):
self.strategy = strategy
def solve(self):
start = time.perf_counter()
path = self.strategy.findPath(self.maze,self.maze.st,self.maze.ex)
end = time.perf_counter()
elapsed = end - start
visitedCells = len(self.strategy.visited)
pathLength = len(path)
for observer in self.observers:
observer.update(MazeEvent('path_found',self.maze,path[-1].coords,path))
return SearchStats(elapsed, visitedCells, pathLength)
# MS = MazeSolver(maze, DFS())
# Stats = MS.solve()
# print(Stats.timeMs)
# print(Stats.visitedCells)
# print(Stats.pathLength)
# MS = MazeSolver(maze, BFS())
# Stats = MS.solve()
# print(Stats.timeMs)
# print(Stats.visitedCells)
# print(Stats.pathLength)
# MS = MazeSolver(maze, Astar())
# Stats = MS.solve()
# print(Stats.timeMs)
# print(Stats.visitedCells)
# print(Stats.pathLength)
#Класс для событий
class MazeEvent():
def __init__(self,event_type, maze, player_position = None, path = []):
if player_position is None:
player_position = maze.st.coords
self.event_type = event_type
self.maze = maze
self.player_position = player_position
self.path = path
#Интерфейс наблюдатель
class Observer(abc.ABC):
@abc.abstractmethod
def update(self, event):
if not isinstance(event, (str, MazeEvent)):
raise TypeError('Только строки и объекты события')
elif isinstance(event, MazeEvent) and event.event_type not in ('path_found','move','maze_loaded'):
raise TypeError('Только события "path_found","move","maze_loaded"')
#Класс консольного просмотра
class ConsoleView(Observer):
def __init__(self, maze, player_position = (0,0), path = []):
self.maze = maze
self.player_position = player_position
self.path = path
def update(self, event):
super().update(event) #проверка через сам интерфейс
if isinstance(event, str):
print('')
print(event+'\n')
self.render(self.maze, self.player_position, self.path)
else:
print('')
print(event.event_type+'\n')
if event.player_position is not None:
self.player_position = event.player_position
if event.path is not None and event.path:
self.path = event.path
self.render(event.maze, self.player_position, self.path)
def render(self, maze, player_position, path):
os.system('cls' if os.name == 'nt' else 'clear')
#из-за системы координат надо всё опять транспонировать
res = []
for row in maze.cells.T[::-1]:
subres = []
for cell in row:
if cell.isWall:
subres += '#'
elif cell.isStart:
subres += 'S'
elif cell.isExit:
subres += 'E'
else:
subres += ' '
res.append(subres)
for cell in path:
x,y = cell.coords
if res[-(y+1)][x] != 'S':
res[-(y+1)][x] = '*'
res[-(player_position[1]+1)][player_position[0]] = 'X'
for row in res:
print(''.join(row))
# builder = TextFileMazeBuilder
# maze = builder.buildFromFile('maze1.txt')
# print(maze)
# CV = ConsoleView(maze, (0,0))
# CV.update('Что-то случилось')
# ME = MazeEvent('maze_loaded', maze, (0,0))
# CV.update(ME)
# CV.update('Что-то случилось')
#Интерфейс для команд
class Command(abc.ABC):
@abc.abstractmethod
def execute(self): pass
@abc.abstractmethod
def undo(self): pass
#Класс команды движения
class MoveCommand(Command):
def __init__(self):
self.previousCell = (0,0)
def execute(self,player,direction):
self.previousCell = player.currentCell
resCell = (self.previousCell[0]+direction.dir[0],self.previousCell[1]+direction.dir[1])
player.moveTo(resCell)
def undo(self,player):
player.moveTo(self.previousCell)
#Класс игрока
class Player():
#Он хранит не текущую клетку, а только её координаты. Поскольку
#нам надо перемещать игрока динамически, а команда для перемещения
#не принимает лабиринт в качестве аргумента, следующую клетку мы
#как объект получить не можем, а можем получить только её координаты.
def __init__(self, currentCell):
self.currentCell = currentCell
def moveTo(self, cell):
self.currentCell = cell
#Класс направление
class Direction():
def __init__(self, x,y):
self.dir = (x,y)
builder = TextFileMazeBuilder
maze = builder.buildFromFile('maze1.txt')
MS = MazeSolver(maze, DFS())
MS.solve()
MC = MoveCommand()
CV = MS.observers[0]
player1 = Player(CV.player_position)
instruct = '\nПеремещайтесь на W/A/S/D. Для отмены используйте ctrl+Z. Для выхода из режима перемещения команда X.\n'
def move(player, direction):
resCoords = (player.currentCell[0]+direction.dir[0], player.currentCell[1]+direction.dir[1])
resCell = maze.getCell(resCoords[0], resCoords[1])
if resCell == None or resCell.isWall:
return
MC.execute(player, direction)
CV.update(MazeEvent('move', maze, player.currentCell))
print(instruct)
def undo(player):
MC.undo(player)
CV.update(MazeEvent('move', maze, player.currentCell))
print(instruct)
keyboard.add_hotkey('w', move, args=[player1, Direction(0,1)])
keyboard.add_hotkey('s', move, args=[player1, Direction(0,-1)])
keyboard.add_hotkey('a', move, args=[player1, Direction(-1,0)])
keyboard.add_hotkey('d', move, args=[player1, Direction(1,0)])
keyboard.add_hotkey('ctrl+z', undo, args=[player1])
keyboard.wait('x')
keyboard.unhook_all()
print('путь с A*:')
for cell in path:
print(cell.coords)