py_maze py_maze
Source

Command-line options

Every flag py_maze takes, the short form beside it, what it defaults to and what it changes.

Option Short Default Description
--width -w from the preset Width of the maze in cells (minimum 2)
--height -H from the preset Height of the maze in cells (minimum 2)
--difficulty -d normal Preset maze size: easy, normal or hard
--algorithm -A backtracker How the maze is carved: backtracker, prim or division
--braid -b 0 Open this share of the dead ends, from 0 for none to 1 for all
--seed -s chosen at random Seed for the maze generator
--collectibles -c 0 Scatter this many pickups through the maze
--mode -m plain How the maze is played: plain, chase or quest
--chase-point   55 Share of the maze walked before the chase begins, from 20 to 90
--chase-speed   2 How fast the chaser moves, from 0 for the slowest preset to 5
--quest-stages   3 How many doors a quest shuts the way on with, from 1 to 9
--save -o   Write the maze to a file so it can be played again, or to standard output as -
--load -l   Play the maze saved in a file instead of generating one, or in standard input as -
--wall-char   * Character a wall is drawn with in a loaded file that carries no py_maze header
--open-char   space Character an open cell is drawn with in the same
--format -f text How the maze is written: text, the picture, or json, a document
--solve -S off Print the solution path overlaid on the maze
--animate -a off Step through the solver’s search on screen
--stats   off Print what the maze measures under it: cells and open over the drawn picture, then dead ends, junctions, longest corridor, solution, the share of the maze it walks and a difficulty score
--quiet -q off Print the maze and nothing else: no banner, no seed line, no prompt
--version -V   Show the installed version and exit
--help -h   Show usage and exit

Note that the short flag for height is a capital -H. Lowercase -h is reserved by argparse for --help. The same goes for -S and -s: capital -S solves the maze, lowercase -s seeds it, and capital -A picks the algorithm. The short flag for --save is -o, as in an output file, since -s is already the seed. --wall-char and --open-char have no short flags: they are read by a loader rather than typed at a prompt. -m is the mode; the chase options and --quest-stages have no short flags, being settings for a mode rather than everyday typing. --stats has none either, there being no letter left that reads as it: -s is the seed and -S solves.

--stats prints under the maze rather than into it, so the picture is the one a run without it draws. A quiet run still reports the measurements, because a flag that was asked for is not what --quiet drops, and this is how --solve is already read. A --format json run prints the document alone, carrying the same measurements under its stats key instead of printing them underneath.

A run that braided reports one tally the others do not: how many dead ends --braid opened, beside how many are left. Reading a maze sets out every measurement, and what the difficulty score is made of.

A maze is drawn with walls between cells, so a maze of W by H cells renders as W * 2 + 1 characters wide and H * 2 + 1 characters tall.

--stats counts cells and open over that drawn picture rather than over the W by H that was asked for, so python -m py_maze -d easy --stats reports cells 169 for a maze of 6 by 6 cells: 13 characters each way, walls counted alongside the positions the player can stand on. Every other tally reads the same picture, which is the maze the solver crosses and the player walks.

Values below 2 cells cannot produce a maze with an interior path, so they are rejected:

python -m py_maze -w 1

Output:

usage: py_maze [-h] [--width WIDTH] [--height HEIGHT]
               [--difficulty {easy,normal,hard}]
               [--algorithm {backtracker,prim,division}] [--braid [SHARE]]
               [--seed SEED] [--collectibles COUNT] [--mode {plain,chase}]
               [--chase-point PERCENT] [--chase-speed PRESET] [--save FILE]
               [--load FILE] [--wall-char CHAR] [--open-char CHAR]
               [--format {text,json}] [--solve] [--animate] [--stats]
               [--quiet] [--version]
py_maze: error: argument --width/-w: maze dimensions must be at least 2 cells, got 1

Difficulty Presets

Each preset is just a maze size, so picking one is a shorter way of passing --width and --height:

Preset Size in cells Rendered size
easy 6 by 6 13 by 13 characters
normal 9 by 11 19 by 23 characters
hard 16 by 20 33 by 41 characters
python -m py_maze --difficulty hard

normal is the size py_maze has always generated, so a run with no options is unchanged. Either dimension can still be set by hand, and doing so overrides that half of the preset:

# a hard maze, but only 8 cells tall
python -m py_maze -d hard -H 8

Fitting the Terminal

A maze larger than the screen scrolls out of view and cannot be played, so sizes are measured against the terminal before the maze is generated. When a requested size does not fit, it is capped to the largest one that does and a warning explains the change:

python -m py_maze -w 60 -H 60

Output (on an 80 by 24 terminal):

warning: --width 60 needs 121 columns but only 80 are available; using 39
warning: --height 60 needs 121 rows but only 19 are available; using 9

Five rows are reserved for the start and end markers, the status line, the spacer and the controls line, which is why the height allowance is smaller than the terminal is tall.

Those five lines stand six rows tall on a terminal too narrow for the controls line, which carries it onto the row below. The reserve counts the rows rather than the lines, so a narrow terminal gives up one more cell of height and the maze it carves is still one the terminal can show whole:

python -m py_maze -w 60 -H 60

Output (on a 50 by 24 terminal):

warning: --width 60 needs 121 columns but only 50 are available; using 24
warning: --height 60 needs 121 rows but only 18 are available; using 8

The line is the mode’s, so a quest reserves the rows its own longer line needs: the plain line is 60 characters and the quest one is 79, and there are widths where the two differ by a row of maze.

The status line is counted the same way, and it is the other line of those five that can outrun the terminal. How wide it will be is settled before the maze is carved: a collectible tally is on it whenever --collectibles asks for any, and an inventory tally whenever the mode fills one, so --mode quest --collectibles 9 draws 47 characters before a key has been pressed. On a terminal narrower than that it takes a second row, and the reserve is seven rows rather than six:

python -m py_maze -w 60 -H 60 --mode quest --collectibles 9

Output (on a 40 by 24 terminal):

warning: --width 60 needs 121 columns but only 40 are available; using 19
warning: --height 60 needs 121 rows but only 17 are available; using 8

Only the maze rows are measured against the terminal’s columns, which is why these two lines are the ones that wrap: --width caps a maze row to the columns there are, and nothing caps the foot. The start and end markers and the spacer never reach the edge of a terminal with room for a maze in it.

Two limits apply to the capping:

  • Mazes are never capped below the 2 cell minimum. If the terminal cannot hold even the smallest maze, the requested size is generated as asked and the warning says the maze will not fit on screen.
  • Nothing is capped when the output is piped or redirected, since there is no terminal to fit. Writing a large maze to a file works exactly as before:
python -m py_maze -w 60 -H 60 > maze.txt

A difficulty preset is capped the same way, so --difficulty hard on a small terminal generates the largest hard-ish maze that fits.

Game Modes

A mode is the maze that is already there played differently: the same grid, the same solver, the same picture and the same keys. A run naming no mode plays exactly as py_maze always has.

Mode Playing it
plain Walk from the entrance to the exit
chase The same walk, with something following you
quest The way on is shut until you find what opens it
python -m py_maze --mode chase

An option belonging to a mode is ignored by every other one, so passing --chase-point to a plain game does no harm and changes nothing.

Game modes is the page for all three: what each one plays like, what each one draws and the options belonging to each. What follows here is how the options themselves read a value.

Chase Mode

Nothing follows the player at first. Once they have walked far enough into the maze, a chaser appears at the entrance and starts after them, drawn with an X. It walks the route the solver finds from itself to wherever the player is standing, so it never crosses a wall and it turns when they turn.

Being caught ends the run the way the exit does: the same tallies, with a line naming which of the two happened.

--chase-point is how far in the chase begins, as a share of the maze the player has walked. It is read off the solution measured as the straight runs it is made of, so a route of four runs of 4, 2, 5 and 3 steps totals 14 and the default of 55 begins the chase 7.7 steps along it:

# start the chase a fifth of the way in, and give it the fastest preset
python -m py_maze --mode chase --chase-point 20 --chase-speed 5

--chase-speed names one of six preset speeds rather than a delay. The preset is the moves the chaser makes in a second: 0 is one a second, rising by one to 5 at six a second.

Given Read as
A number inside the range Itself
A number under the range The bottom of it, 20 or 0
A number over the range The top of it, 90 or 5
A decimal The nearest whole number, 60.5 becoming 61
Something that is not a number A notice, and the run carries on without it

The last of those is not an error and does not stop the run:

python -m py_maze --mode chase --chase-point a34

Output on standard error:

py_maze: --chase-point: 'a34' is not a number, carrying on without it

Quest Mode

The way on is shut by a door, drawn #, and what opens it is in a box, drawn &, hidden in the part of the maze the player can already reach. A guide, drawn @, waits just inside the entrance and says what the next errand is when n is pressed.

--quest-stages is how many door-and-box pairs the chain runs to, from 1 to 9:

python -m py_maze --mode quest --quest-stages 5

It shapes the maze before a word is drawn, the way --collectibles and --braid do, so unlike the chase options it is refused rather than settled into its range:

python -m py_maze --mode quest --quest-stages 40

Output on standard error:

py_maze: error: argument --quest-stages: a quest runs from 1 to 9 stages, got 40

The maze is then played with the default chase point, exactly as though the option had not been given. The notice goes to standard error, so a --quiet run’s standard output is still the maze and nothing else.

Nothing is walked by the chaser until the player is at least a fifth of the way in, which is what stops it reaching them the moment it appears, and the default speed is one a player who keeps moving matches without hurrying. Standing still is what a chase punishes.

Status codes

A run that could not do what it was asked exits with a code naming what went wrong. Those are tabled under Scripting py_maze, along with everything else a script needs.