O arquivo do gerador
Depois do laço de tentativas da página anterior, a variável maze guarda um
labirinto aceito, mas ele ainda está nos tipos internos do pacote (Maze, Cell). Falta calcular
o caminho mais curto e entregar o resultado em tipos simples, que é o que a fachada promete. Esta
página fecha o método generate() e mostra o arquivo mazegen/generator.py inteiro.
Resolver e publicar
generate() termina com duas linhas:
self.solution = Solver().shortest_path(maze, self._entry, self._exit)
self.grid = maze.rows()
Solver().shortest_path(...)devolve o caminho mais curto da entrada até a saída, em letrasN,E,SeW(parte 7). Ele é calculado uma vez por labirinto, aqui; o arquivo de saída e a tela usam o mesmo texto.maze.rows()devolve uma cópia da grade de paredes: uma lista por linha, com o número das paredes de cada célula (parte 5). Sendo cópia, quem mexer emgen.gridnão estraga nada dentro do pacote.
O Maze deixa de existir quando o método termina: só os tipos simples (str e listas de int)
saem do pacote.
O arquivo completo
Juntando as constantes e a assinatura (página 2), as
conferências do construtor (página 3) e o laço de tentativas
(página 4). Os imports trazem cada peça das partes 4 a 8; do módulo do
Braider vêm também as constantes MAX_DEAD_ENDS e MIN_LOOPS. A docstring da classe (o texto
entre """ abaixo de class MazeGenerator:) descreve os três atributos públicos.
"""Public facade of the package: MazeGenerator."""
import random
from mazegen.entities.maze import Maze
from mazegen.errors import MazeError
from mazegen.services.braider import MAX_DEAD_ENDS, MIN_LOOPS, Braider
from mazegen.services.carver import Carver
from mazegen.services.solver import Solver
from mazegen.value_objects.cell import Cell
from mazegen.value_objects.pattern42 import Pattern42
MIN_SIZE = 1
MAX_SIZE = 100
MAX_ATTEMPTS = 20
class MazeGenerator:
"""Generate perfect mazes or Pac-Man boards with a "42" in the middle.
Attributes:
grid: height rows of width wall masks after generate(); empty
before it.
solution: Shortest path from entry to exit after generate(), one
letter N, E, S or W per step; empty before it.
pattern: (x, y) cells of the "42"; empty when it does not fit.
"""
def __init__(self, width: int, height: int,
entry: tuple[int, int] = (0, 0),
exit: tuple[int, int] | None = None, perfect: bool = False,
seed: int | None = None) -> None:
"""Validate the parameters and prepare the single random source.
Args:
width: Number of columns, MIN_SIZE to MAX_SIZE.
height: Number of rows, MIN_SIZE to MAX_SIZE.
entry: Entry cell as (x, y).
exit: Exit cell as (x, y); None means the bottom-right cell.
perfect: True for a perfect maze, False for a Pac-Man board.
seed: Seed of the random source; None for a fresh one.
Raises:
MazeError: A parameter is impossible; the message names it.
"""
for name, value in (("width", width), ("height", height)):
if not MIN_SIZE <= value <= MAX_SIZE:
raise MazeError(f"{name} must be between {MIN_SIZE} and "
f"{MAX_SIZE}, got {value}")
if exit is None:
exit = (width - 1, height - 1)
for name, (x, y) in (("entry", entry), ("exit", exit)):
if not (0 <= x < width and 0 <= y < height):
raise MazeError(f"{name} ({x}, {y}) is outside the maze")
if entry == exit:
raise MazeError("entry and exit must differ")
if not perfect and (width - 1) * (height - 1) < 2:
raise MazeError(
"perfect=False needs (width - 1) x (height - 1) >= 2 for a "
f"Pac-Man board, got {width}x{height}")
pattern42 = Pattern42.for_size(width, height)
for name, (x, y) in (("entry", entry), ("exit", exit)):
if Cell(x, y) in pattern42:
raise MazeError(f'{name} ({x}, {y}) lies on the "42" pattern')
self._width = width
self._height = height
self._entry = Cell(*entry)
self._exit = Cell(*exit)
self._perfect = perfect
self._pattern42 = pattern42
self._rng = random.Random(seed)
self.grid: list[list[int]] = []
self.solution = ""
self.pattern: set[tuple[int, int]] = {
cell.as_tuple() for cell in pattern42.cells
}
def generate(self) -> None:
"""Build a new maze and fill grid and solution.
Every attempt and every call draws from the same random source, so
a seed repeats the whole sequence of mazes.
Raises:
MazeError: MAX_ATTEMPTS attempts in a row failed to build a
Pac-Man board.
"""
for _ in range(MAX_ATTEMPTS):
maze = Maze(self._width, self._height, self._pattern42)
Carver().carve(maze, self._entry, self._rng)
if self._perfect:
break
Braider().braid(maze, self._rng)
if (len(maze.dead_ends()) <= MAX_DEAD_ENDS
and maze.loop_count() >= MIN_LOOPS):
break
else:
raise MazeError("could not build a Pac-Man board after "
f"{MAX_ATTEMPTS} attempts")
self.solution = Solver().shortest_path(maze, self._entry, self._exit)
self.grid = maze.rows()
Experimente
O mazegen/__init__.py ainda tem só a docstring de uma linha da
parte 3, então os exemplos importam pelo caminho
completo, from mazegen.generator import MazeGenerator. A forma curta,
from mazegen import MazeGenerator, passa a funcionar na página 6.
Os atributos antes e depois de generate()
O código cria um gerador, olha os atributos antes e depois de generate(), gera um segundo
labirinto e compara com um gerador novo de mesma semente. Na raiz do projeto:
.venv/bin/python - <<'EOF'
from mazegen.generator import MazeGenerator
gen = MazeGenerator(width=20, height=15, seed=42)
print(gen.grid, repr(gen.solution), len(gen.pattern))
gen.generate()
print(len(gen.grid), len(gen.grid[0]), gen.solution)
first = gen.grid
gen.generate()
print(gen.grid == first)
again = MazeGenerator(width=20, height=15, seed=42)
again.generate()
print(again.grid == first)
EOF
[] '' 18
15 20 SSEEESSSSSENNNESSSSEESSSSEESSEEEENNESSEEEEE
False
True
- Antes de
generate(), sópatternestá preenchido: 18 células do "42". (reprmostra o texto vazio como'', para ele aparecer na saída.) - Depois,
gridtem 15 linhas de 20 números, esolutiontem o caminho. first = gen.gridnão copia a grade:firstpassa a ser outro nome para a mesma lista. A segunda chamada agenerate()não mexe nessa lista; ela guarda emgen.griduma lista nova, a quemaze.rows()devolve. Entãofirstcontinua com o primeiro labirinto, egen.gridpassa a ter o segundo.- A segunda chamada dá outro labirinto (
False): o sorteador continuou de onde parou. - Um gerador novo com a mesma semente repete o primeiro labirinto (
True).
Os parâmetros recusados
Cada recusa do construtor (página 3), uma por vez:
.venv/bin/python - <<'EOF'
from mazegen.errors import MazeError
from mazegen.generator import MazeGenerator
for kwargs in [
{"width": 0, "height": 15},
{"width": 20, "height": 101},
{"width": 20, "height": 15, "entry": (20, 0)},
{"width": 20, "height": 15, "exit": (0, 0)},
{"width": 2, "height": 2},
{"width": 20, "height": 15, "entry": (6, 5)},
]:
try:
MazeGenerator(**kwargs)
except MazeError as error:
print(error)
EOF
width must be between 1 and 100, got 0
height must be between 1 and 100, got 101
entry (20, 0) is outside the maze
entry and exit must differ
perfect=False needs (width - 1) x (height - 1) >= 2 for a Pac-Man board, got 2x2
entry (6, 5) lies on the "42" pattern
- A lista tem seis dicionários, um por tentativa.
kwargsé só o nome da variável, comum em Python para isto: vem de keyword arguments, "argumentos com nome". MazeGenerator(**kwargs): os dois asteriscos desmontam o dicionário e passam cada chave como um parâmetro com nome.{"width": 0, "height": 15}viraMazeGenerator(width=0, height=15).- Cada tentativa levanta um
MazeError, e oexceptimprime só a mensagem. - Em 20x15, o canto de cima à esquerda do "42" fica na célula
6,5: por isso a última entrada é recusada.