Реализован паттерн Observer

This commit is contained in:
oSTEVEo 2026-05-24 00:33:24 +03:00
parent 74928a997a
commit 95805467ab
8 changed files with 193 additions and 19 deletions

20
MusinAA/main.py Normal file
View 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()

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

View File

@ -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
@ -69,4 +67,3 @@ class TextFileMazeBuilder(MazeBuilder):
return Maze(array, self.start, self.end) return Maze(array, self.start, self.end)

View File

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

View File

@ -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'])

View File

@ -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

View File

@ -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):
if not self._maze:
raise ValueError
t_start = time.perf_counter() t_start = time.perf_counter()
path = self.strategy.findPath(self.maze, self.maze.startCell, self.maze.endCell) path = self.strategy.findPath(self._maze, self._maze.startCell, self._maze.endCell)
duration = time.perf_counter() - t_start 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

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