Реализован паттерн Observer
This commit is contained in:
parent
74928a997a
commit
95805467ab
20
MusinAA/main.py
Normal file
20
MusinAA/main.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from task2.consoleView import ConsoleView
|
||||||
|
from task2.mazeBuilder import TextFileMazeBuilder
|
||||||
|
from task2.mazeSolver import MazeSolver
|
||||||
|
|
||||||
|
from task2.strategyObjects.BFS import BFS
|
||||||
|
from task2.strategyObjects.DFS import DFS
|
||||||
|
from task2.strategyObjects.AStar import AStar
|
||||||
|
|
||||||
|
console = ConsoleView()
|
||||||
|
builder = TextFileMazeBuilder()
|
||||||
|
solver = MazeSolver(BFS())
|
||||||
|
solver.attach(console)
|
||||||
|
|
||||||
|
maze = builder.buildFromFile("task2/mazeExamples/low.txt")
|
||||||
|
console.maze = maze # хмммм
|
||||||
|
solver.setMaze(maze)
|
||||||
|
|
||||||
|
input()
|
||||||
|
|
||||||
|
solver.solve()
|
||||||
94
MusinAA/task2/consoleView.py
Normal file
94
MusinAA/task2/consoleView.py
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
"""
|
||||||
|
Реализовать класс ConsoleView, который отображает лабиринт,
|
||||||
|
текущее положение игрока (если реализован пошаговый режим) и найденный путь.
|
||||||
|
Метод render(maze, player_position, path) рисует карту в консоли."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from task2.mazeObjects.cell import Cell
|
||||||
|
from task2.mazeObjects.maze import Maze
|
||||||
|
from task2.mazeObjects.path import Path
|
||||||
|
from task2.observerSubject import MazeEvent, MazeEventType, Observer
|
||||||
|
|
||||||
|
SBROS = "\033[0m"
|
||||||
|
|
||||||
|
WALL = "#"
|
||||||
|
EXIT = "E"
|
||||||
|
START = "S"
|
||||||
|
PATH_SYMBOL = " "
|
||||||
|
SPACE_SYMBOL = " "
|
||||||
|
|
||||||
|
# Я убрал аргументы из render(), чтобы не передавать их при каждом запуске.
|
||||||
|
# И работать внутри класса приятнее, чем тянуть эти аргументы туда-сюда
|
||||||
|
class ConsoleView(Observer):
|
||||||
|
maze:Maze|None
|
||||||
|
path:Path|None
|
||||||
|
|
||||||
|
def __init__(self, maze:Maze|None=None, path:Path|None=None):
|
||||||
|
super().__init__()
|
||||||
|
self.maze = maze
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
def _getCellColored(self, cell:Cell) -> str:
|
||||||
|
if cell.isWall:
|
||||||
|
# Белый
|
||||||
|
return self._fmt_str(7, 7, WALL)
|
||||||
|
elif cell.isExit:
|
||||||
|
# Кислотно-зелёный
|
||||||
|
return self._fmt_str(12, 7, EXIT)
|
||||||
|
elif cell.isStart:
|
||||||
|
# Кислотно-красный
|
||||||
|
return self._fmt_str(9, 7, START)
|
||||||
|
elif self.path and self.path.array:
|
||||||
|
if cell in self.path.array:
|
||||||
|
# Градиент
|
||||||
|
percent = self.path.array.index(cell) / len(self.path.array)
|
||||||
|
n = self._ANSICalculator(*self._getGradient(percent))
|
||||||
|
return self._fmt_str(n, n, PATH_SYMBOL)
|
||||||
|
return SPACE_SYMBOL
|
||||||
|
|
||||||
|
def _fmt_str(self, bg:int, fg:int, symbol:str) -> str:
|
||||||
|
return f"\033[48;5;{bg}m\033[38;5;{fg}m{symbol}{SBROS}"
|
||||||
|
|
||||||
|
def _ANSICalculator(self, r:int, g:int, b:int):
|
||||||
|
r = max(0, min(5, r))
|
||||||
|
g = max(0, min(5, g))
|
||||||
|
b = max(0, min(5, b))
|
||||||
|
return 16 + 36*r + 6*g + b
|
||||||
|
|
||||||
|
def _getGradient(self, percent:float):
|
||||||
|
r = 5 * (1-percent)
|
||||||
|
g = 0
|
||||||
|
b = 5 * percent
|
||||||
|
return int(round(r)), int(round(g)), int(round(b))
|
||||||
|
|
||||||
|
def render(self, player_position=None):
|
||||||
|
"""
|
||||||
|
Печатем ячейку.
|
||||||
|
Цвет зависит от индекса ячейчки в массиве path.
|
||||||
|
Если в массиве нет - просто белый.
|
||||||
|
"""
|
||||||
|
|
||||||
|
os.system('cls' if os.name == 'nt' else 'clear')
|
||||||
|
|
||||||
|
if not self.maze:
|
||||||
|
print("Лабиринт ещё не загружен")
|
||||||
|
return None
|
||||||
|
|
||||||
|
output = ""
|
||||||
|
for y in range(self.maze.height):
|
||||||
|
for x in range(self.maze.width):
|
||||||
|
cell = self.maze.getCell(x, y)
|
||||||
|
output += self._getCellColored(cell)
|
||||||
|
output += "\n"
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
def update(self, event: MazeEvent):
|
||||||
|
if event.evtype in (MazeEventType.MAZE_LOADED, MazeEventType.PATH_FOUND, MazeEventType.MOVE):
|
||||||
|
if event.evtype == MazeEventType.PATH_FOUND:
|
||||||
|
if not event.data: raise ValueError
|
||||||
|
self.path = event.data
|
||||||
|
if event.evtype == MazeEventType.MAZE_LOADED:
|
||||||
|
if not event.data: raise ValueError
|
||||||
|
self.maze = self.maze
|
||||||
|
self.render()
|
||||||
|
|
@ -4,15 +4,14 @@ import sys
|
||||||
|
|
||||||
from task2.mazeObjects.maze import Maze
|
from task2.mazeObjects.maze import Maze
|
||||||
from task2.mazeObjects.cell import Cell
|
from task2.mazeObjects.cell import Cell
|
||||||
|
from task2.observerSubject import MazeEvent, MazeEventType, Subject
|
||||||
|
|
||||||
class MazeBuilder(ABC):
|
class MazeBuilder(ABC):
|
||||||
"""Интерфейс MazeBuilder с методом buildFromFile(filename)"""
|
"""Интерфейс MazeBuilder с методом buildFromFile(filename)"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def buildFromFile(self, filename: str):
|
def buildFromFile(self, filename: str):
|
||||||
"""Создание лабиринта из файла."""
|
"""Создание лабиринта из файла."""
|
||||||
|
|
||||||
|
|
||||||
class TextFileMazeBuilder(MazeBuilder):
|
class TextFileMazeBuilder(MazeBuilder):
|
||||||
"""Читает файл, парсит символы,
|
"""Читает файл, парсит символы,
|
||||||
создаёт объекты Cell,
|
создаёт объекты Cell,
|
||||||
|
|
@ -22,7 +21,7 @@ class TextFileMazeBuilder(MazeBuilder):
|
||||||
start = {'x': 0, 'y': 0}
|
start = {'x': 0, 'y': 0}
|
||||||
end = {'x': 0, 'y': 0}
|
end = {'x': 0, 'y': 0}
|
||||||
|
|
||||||
def cellStrategy(self, letter: str) -> Cell:
|
def _cellStrategy(self, letter: str) -> Cell:
|
||||||
if letter == '#':
|
if letter == '#':
|
||||||
return Cell(isWall=True)
|
return Cell(isWall=True)
|
||||||
elif letter == ' ':
|
elif letter == ' ':
|
||||||
|
|
@ -35,13 +34,13 @@ class TextFileMazeBuilder(MazeBuilder):
|
||||||
sys.stderr.write(f"Неизвестный символ '{letter}' при загрузке из файла\n")
|
sys.stderr.write(f"Неизвестный символ '{letter}' при загрузке из файла\n")
|
||||||
return Cell()
|
return Cell()
|
||||||
|
|
||||||
def updateStartEnd(self, letter: str, x:int, y:int) -> None:
|
def _updateStartEnd(self, letter: str, x:int, y:int) -> None:
|
||||||
if letter == 'S':
|
if letter == 'S':
|
||||||
self.start = {'x': x, 'y': y}
|
self.start = {'x': x, 'y': y}
|
||||||
elif letter == 'E':
|
elif letter == 'E':
|
||||||
self.end = {'x': x, 'y': y}
|
self.end = {'x': x, 'y': y}
|
||||||
|
|
||||||
def generate_row_from_txt(self, filename: str) -> list[str]:
|
def _generate_row_from_txt(self, filename: str) -> list[str]:
|
||||||
with open(filename) as file:
|
with open(filename) as file:
|
||||||
text = file.read()
|
text = file.read()
|
||||||
text = text.strip()
|
text = text.strip()
|
||||||
|
|
@ -51,16 +50,15 @@ class TextFileMazeBuilder(MazeBuilder):
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def buildFromFile(self, filename: str):
|
def buildFromFile(self, filename: str):
|
||||||
rows = self.generate_row_from_txt(filename)
|
rows = self._generate_row_from_txt(filename)
|
||||||
height = len(rows)
|
height = len(rows)
|
||||||
width = len(rows[0])
|
width = len(rows[0])
|
||||||
array = [[Cell() for j in range(width)] for i in range(height)]
|
array = [[Cell() for j in range(width)] for i in range(height)]
|
||||||
|
|
||||||
# Здесь x и y где-то перепутаны, но мне лень это чинить
|
|
||||||
try:
|
try:
|
||||||
for x, y in product(range(width), range(height)):
|
for x, y in product(range(width), range(height)):
|
||||||
cell = self.cellStrategy(rows[y][x])
|
cell = self._cellStrategy(rows[y][x])
|
||||||
self.updateStartEnd(rows[y][x], x, y)
|
self._updateStartEnd(rows[y][x], x, y)
|
||||||
cell.x = x
|
cell.x = x
|
||||||
cell.y = y
|
cell.y = y
|
||||||
array[y][x] = cell
|
array[y][x] = cell
|
||||||
|
|
@ -68,5 +66,4 @@ class TextFileMazeBuilder(MazeBuilder):
|
||||||
raise ValueError(f"Строка {x+1} имеет длину {len(rows[x])}, ожидалось {width}")
|
raise ValueError(f"Строка {x+1} имеет длину {len(rows[x])}, ожидалось {width}")
|
||||||
|
|
||||||
return Maze(array, self.start, self.end)
|
return Maze(array, self.start, self.end)
|
||||||
|
|
||||||
|
|
||||||
5
MusinAA/task2/mazeExamples/low.txt
Normal file
5
MusinAA/task2/mazeExamples/low.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#####
|
||||||
|
# S #
|
||||||
|
# ###
|
||||||
|
# E
|
||||||
|
#####
|
||||||
|
|
@ -9,8 +9,8 @@ class Maze:
|
||||||
|
|
||||||
def __init__(self, mazeArray: list[list[Cell]], start: dict, end: dict) -> None:
|
def __init__(self, mazeArray: list[list[Cell]], start: dict, end: dict) -> None:
|
||||||
self.mazeArray = mazeArray
|
self.mazeArray = mazeArray
|
||||||
self.height = len(mazeArray)
|
self.height = len(mazeArray) # X
|
||||||
self.width = len(mazeArray[0])
|
self.width = len(mazeArray[0]) # Y
|
||||||
|
|
||||||
self.startCell = self.getCell(start['x'], start['y'])
|
self.startCell = self.getCell(start['x'], start['y'])
|
||||||
self.endCell = self.getCell(end['x'], end['y'])
|
self.endCell = self.getCell(end['x'], end['y'])
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
from task2.mazeObjects.cell import Cell
|
||||||
|
|
||||||
class Path:
|
class Path:
|
||||||
def __init__(self, array:list[Cell]|None, visited_cells:int):
|
def __init__(self, array:list[Cell]|None, visited_cells:int):
|
||||||
self.array = array
|
self.array = array
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
from task2.mazeObjects.maze import Maze
|
from task2.mazeObjects.maze import Maze
|
||||||
from task2.mazeObjects.cell import Cell
|
from task2.mazeObjects.cell import Cell
|
||||||
|
from task2.observerSubject import MazeEvent, MazeEventType, Subject
|
||||||
from task2.strategyObjects.pathFindingStrategy import PathFindingStrategy
|
from task2.strategyObjects.pathFindingStrategy import PathFindingStrategy
|
||||||
|
from task2.strategyObjects.BFS import BFS
|
||||||
|
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
@ -13,7 +16,7 @@ class SearchStats:
|
||||||
self.path = path
|
self.path = path
|
||||||
self.strategy_name = strategy_name
|
self.strategy_name = strategy_name
|
||||||
|
|
||||||
class MazeSolver:
|
class MazeSolver(Subject):
|
||||||
"""
|
"""
|
||||||
MazeSolver содержит поля maze и strategy.
|
MazeSolver содержит поля maze и strategy.
|
||||||
Метод setStrategy(strategy) для динамической смены алгоритма.
|
Метод setStrategy(strategy) для динамической смены алгоритма.
|
||||||
|
|
@ -22,10 +25,15 @@ class MazeSolver:
|
||||||
Для замера времени используйте time.perf_counter() до и после вызова стратегии.
|
Для замера времени используйте time.perf_counter() до и после вызова стратегии.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, maze:Maze, strategy:PathFindingStrategy):
|
def __init__(self, strategy:PathFindingStrategy, maze:Maze|None=None):
|
||||||
self.maze = maze
|
super().__init__()
|
||||||
|
self._maze = maze
|
||||||
self.strategy = strategy
|
self.strategy = strategy
|
||||||
|
|
||||||
|
def setMaze(self, maze: Maze|None):
|
||||||
|
self._maze = maze
|
||||||
|
self.notify(MazeEvent(MazeEventType.MAZE_LOADED, data=maze))
|
||||||
|
|
||||||
def setStrategy(self, strategy:PathFindingStrategy):
|
def setStrategy(self, strategy:PathFindingStrategy):
|
||||||
self.strategy = strategy
|
self.strategy = strategy
|
||||||
|
|
||||||
|
|
@ -33,11 +41,16 @@ class MazeSolver:
|
||||||
return self.strategy.__class__.__name__
|
return self.strategy.__class__.__name__
|
||||||
|
|
||||||
def solve(self):
|
def solve(self):
|
||||||
t_start = time.perf_counter()
|
if not self._maze:
|
||||||
path = self.strategy.findPath(self.maze, self.maze.startCell, self.maze.endCell)
|
raise ValueError
|
||||||
duration = time.perf_counter() - t_start
|
|
||||||
|
|
||||||
|
t_start = time.perf_counter()
|
||||||
|
path = self.strategy.findPath(self._maze, self._maze.startCell, self._maze.endCell)
|
||||||
|
duration = time.perf_counter() - t_start
|
||||||
|
|
||||||
path_len = len(path.array) if path.array else 0
|
path_len = len(path.array) if path.array else 0
|
||||||
strategy_name = self.getStrategyName()
|
strategy_name = self.getStrategyName()
|
||||||
|
|
||||||
return SearchStats(path.array, duration, path.visited_cells, path_len, strategy_name)
|
stats = SearchStats(path.array, duration, path.visited_cells, path_len, strategy_name)
|
||||||
|
self.notify(MazeEvent(MazeEventType.PATH_FOUND, data=path))
|
||||||
|
return stats
|
||||||
43
MusinAA/task2/observerSubject.py
Normal file
43
MusinAA/task2/observerSubject.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
"""
|
||||||
|
Создать интерфейс Observer с методом update(event),
|
||||||
|
где event может быть строкой или объектом с типом события ("path_found", "move", "maze_loaded").
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
class MazeEventType(Enum):
|
||||||
|
PATH_FOUND = "path_found"
|
||||||
|
MOVE = "move"
|
||||||
|
MAZE_LOADED = "maze_loaded"
|
||||||
|
|
||||||
|
class MazeEvent:
|
||||||
|
data=None
|
||||||
|
def __init__(self, evtype: MazeEventType, data=None):
|
||||||
|
if not isinstance(evtype, MazeEventType):
|
||||||
|
raise TypeError(f"evtype must be an EventType, got {type(evtype)}")
|
||||||
|
self.evtype = evtype
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
class Observer(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def update(self, event: MazeEvent):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class Subject(ABC):
|
||||||
|
"""Издатель: управляет подписчиками и отправляет им уведомления."""
|
||||||
|
def __init__(self):
|
||||||
|
self._observers:set[Observer] = set()
|
||||||
|
|
||||||
|
def attach(self, obs:Observer):
|
||||||
|
"Подписать наблюдателя"
|
||||||
|
self._observers.add(obs)
|
||||||
|
|
||||||
|
def detach(self, obs:Observer):
|
||||||
|
"Отписать наблюдателя"
|
||||||
|
self._observers.discard(obs)
|
||||||
|
|
||||||
|
def notify(self, event:MazeEvent):
|
||||||
|
for obs in self._observers:
|
||||||
|
obs.update(event)
|
||||||
Loading…
Reference in New Issue
Block a user