py_maze py_maze
Source

Generating a maze

How a maze is carved, how to open its dead ends, how to get the same maze back tomorrow, and how to scatter pickups through it.

Carving Algorithms

--algorithm picks how the maze is carved. All three leave exactly one route between any two cells, so every maze is solvable whichever one carves it; what changes is the shape of the corridors:

Algorithm Short What it carves
backtracker -A backtracker One winding route, with long dead ends. The default
prim -A prim A more open maze, with short dead ends
division -A division Straight corridors and squared-off rooms
python -m py_maze -d easy --seed 2024 --algorithm prim

Output:

start
* ***********
*           *
* ********* *
*         * *
*** *********
*   *       *
*** * *******
*   *       *
*** * *******
*           *
*** * * * ***
*   * * *   *
*********** *
end

Prim’s grows the maze outward from one cell, drawing each step at random from the whole of the growing edge rather than from wherever the last step landed. It spreads evenly, so it branches often and its dead ends are short.

python -m py_maze -d easy --seed 2024 --algorithm division

Output:

start
* ***********
*   *       *
*** * *** * *
*   *   * * *
*** ******* *
*   *     * *
*** * * * * *
*   * * *   *
*** *** *** *
*   *     * *
* ******* ***
*           *
*********** *
end

Recursive division works the other way about from the other two. Rather than carving passages out of solid wall, it starts from an empty floor and builds a wall the whole way across it, leaving one gap to cross by, then divides each half the same way until what is left is a corridor one cell wide. Each wall runs straight, which is where the long runs and the squared-off rooms come from.

backtracker is the algorithm py_maze has always carved with, so a run without --algorithm is unchanged.

Braiding

A carved maze is a perfect maze: one route between any two cells, and every wrong turn ends in a dead end. --braid opens a share of those dead ends, knocking out one wall apiece so each joins the corridor behind it. The maze then has more than one way through, and --solve reports a shortest way rather than the only one:

python -m py_maze -d easy --seed 2024 --braid --solve

Output:

start
*.***********
*.....  *   *
* ***.* * * *
*   *.*     *
*** *.*******
*   *.......*
* *** *****.*
*   * *    .*
*** * * ***.*
*   *   *  .*
* ******* *.*
*         *.*
***********.*
end

That is the same maze --seed 2024 carves without --braid, where the only route through runs 35 cells. Opening its dead ends leaves a shortest route of 23.

The share runs from 0 for none of the dead ends to 1 for all of them, and --braid on its own means 1:

# open a quarter of the dead ends, for a maze with a few loops in it
python -m py_maze --braid 0.25

Braiding is applied to a maze as it is generated, so it does not apply to a maze read back with --load: that maze comes out of the file exactly as it went in, braided or not.

Repeating a Maze

Every run reports the seed its maze was generated from:

seed: 2024

Passing that seed back generates exactly the same maze, so a maze worth keeping does not have to be planned for in advance:

python -m py_maze --seed 2024

A seed can be a number or a word, whichever is easier to remember:

python -m py_maze --seed winter

The same seed only reproduces the same maze at the same size, since the size decides how many turns the generator takes. The same goes for --algorithm and --braid: each draws its own random numbers from the seed, so a seed reproduces a maze only alongside the options it was carved with. Pair a seed with --difficulty, or with --width and --height, and with whichever of those two the run used, to get the identical maze back.

Collectibles

--collectibles scatters that many $ markers through the maze for the player to pick up on the way past:

python -m py_maze -d easy --seed 2024 --collectibles 4

Output:

start
* ***********
* *     *   *
* *** * *** *
*   * *     *
*** * *******
*   *       *
* *** ***** *
*   *$*$  * *
*** * * *** *
*   * $$*   *
* ******* * *
*         * *
*********** *
end
seed: 2024

Nothing is scattered unless the option is given, so a run without it is unchanged. The places are drawn from the same seed as the maze, so the command above puts the collectibles in those cells every time it is run.

Every cell the player can stand on is a candidate, corridors as well as junctions, apart from the entrance and the exit. Those two are left clear so nothing is handed over before the first step or after the last. Asking for more than the maze has room for simply fills every cell there is.

Collectibles are drawn over a solution path rather than under it, so a maze printed with --solve still shows what there is to pick up along the way.

Picking one up is a matter of walking onto it. The running tally sits under the maze while the game is played, and the final one is part of the end-of-game summary.