forked from UNN/2026-rff_mp
[2] Добавлено:
- Тесты на классы Cell, Maze - Алгоритмы поиска пути: BFS, DFS, Astar
This commit is contained in:
parent
535c706af8
commit
f7577f803c
|
|
@ -103,12 +103,12 @@ class Cell:
|
||||||
Строковый символ, соответствующий текущему типу клетки.
|
Строковый символ, соответствующий текущему типу клетки.
|
||||||
"""
|
"""
|
||||||
if self._is_wall:
|
if self._is_wall:
|
||||||
return cell_mapping['wall']
|
return cell_mapping["wall"]
|
||||||
if self._is_start:
|
if self._is_start:
|
||||||
return cell_mapping['start']
|
return cell_mapping["start"]
|
||||||
if self._is_exit:
|
if self._is_exit:
|
||||||
return cell_mapping['exit']
|
return cell_mapping["exit"]
|
||||||
return cell_mapping['empty']
|
return cell_mapping["empty"]
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self._get_type_cell()
|
return self._get_type_cell()
|
||||||
|
|
@ -132,8 +132,7 @@ class Maze:
|
||||||
"""
|
"""
|
||||||
self._width, self._height = size
|
self._width, self._height = size
|
||||||
self._map: list[list[Cell]] = [
|
self._map: list[list[Cell]] = [
|
||||||
[Cell(x, y) for x in range(self._width)]
|
[Cell(x, y) for x in range(self._width)] for y in range(self._height)
|
||||||
for y in range(self._height)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def _check_point_in_map(self, x: int, y: int) -> bool:
|
def _check_point_in_map(self, x: int, y: int) -> bool:
|
||||||
|
|
@ -185,6 +184,10 @@ class Maze:
|
||||||
|
|
||||||
return neighbors
|
return neighbors
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shape(self) -> tuple[int, int]:
|
||||||
|
return self._height, self._width
|
||||||
|
|
||||||
def __getitem__(self, index: tuple[int, int]) -> Cell:
|
def __getitem__(self, index: tuple[int, int]) -> Cell:
|
||||||
"""Возвращает клетку по индексу [row, col].
|
"""Возвращает клетку по индексу [row, col].
|
||||||
|
|
||||||
|
|
@ -226,13 +229,13 @@ class Maze:
|
||||||
if cell_type is None:
|
if cell_type is None:
|
||||||
raise ValueError(f"Символ '{value}' не соответствует ни одному типу клетки")
|
raise ValueError(f"Символ '{value}' не соответствует ни одному типу клетки")
|
||||||
|
|
||||||
if cell_type == 'empty':
|
if cell_type == "empty":
|
||||||
cell._clear_flags()
|
cell._clear_flags()
|
||||||
else:
|
else:
|
||||||
setattr(cell, f"is_{cell_type}", True)
|
setattr(cell, f"is_{cell_type}", True)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return '\n'.join(
|
return "\n".join(
|
||||||
''.join(str(self._map[y][x]) for x in range(self._width))
|
"".join(str(self._map[y][x]) for x in range(self._width))
|
||||||
for y in range(self._height)
|
for y in range(self._height)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1 @@
|
||||||
cell_mapping = {
|
cell_mapping = {"wall": "#", "empty": " ", "start": "S", "exit": "E"}
|
||||||
'wall': '#',
|
|
||||||
'empty': ' ',
|
|
||||||
'start': 'S',
|
|
||||||
'exit': 'E'
|
|
||||||
}
|
|
||||||
|
|
|
||||||
11
skorohodovsa/task_2/source/strategy/__init__.py
Normal file
11
skorohodovsa/task_2/source/strategy/__init__.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
from source.strategy.algorithms import PathFindingStrategy
|
||||||
|
from source.strategy.astar import AStarStrategy
|
||||||
|
from source.strategy.bfs import BFSStrategy
|
||||||
|
from source.strategy.dfs import DFSStrategy
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"PathFindingStrategy",
|
||||||
|
"BFSStrategy",
|
||||||
|
"DFSStrategy",
|
||||||
|
"AStarStrategy",
|
||||||
|
]
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections import deque
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from source.models.base import Maze, Cell
|
from source.models.base import Maze, Cell
|
||||||
|
|
@ -9,7 +8,9 @@ class PathFindingStrategy(ABC):
|
||||||
"""Интерфейс стратегии поиска пути в лабиринте."""
|
"""Интерфейс стратегии поиска пути в лабиринте."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> list[Cell]:
|
def find_path(
|
||||||
|
self, maze: Maze, start: Cell = None, exit: Cell = None
|
||||||
|
) -> list[Cell]:
|
||||||
"""Найти путь от start до exit.
|
"""Найти путь от start до exit.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -22,7 +23,27 @@ class PathFindingStrategy(ABC):
|
||||||
Пустой список, если путь не найден.
|
Пустой список, если путь не найден.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _reconstruct_path(came_from: dict[Cell, Optional[Cell]], end: Cell) -> list[Cell]:
|
def _find_start(self, maze: Maze) -> Optional[Cell]:
|
||||||
|
row, col = maze.shape
|
||||||
|
|
||||||
|
for y in range(row):
|
||||||
|
for x in range(col):
|
||||||
|
if maze[y, x].is_start:
|
||||||
|
return maze[y, x]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _find_exit(self, maze: Maze) -> Optional[Cell]:
|
||||||
|
row, col = maze.shape
|
||||||
|
|
||||||
|
for y in range(row):
|
||||||
|
for x in range(col):
|
||||||
|
if maze[y, x].is_exit:
|
||||||
|
return maze[y, x]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _reconstruct_path(
|
||||||
|
self, came_from: dict[Cell, Optional[Cell]], end: Cell
|
||||||
|
) -> list[Cell]:
|
||||||
"""Восстанавливает путь от старта до end, идя по came_from в обратном порядке.
|
"""Восстанавливает путь от старта до end, идя по came_from в обратном порядке.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -44,6 +65,10 @@ class PathFindingStrategy(ABC):
|
||||||
path.reverse()
|
path.reverse()
|
||||||
return path
|
return path
|
||||||
|
|
||||||
class BFS(PathFindingStrategy):
|
|
||||||
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> list[Cell]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
46
skorohodovsa/task_2/source/strategy/astar.py
Normal file
46
skorohodovsa/task_2/source/strategy/astar.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import heapq
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from source.models.base import Cell, Maze
|
||||||
|
from source.strategy.algorithms import PathFindingStrategy
|
||||||
|
|
||||||
|
|
||||||
|
def _manhattan(a: Cell, b: Cell) -> int:
|
||||||
|
"""Манхэттенское расстояние между двумя клетками."""
|
||||||
|
return abs(a.x - b.x) + abs(a.y - b.y)
|
||||||
|
|
||||||
|
|
||||||
|
class AStarStrategy(PathFindingStrategy):
|
||||||
|
"""Алгоритм A* с манхэттенской эвристикой."""
|
||||||
|
|
||||||
|
def find_path(self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None) -> list[Cell]:
|
||||||
|
if start is None:
|
||||||
|
start = self._find_start(maze)
|
||||||
|
if exit is None:
|
||||||
|
exit = self._find_exit(maze)
|
||||||
|
|
||||||
|
g_score: dict[Cell, int] = {start: 0}
|
||||||
|
came_from: dict[Cell, Optional[Cell]] = {start: None}
|
||||||
|
|
||||||
|
counter = 0
|
||||||
|
open_heap: list[tuple[int, int, Cell]] = [
|
||||||
|
(_manhattan(start, exit), counter, start)
|
||||||
|
]
|
||||||
|
|
||||||
|
while open_heap:
|
||||||
|
_, _, current = heapq.heappop(open_heap)
|
||||||
|
|
||||||
|
if current is exit:
|
||||||
|
return self._reconstruct_path(came_from, exit)
|
||||||
|
|
||||||
|
for neighbor in maze.get_neighbors(current.x, current.y):
|
||||||
|
tentative_g = g_score[current] + 1
|
||||||
|
|
||||||
|
if tentative_g < g_score.get(neighbor, float("inf")):
|
||||||
|
g_score[neighbor] = tentative_g
|
||||||
|
came_from[neighbor] = current
|
||||||
|
f = tentative_g + _manhattan(neighbor, exit)
|
||||||
|
counter += 1
|
||||||
|
heapq.heappush(open_heap, (f, counter, neighbor))
|
||||||
|
|
||||||
|
return []
|
||||||
37
skorohodovsa/task_2/source/strategy/bfs.py
Normal file
37
skorohodovsa/task_2/source/strategy/bfs.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
from collections import deque
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from source.models.base import Cell, Maze
|
||||||
|
from source.strategy.algorithms import PathFindingStrategy
|
||||||
|
|
||||||
|
|
||||||
|
class BFSStrategy(PathFindingStrategy):
|
||||||
|
"""Поиск в ширину (Breadth-First Search).
|
||||||
|
|
||||||
|
Гарантирует кратчайший путь по количеству шагов.
|
||||||
|
Сложность: O(V + E) по времени и памяти.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def find_path(
|
||||||
|
self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None
|
||||||
|
) -> list[Cell]:
|
||||||
|
if start is None:
|
||||||
|
start = self._find_start(maze)
|
||||||
|
if exit is None:
|
||||||
|
exit = self._find_exit(maze)
|
||||||
|
|
||||||
|
came_from: dict[Cell, Optional[Cell]] = {start: None}
|
||||||
|
queue: deque[Cell] = deque([start])
|
||||||
|
|
||||||
|
while queue:
|
||||||
|
current = queue.popleft()
|
||||||
|
|
||||||
|
if current is exit:
|
||||||
|
return self._reconstruct_path(came_from, exit)
|
||||||
|
|
||||||
|
for neighbor in maze.get_neighbors(current.x, current.y):
|
||||||
|
if neighbor not in came_from:
|
||||||
|
came_from[neighbor] = current
|
||||||
|
queue.append(neighbor)
|
||||||
|
|
||||||
|
return []
|
||||||
35
skorohodovsa/task_2/source/strategy/dfs.py
Normal file
35
skorohodovsa/task_2/source/strategy/dfs.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from source.models.base import Maze, Cell
|
||||||
|
from source.strategy.algorithms import PathFindingStrategy
|
||||||
|
|
||||||
|
|
||||||
|
class DFSStrategy(PathFindingStrategy):
|
||||||
|
"""Поиск в глубину (Depth-First Search).
|
||||||
|
|
||||||
|
Находит путь, но не гарантирует кратчайший.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def find_path(
|
||||||
|
self, maze: Maze, start: Optional[Cell] = None, exit: Optional[Cell] = None
|
||||||
|
) -> list[Cell]:
|
||||||
|
if start is None:
|
||||||
|
start = self._find_start(maze)
|
||||||
|
if exit is None:
|
||||||
|
exit = self._find_exit(maze)
|
||||||
|
|
||||||
|
came_from: dict[Cell, Optional[Cell]] = {start: None}
|
||||||
|
stack: list[Cell] = [start]
|
||||||
|
|
||||||
|
while stack:
|
||||||
|
current = stack.pop()
|
||||||
|
|
||||||
|
if current is exit:
|
||||||
|
return self._reconstruct_path(came_from, exit)
|
||||||
|
|
||||||
|
for neighbor in maze.get_neighbors(current.x, current.y):
|
||||||
|
if neighbor not in came_from:
|
||||||
|
came_from[neighbor] = current
|
||||||
|
stack.append(neighbor)
|
||||||
|
|
||||||
|
return []
|
||||||
93
skorohodovsa/task_2/source/strategy/solver.py
Normal file
93
skorohodovsa/task_2/source/strategy/solver.py
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from source.models.base import Maze, Cell
|
||||||
|
from source.strategy import PathFindingStrategy
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SearchStats:
|
||||||
|
"""Статистика выполнения поиска пути.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
elapsed_ms: Время выполнения в миллисекундах.
|
||||||
|
visited_count: Количество посещённых клеток.
|
||||||
|
path_length: Длина найденного пути (0 если путь не найден).
|
||||||
|
path: Найденный путь — список клеток от старта до выхода.
|
||||||
|
"""
|
||||||
|
elapsed_ms: float
|
||||||
|
visited_count: int
|
||||||
|
path_length: int
|
||||||
|
path: list[Cell]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"Время: {self.elapsed_ms:.3f} мс | "
|
||||||
|
f"Посещено клеток: {self.visited_count} | "
|
||||||
|
f"Длина пути: {self.path_length}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MazeSolver:
|
||||||
|
"""Оркестратор поиска пути в лабиринте.
|
||||||
|
|
||||||
|
Принимает лабиринт и стратегию поиска, выполняет поиск
|
||||||
|
и возвращает результат вместе со статистикой выполнения.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
solver = MazeSolver(maze, BFSStrategy())
|
||||||
|
stats = solver.solve()
|
||||||
|
print(stats)
|
||||||
|
|
||||||
|
solver.set_strategy(AStarStrategy())
|
||||||
|
stats = solver.solve()
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, maze: Maze, strategy: PathFindingStrategy) -> None:
|
||||||
|
"""Инициализирует солвер с лабиринтом и стратегией поиска.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
maze: Объект лабиринта.
|
||||||
|
strategy: Стратегия поиска пути.
|
||||||
|
"""
|
||||||
|
self._maze = maze
|
||||||
|
self._strategy = strategy
|
||||||
|
|
||||||
|
def set_strategy(self, strategy: PathFindingStrategy) -> None:
|
||||||
|
"""Заменяет текущую стратегию поиска.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
strategy: Новая стратегия поиска пути.
|
||||||
|
"""
|
||||||
|
self._strategy = strategy
|
||||||
|
|
||||||
|
def solve(
|
||||||
|
self,
|
||||||
|
start: Cell = None,
|
||||||
|
exit: Cell = None,
|
||||||
|
) -> SearchStats:
|
||||||
|
"""Выполняет поиск пути и собирает статистику.
|
||||||
|
|
||||||
|
Если start или exit не переданы явно, стратегия найдёт
|
||||||
|
их самостоятельно по флагам is_start / is_exit в лабиринте.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
start: Стартовая клетка (опционально).
|
||||||
|
exit: Конечная клетка (опционально).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Объект SearchStats с временем выполнения, количеством
|
||||||
|
посещённых клеток и длиной найденного пути.
|
||||||
|
"""
|
||||||
|
t_start = time.perf_counter()
|
||||||
|
path = self._strategy.find_path(self._maze, start, exit)
|
||||||
|
t_end = time.perf_counter()
|
||||||
|
|
||||||
|
elapsed_ms = (t_end - t_start) * 1000
|
||||||
|
|
||||||
|
return SearchStats(
|
||||||
|
elapsed_ms=elapsed_ms,
|
||||||
|
visited_count=len(path),
|
||||||
|
path_length=len(path),
|
||||||
|
path=path,
|
||||||
|
)
|
||||||
|
|
@ -1,8 +1,111 @@
|
||||||
import pytest
|
import pytest
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import copy
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "./../../models")))
|
from source.models.base import Cell
|
||||||
|
|
||||||
from base import Cell
|
class TestCellCreation:
|
||||||
|
"""Тесты создания клетки и начальных значений."""
|
||||||
|
|
||||||
|
def test_coordinates_are_set(self):
|
||||||
|
cell = Cell(3, 7)
|
||||||
|
assert cell.x == 3
|
||||||
|
assert cell.y == 7
|
||||||
|
|
||||||
|
def test_default_flags_are_false(self):
|
||||||
|
cell = Cell(0, 0)
|
||||||
|
assert cell.is_wall is False
|
||||||
|
assert cell.is_start is False
|
||||||
|
assert cell.is_exit is False
|
||||||
|
|
||||||
|
def test_create_wall(self):
|
||||||
|
cell = Cell(0, 0, is_wall=True)
|
||||||
|
assert cell.is_wall is True
|
||||||
|
|
||||||
|
def test_create_start(self):
|
||||||
|
cell = Cell(0, 0, is_start=True)
|
||||||
|
assert cell.is_start is True
|
||||||
|
|
||||||
|
def test_create_exit(self):
|
||||||
|
cell = Cell(0, 0, is_exit=True)
|
||||||
|
assert cell.is_exit is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestCellIsPassable:
|
||||||
|
"""Тесты метода is_possible."""
|
||||||
|
|
||||||
|
def test_empty_cell_is_passable(self):
|
||||||
|
cell = Cell(0, 0)
|
||||||
|
assert cell.is_possible() is True
|
||||||
|
|
||||||
|
def test_wall_is_not_passable(self):
|
||||||
|
cell = Cell(0, 0, is_wall=True)
|
||||||
|
assert cell.is_possible() is False
|
||||||
|
|
||||||
|
def test_start_cell_is_passable(self):
|
||||||
|
cell = Cell(0, 0, is_start=True)
|
||||||
|
assert cell.is_possible() is True
|
||||||
|
|
||||||
|
def test_exit_cell_is_passable(self):
|
||||||
|
cell = Cell(0, 0, is_exit=True)
|
||||||
|
assert cell.is_possible() is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestCellFlagsAreMutuallyExclusive:
|
||||||
|
"""Тесты взаимного исключения флагов."""
|
||||||
|
|
||||||
|
def test_set_wall_clears_start(self):
|
||||||
|
cell = Cell(0, 0, is_start=True)
|
||||||
|
cell.is_wall = True
|
||||||
|
assert cell.is_start is False
|
||||||
|
assert cell.is_wall is True
|
||||||
|
|
||||||
|
def test_set_wall_clears_exit(self):
|
||||||
|
cell = Cell(0, 0, is_exit=True)
|
||||||
|
cell.is_wall = True
|
||||||
|
assert cell.is_exit is False
|
||||||
|
assert cell.is_wall is True
|
||||||
|
|
||||||
|
def test_set_start_clears_wall(self):
|
||||||
|
cell = Cell(0, 0, is_wall=True)
|
||||||
|
cell.is_start = True
|
||||||
|
assert cell.is_wall is False
|
||||||
|
assert cell.is_start is True
|
||||||
|
|
||||||
|
def test_set_start_clears_exit(self):
|
||||||
|
cell = Cell(0, 0, is_exit=True)
|
||||||
|
cell.is_start = True
|
||||||
|
assert cell.is_exit is False
|
||||||
|
assert cell.is_start is True
|
||||||
|
|
||||||
|
def test_set_exit_clears_wall(self):
|
||||||
|
cell = Cell(0, 0, is_wall=True)
|
||||||
|
cell.is_exit = True
|
||||||
|
assert cell.is_wall is False
|
||||||
|
assert cell.is_exit is True
|
||||||
|
|
||||||
|
def test_set_exit_clears_start(self):
|
||||||
|
cell = Cell(0, 0, is_start=True)
|
||||||
|
cell.is_exit = True
|
||||||
|
assert cell.is_start is False
|
||||||
|
assert cell.is_exit is True
|
||||||
|
|
||||||
|
def test_unset_wall_does_not_clear_others(self):
|
||||||
|
# снятие флага (False) не должно трогать остальные
|
||||||
|
cell = Cell(0, 0, is_wall=True)
|
||||||
|
cell.is_wall = False
|
||||||
|
assert cell.is_start is False
|
||||||
|
assert cell.is_exit is False
|
||||||
|
|
||||||
|
|
||||||
|
class TestCellStr:
|
||||||
|
"""Тесты строкового представления клетки."""
|
||||||
|
|
||||||
|
def test_str_returns_string(self):
|
||||||
|
cell = Cell(0, 0)
|
||||||
|
assert isinstance(str(cell), str)
|
||||||
|
|
||||||
|
def test_repr_contains_coordinates(self):
|
||||||
|
cell = Cell(4, 9)
|
||||||
|
assert "4" in repr(cell)
|
||||||
|
assert "9" in repr(cell)
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
import pytest
|
||||||
|
import random
|
||||||
|
|
||||||
|
random.seed("РФ СЛФ!")
|
||||||
|
|
||||||
|
from source.models.base import Cell, Maze
|
||||||
|
from source.settings import cell_mapping
|
||||||
|
|
||||||
|
|
||||||
|
class TestMaze:
|
||||||
|
|
||||||
|
def test_default_size(self):
|
||||||
|
"""Проверка размеров лабиринта со значениями по умолчанию"""
|
||||||
|
maze = Maze()
|
||||||
|
row, col = maze.shape
|
||||||
|
assert row == 10
|
||||||
|
assert col == 10
|
||||||
|
|
||||||
|
def test_custom_size(self):
|
||||||
|
"""Проверка размеров лабиринта с заданными размерами"""
|
||||||
|
maze = Maze(size=(7, 3))
|
||||||
|
assert maze._width == 7
|
||||||
|
assert maze._height == 3
|
||||||
|
|
||||||
|
def test_all_cells_empty_on_init(self):
|
||||||
|
"""Проверка создания пустого лабиринта с заданными размерами"""
|
||||||
|
maze = Maze(size=(3, 3))
|
||||||
|
for y in range(3):
|
||||||
|
for x in range(3):
|
||||||
|
cell = maze.get_cell(x, y)
|
||||||
|
assert not cell.is_wall
|
||||||
|
assert not cell.is_start
|
||||||
|
assert not cell.is_exit
|
||||||
|
|
||||||
|
def test_get_cell_valid(self):
|
||||||
|
"""Проверка получения объекта Cell из лабиринта функцией `get_cell()`"""
|
||||||
|
maze = Maze(size=(5, 5))
|
||||||
|
assert isinstance(maze.get_cell(2, 3), Cell)
|
||||||
|
|
||||||
|
def test_get_cell_out_of_bounds(self):
|
||||||
|
"""Проверка неправильных указанных индексов лабиринта"""
|
||||||
|
maze = Maze(size=(5, 5))
|
||||||
|
assert maze.get_cell(-1, 0) is None
|
||||||
|
assert maze.get_cell(0, -1) is None
|
||||||
|
assert maze.get_cell(5, 0) is None
|
||||||
|
assert maze.get_cell(0, 5) is None
|
||||||
|
|
||||||
|
def test_center_has_four_neighbors(self):
|
||||||
|
"""Проверка нахождения соседей"""
|
||||||
|
maze = Maze(size=(5, 5))
|
||||||
|
assert len(maze.get_neighbors(2, 2)) == 4
|
||||||
|
|
||||||
|
def test_corner_has_two_neighbors(self):
|
||||||
|
"""Проверка нахождения соседей, когда указанное поле в углу лабиринта"""
|
||||||
|
maze = Maze(size=(5, 5))
|
||||||
|
assert len(maze.get_neighbors(0, 0)) == 2
|
||||||
|
|
||||||
|
def test_wall_excluded_from_neighbors(self):
|
||||||
|
"""Проверка что стена не попадает в список соседей"""
|
||||||
|
maze = Maze(size=(5, 5))
|
||||||
|
maze[1, 2] = cell_mapping['wall']
|
||||||
|
assert all(not n.is_wall for n in maze.get_neighbors(2, 2))
|
||||||
|
|
||||||
|
def test_setitem_wall(self):
|
||||||
|
"""Проверка установки стены через оператор []"""
|
||||||
|
maze = Maze(size=(5, 5))
|
||||||
|
maze[0, 0] = cell_mapping['wall']
|
||||||
|
assert maze[0, 0].is_wall is True
|
||||||
|
|
||||||
|
def test_setitem_start(self):
|
||||||
|
"""Проверка установки старта через оператор []"""
|
||||||
|
maze = Maze(size=(5, 5))
|
||||||
|
maze[0, 0] = cell_mapping['start']
|
||||||
|
assert maze[0, 0].is_start is True
|
||||||
|
|
||||||
|
def test_setitem_exit(self):
|
||||||
|
"""Проверка установки выхода через оператор []"""
|
||||||
|
maze = Maze(size=(5, 5))
|
||||||
|
maze[0, 0] = cell_mapping['exit']
|
||||||
|
assert maze[0, 0].is_exit is True
|
||||||
|
|
||||||
|
def test_setitem_empty_clears_flags(self):
|
||||||
|
"""Проверка сброса флагов клетки при установке пустого типа"""
|
||||||
|
maze = Maze(size=(5, 5))
|
||||||
|
maze[0, 0] = cell_mapping['wall']
|
||||||
|
maze[0, 0] = cell_mapping['empty']
|
||||||
|
assert not maze[0, 0].is_wall
|
||||||
|
|
||||||
|
def test_getitem_out_of_bounds_raises(self):
|
||||||
|
"""Проверка выброса IndexError при обращении к клетке вне границ лабиринта"""
|
||||||
|
maze = Maze(size=(5, 5))
|
||||||
|
with pytest.raises(IndexError):
|
||||||
|
_ = maze[10, 10]
|
||||||
|
|
||||||
|
def test_setitem_invalid_symbol_raises(self):
|
||||||
|
"""Проверка выброса ValueError при установке неизвестного символа"""
|
||||||
|
maze = Maze(size=(5, 5))
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
maze[0, 0] = "?"
|
||||||
|
|
||||||
|
def test_str_lines_match_height(self):
|
||||||
|
"""Проверка что количество строк в строковом представлении совпадает с высотой"""
|
||||||
|
maze = Maze(size=(4, 6))
|
||||||
|
print(str(maze).splitlines())
|
||||||
|
assert len(str(maze).splitlines()) == 6
|
||||||
|
|
||||||
|
def test_str_line_length_matches_width(self):
|
||||||
|
"""Проверка что длина каждой строки в строковом представлении совпадает с шириной"""
|
||||||
|
maze = Maze(size=(5, 3))
|
||||||
|
for line in str(maze).strip().splitlines():
|
||||||
|
assert len(line) == 5
|
||||||
Loading…
Reference in New Issue
Block a user