Using py_maze as a library
Every part of the game is a function or a class you can call. The grid it all passes around, a worked example, and the whole public surface tabled.
Every part of the game is a function or a class that can be called from your
own code. import py_maze reaches every public name whichever module it
lives in, so nothing has to be imported out of a submodule:
import py_maze
grid = py_maze.MazeGenerator(width=6, height=6, seed=2024).generate()
msvcrt, tty and termios are imported by py_maze.keys alone, so
every module that generates, solves, draws or saves a maze leaves the
terminal alone. Importing the package re-exports the game as well, so it
brings that one module with it, but no console is read or written at
import time, and a script that wants none of the terminal at all can
import py_maze.generation and py_maze.solving on their own.
The Grid
One type is passed between every name below. A maze is a grid: a list of
rows, each row a list of booleans, True for a wall and False for a cell
the player can stand on. grid[y][x] addresses row y, column x, and a
cell is always the pair (x, y).
A maze of W by H cells is carved into H * 2 + 1 rows of W * 2 + 1
booleans, the extra line in each direction being the wall between one cell
and the next. Every row is the same length: a ragged grid is not a maze.
>>> grid = py_maze.MazeGenerator(width=2, height=2, seed=1).generate()
>>> len(grid), len(grid[0]) # rows, then columns
(5, 5)
>>> grid[0][1] # the entrance, carved into the top row
False
The grid is built from nothing but lists and booleans, so it can be copied,
compared, pickled or written out with json.dumps as it stands. A maze
carved by MazeGenerator is the same object the solver walks, the renderer
draws and the save file writer writes, so nothing is converted between one
step and the next.
The entrance and the exit are not stored beside the grid, they are found in
it: find_entrance returns the open cell in the top row and find_exit the
one in the bottom row. That is why a maze read back from a file is used
exactly like a carved one, having nothing but its grid to go on.
A Worked Example
Generating a maze, solving it and drawing the solution, with no game and no keyboard involved:
import py_maze
# carve a 6 by 6 maze from a seed, so the same maze comes back every run
grid = py_maze.MazeGenerator(width=6, height=6, seed=2024).generate()
# the entrance and the exit are read out of the grid, not stored beside it
entrance = py_maze.find_entrance(grid)
exit_cell = py_maze.find_exit(grid)
# breadth-first search from the entrance to the exit, shortest route first
path = py_maze.solve_maze(grid)
print("%s to %s in %d cells" % (entrance, exit_cell, len(path)))
# draw the maze with the solution laid over it, one string per row
for line in py_maze.maze_lines(grid, py_maze.solution_overlay(path)):
print(line)
Output:
(1, 0) to (11, 12) in 35 cells
*.***********
*.* * *
*.*** * *** *
*...* * *
***.* *******
*...* *
*.*** ***** *
*...* * * *
***.* * *** *
*...* *...*
*.*******.*.*
*.........*.*
***********.*
That is the maze python -m py_maze -d easy --seed 2024 --solve prints,
since easy is 6 by 6 cells and the seed decides the rest.
solve_maze takes start and end cells too, each defaulting to the
entrance and the exit, so a route can be solved from wherever a player has
wandered to. It returns None when there is no way through rather than
raising, which is worth checking on a maze that came from a file:
path = py_maze.solve_maze(grid)
if path is None:
print("no way through")
Carving and Braiding
MazeGenerator looks the algorithm up by name and holds the seed, but a
carver can be called on its own: a width, a height and a random number
generator in, a carved grid out.
import random
import py_maze
# the names --algorithm reads from: ['backtracker', 'division', 'prim']
print(sorted(py_maze.ALGORITHMS))
# a carver on its own, and the same maze through the generator
grid = py_maze.carve_prim(8, 8, random.Random(7))
generator = py_maze.MazeGenerator(width=8, height=8, seed=7, algorithm='prim')
grid = generator.generate()
print(len(py_maze.solve_maze(grid))) # 35 cells, the only way through
# open every dead end, so there is more than one way through
py_maze.braid_maze(grid, 1.0, generator.random)
print(len(py_maze.solve_maze(grid))) # 31 cells, the shortest of them
py_maze.carver(name) returns the function a name stands for and raises
ValueError for a name no algorithm answers to, which is the same check
MazeGenerator makes when it is built. braid_maze modifies the grid it is
given and hands it back, so it can be wrapped around generate() or called
on a maze read out of a file.
Collectibles and Save Files
place_collectibles picks the cells to scatter pickups over, and drawing
the places from the generator’s own random numbers keeps them wherever the
seed put them. write_save and read_save round-trip a maze and its
pickups through a file:
import py_maze
generator = py_maze.MazeGenerator(width=6, height=6, seed=2024)
grid = generator.generate()
# the same places --collectibles would pick for this seed
collectibles = py_maze.place_collectibles(grid, 4, generator.random)
py_maze.write_save('maze.txt', grid, collectibles, seed=generator.seed)
grid, collectibles, seed = py_maze.read_save('maze.txt')
read_save hands back the grid, the cells holding a collectible and the
seed the file records, the seed being None for a file that records none. A
file that is not a maze this build can read raises SaveFileError, a
subclass of ValueError, with a message naming what was wrong. The format
is specified in the save file format.
The Names
Every name below is re-exported from the package, so py_maze.solve_maze
and py_maze.solving.solve_maze are the same function. Each carries a
docstring, so help(py_maze) and help(py_maze.solve_maze) describe the
surface, and py_maze.__all__ lists it in full.
Reading a grid (py_maze.grid)
| Name | What it does |
|---|---|
walled_grid(width, height) |
Build the solid block of wall a maze is carved out of |
find_entrance(grid) |
The (x, y) of the entrance, in the top row |
find_exit(grid) |
The (x, y) of the exit, in the bottom row |
open_cells(grid) |
Yield every cell the player can stand on, in reading order |
open_neighbors(grid, x, y) |
Yield the open cells one step from (x, y) |
open_ends(grid) |
Cut the entrance and the exit into a carved maze |
has_ends(grid) |
Whether a maze is wide enough to have an entrance and an exit |
MOVES |
The four steps a player, and the solver, can make |
MIN_DIMENSION |
The smallest maze with an interior path, 2 cells |
MIN_GRID_WIDTH |
The narrowest a maze can be drawn and still have both ends, 3 characters |
Generating (py_maze.generation)
| Name | What it does |
|---|---|
MazeGenerator(width, height, seed, algorithm) |
Carve a maze; generate() returns the grid |
braid_maze(grid, share, rng) |
Open a share of the dead ends, for more than one way through |
place_collectibles(grid, count, rng) |
The set of cells to scatter pickups over |
maze_seed(value) |
Read a seed from text, as --seed and a save file both do |
MAX_SEED |
The bound a seed is drawn from when none is given |
Carving (py_maze.algorithms)
Every carver is the same call, carve(width, height, rng), returning a
carved grid with its entrance and exit already opened:
| Name | What it does |
|---|---|
carve_backtracker(width, height, rng) |
Recursive backtracking: one winding route, long dead ends |
carve_prim(width, height, rng) |
Randomized Prim’s: a more open maze, short dead ends |
carve_division(width, height, rng) |
Recursive division: straight corridors and rooms |
carver(name) |
The carving function a name stands for, or ValueError |
ALGORITHMS |
The name --algorithm takes, mapped to the function that carves it |
ALGORITHM_NOTES |
What each one carves, in the words the help text uses |
DEFAULT_ALGORITHM |
The algorithm a bare run carves with, backtracker |
Solving (py_maze.solving)
| Name | What it does |
|---|---|
solve_maze(grid, start, end) |
The shortest route as a list of cells, or None |
search_frames(grid, start, end) |
Yield (visited, frontier, path), one wave at a time |
Drawing (py_maze.rendering)
| Name | What it does |
|---|---|
maze_lines(grid, overlays) |
The maze as one string per row |
print_maze(grid, overlays, stream) |
The same, written between the start and end markers |
solution_overlay(path) |
The overlay that draws a solution over a maze |
collectible_overlay(collectibles) |
The overlay that draws pickups over a maze |
animate_search(grid, start, end, ...) |
Step the search across a terminal, frame by frame |
status_line(...), summary_lines(...) |
The tallies shown during play and at the end |
format_duration(seconds) |
A length of time written the way a stopwatch would |
terminal_size() |
The screen the maze will be drawn in |
fit_to_terminal(...), fit_dimension(...) |
Cap a maze to the space there is for it |
fit_frame(lines, size, focus) |
Cut a play screen to the rows the terminal has, around a row to keep |
frame_text(lines, home, stream) |
A whole frame as the one string that draws it |
frame_diff(previous, current, whole) |
The same for the lines that changed, or every row when whole |
frame_wraps(lines, size) |
Whether a frame runs past the terminal’s last column |
clear_screen, ansi_enabled, can_encode |
The rest of the escape sequence machinery behind a redraw |
An overlay is a (marker, cells) pair, and maze_lines takes a sequence of
them running from the most important marker to the least: the first pair
holding a cell decides what is drawn there. The markers themselves are named
constants, so a caller need not repeat the characters: WALL_MARKER,
OPEN_MARKER, PLAYER_MARKER, SOLUTION_MARKER, VISITED_MARKER,
FRONTIER_MARKER, HINT_MARKER and COLLECTIBLE_MARKER.
Save files (py_maze.saves)
| Name | What it does |
|---|---|
write_save(path, grid, collectibles, seed, solution, form, stream) |
Write a maze to a file, in either form |
read_save(path, chars, stream) |
Read one back as (grid, collectibles, seed) |
save_lines(grid, collectibles, seed) |
The file’s lines, without writing them |
save_json(grid, collectibles, seed, solution) |
The document it would write under form='json' |
parse_save(text, source, chars) |
The same read, from text already in hand |
parse_json_save(text, source) |
The same again for a document, which parse_save calls for one |
picture_chars(wall, open_cell) |
The chars map a picture with no header is read with |
SaveFileError |
Raised for a file this build cannot read |
SAVE_FORMAT, SAVE_HEADER, SAVE_CHARS |
The format number, its header line and the characters it allows |
FORMATS, TEXT_FORMAT, JSON_FORMAT, DEFAULT_FORMAT |
The two forms a maze is written in, and the one written unasked |
JSON_FORMAT_KEY |
The key a document carries the format number under |
STDIO_PATH |
The file name that means standard input or standard output, - |
STDIN_NAME |
What a message calls that stream, <stdin>, there being no file name |
write_save and read_save take the file name - for standard output and
standard input, or a stream of your own in its place, which is what makes
them testable without a pipe. parse_save reads a document, a picture under
its header or a picture with no header at all, and hands back the same three
things whichever it was:
import py_maze
grid = py_maze.MazeGenerator(width=6, height=6, seed=2024).generate()
# the same maze as a document, with a route through it recorded
document = py_maze.save_json(grid, seed=2024,
solution=py_maze.solve_maze(grid))
grid, collectibles, seed = py_maze.parse_save(document)
# and a picture somebody else drew, on the terms it was drawn on
grid, _, _ = py_maze.parse_save("#.#\n#.#\n",
chars=py_maze.picture_chars('#', '.'))
The terminal half is public too: MazeGame plays a maze at the console,
read_key and read_response take single keypresses, read_key_timed waits
a given moment for one and answers None when the moment passes empty, or
waits however long it takes when the moment it is given is None itself, and
build_parser, build_maze and main are the command line itself, along
with the EXIT_OK, EXIT_USAGE, EXIT_SAVE_FILE, EXIT_FILE_ERROR and
EXIT_NO_WAY_THROUGH codes it exits with. Those are the names that want a
terminal. Everything above runs without one.