From 95805467ab9d831ec52b80d0eca2f6e2b92e541c Mon Sep 17 00:00:00 2001 From: oSTEVEo Date: Sun, 24 May 2026 00:33:24 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=20=D0=BF=D0=B0=D1=82=D1=82=D0=B5=D1=80=D0=BD?= =?UTF-8?q?=20Observer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MusinAA/main.py | 20 +++++++ MusinAA/task2/consoleView.py | 94 ++++++++++++++++++++++++++++++ MusinAA/task2/mazeBuilder.py | 17 +++--- MusinAA/task2/mazeExamples/low.txt | 5 ++ MusinAA/task2/mazeObjects/maze.py | 4 +- MusinAA/task2/mazeObjects/path.py | 2 + MusinAA/task2/mazeSolver.py | 27 ++++++--- MusinAA/task2/observerSubject.py | 43 ++++++++++++++ 8 files changed, 193 insertions(+), 19 deletions(-) create mode 100644 MusinAA/main.py create mode 100644 MusinAA/task2/consoleView.py create mode 100644 MusinAA/task2/mazeExamples/low.txt create mode 100644 MusinAA/task2/observerSubject.py diff --git a/MusinAA/main.py b/MusinAA/main.py new file mode 100644 index 0000000..752e187 --- /dev/null +++ b/MusinAA/main.py @@ -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() diff --git a/MusinAA/task2/consoleView.py b/MusinAA/task2/consoleView.py new file mode 100644 index 0000000..50b1a4a --- /dev/null +++ b/MusinAA/task2/consoleView.py @@ -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() diff --git a/MusinAA/task2/mazeBuilder.py b/MusinAA/task2/mazeBuilder.py index 7ddcc0f..1fcec59 100644 --- a/MusinAA/task2/mazeBuilder.py +++ b/MusinAA/task2/mazeBuilder.py @@ -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 @@ -68,5 +66,4 @@ class TextFileMazeBuilder(MazeBuilder): raise ValueError(f"Строка {x+1} имеет длину {len(rows[x])}, ожидалось {width}") return Maze(array, self.start, self.end) - \ No newline at end of file diff --git a/MusinAA/task2/mazeExamples/low.txt b/MusinAA/task2/mazeExamples/low.txt new file mode 100644 index 0000000..28f1058 --- /dev/null +++ b/MusinAA/task2/mazeExamples/low.txt @@ -0,0 +1,5 @@ +##### +# S # +# ### +# E +##### \ No newline at end of file diff --git a/MusinAA/task2/mazeObjects/maze.py b/MusinAA/task2/mazeObjects/maze.py index a9aae9d..301d77d 100644 --- a/MusinAA/task2/mazeObjects/maze.py +++ b/MusinAA/task2/mazeObjects/maze.py @@ -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']) diff --git a/MusinAA/task2/mazeObjects/path.py b/MusinAA/task2/mazeObjects/path.py index 9579dbc..3277c73 100644 --- a/MusinAA/task2/mazeObjects/path.py +++ b/MusinAA/task2/mazeObjects/path.py @@ -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 diff --git a/MusinAA/task2/mazeSolver.py b/MusinAA/task2/mazeSolver.py index d346b36..6d28cdc 100644 --- a/MusinAA/task2/mazeSolver.py +++ b/MusinAA/task2/mazeSolver.py @@ -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): - t_start = time.perf_counter() - path = self.strategy.findPath(self.maze, self.maze.startCell, self.maze.endCell) - duration = time.perf_counter() - t_start + if not self._maze: + raise ValueError + 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 strategy_name = self.getStrategyName() - return SearchStats(path.array, duration, path.visited_cells, path_len, strategy_name) \ No newline at end of file + stats = SearchStats(path.array, duration, path.visited_cells, path_len, strategy_name) + self.notify(MazeEvent(MazeEventType.PATH_FOUND, data=path)) + return stats \ No newline at end of file diff --git a/MusinAA/task2/observerSubject.py b/MusinAA/task2/observerSubject.py new file mode 100644 index 0000000..c49b8f8 --- /dev/null +++ b/MusinAA/task2/observerSubject.py @@ -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) \ No newline at end of file