Реализован паттерн 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.cell import Cell
from task2.observerSubject import MazeEvent, MazeEventType, Subject
class MazeBuilder(ABC):
"""Интерфейс MazeBuilder с методом buildFromFile(filename)"""
@abstractmethod
def buildFromFile(self, filename: str):
"""Создание лабиринта из файла."""
class TextFileMazeBuilder(MazeBuilder):
"""Читает файл, парсит символы,
создаёт объекты Cell,
@ -22,7 +21,7 @@ class TextFileMazeBuilder(MazeBuilder):
start = {'x': 0, 'y': 0}
end = {'x': 0, 'y': 0}
def cellStrategy(self, letter: str) -> Cell:
def _cellStrategy(self, letter: str) -> Cell:
if letter == '#':
return Cell(isWall=True)
elif letter == ' ':
@ -35,13 +34,13 @@ class TextFileMazeBuilder(MazeBuilder):
sys.stderr.write(f"Неизвестный символ '{letter}' при загрузке из файла\n")
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':
self.start = {'x': x, 'y': y}
elif letter == 'E':
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:
text = file.read()
text = text.strip()
@ -51,16 +50,15 @@ class TextFileMazeBuilder(MazeBuilder):
return text
def buildFromFile(self, filename: str):
rows = self.generate_row_from_txt(filename)
rows = self._generate_row_from_txt(filename)
height = len(rows)
width = len(rows[0])
array = [[Cell() for j in range(width)] for i in range(height)]
# Здесь x и y где-то перепутаны, но мне лень это чинить
try:
for x, y in product(range(width), range(height)):
cell = self.cellStrategy(rows[y][x])
self.updateStartEnd(rows[y][x], x, y)
cell = self._cellStrategy(rows[y][x])
self._updateStartEnd(rows[y][x], x, y)
cell.x = x
cell.y = y
array[y][x] = cell
@ -69,4 +67,3 @@ class TextFileMazeBuilder(MazeBuilder):
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:
self.mazeArray = mazeArray
self.height = len(mazeArray)
self.width = len(mazeArray[0])
self.height = len(mazeArray) # X
self.width = len(mazeArray[0]) # Y
self.startCell = self.getCell(start['x'], start['y'])
self.endCell = self.getCell(end['x'], end['y'])

View File

@ -1,3 +1,5 @@
from task2.mazeObjects.cell import Cell
class Path:
def __init__(self, array:list[Cell]|None, visited_cells:int):
self.array = array

View File

@ -1,6 +1,9 @@
from task2.mazeObjects.maze import Maze
from task2.mazeObjects.cell import Cell
from task2.observerSubject import MazeEvent, MazeEventType, Subject
from task2.strategyObjects.pathFindingStrategy import PathFindingStrategy
from task2.strategyObjects.BFS import BFS
import time
@ -13,7 +16,7 @@ class SearchStats:
self.path = path
self.strategy_name = strategy_name
class MazeSolver:
class MazeSolver(Subject):
"""
MazeSolver содержит поля maze и strategy.
Метод setStrategy(strategy) для динамической смены алгоритма.
@ -22,10 +25,15 @@ class MazeSolver:
Для замера времени используйте time.perf_counter() до и после вызова стратегии.
"""
def __init__(self, maze:Maze, strategy:PathFindingStrategy):
self.maze = maze
def __init__(self, strategy:PathFindingStrategy, maze:Maze|None=None):
super().__init__()
self._maze = maze
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):
self.strategy = strategy
@ -33,11 +41,16 @@ class MazeSolver:
return self.strategy.__class__.__name__
def solve(self):
if not self._maze:
raise ValueError
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
path_len = len(path.array) if path.array else 0
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)