How to play
The keys, the status line, the end-of-game summary, and what the screen is doing between one move and the next.
- Run
py_mazefrom your terminal - A random maze will be generated and displayed
- Choose whether you want to play (press ‘y’ for yes, ‘n’ for no). The answer is a single keypress on every platform: there is no Enter to press
- If you choose to play:
- Use arrow keys or WASD to move your character (
o) - Navigate from the start (top) to the end (bottom)
- Walk over any collectibles (
$) to pick them up - Press ‘h’ for a hint when you are stuck
- Press ‘q’ to quit at any time, or Ctrl+C to interrupt
- Use arrow keys or WASD to move your character (
--mode chaseplays the same maze with something following you, and--mode questshuts the way on until you find what opens it. See game modes for all three and the options that go with them
Controls
- Arrow Keys or W/A/S/D: Move up/left/down/right
- H: Show a hint for a moment
- N: Ask the guide what the next errand is, in quest mode
- Q: Quit the game
- Ctrl+C: Interrupt the game
N does nothing outside quest mode, there being nobody to ask. Quest mode says so on its own controls line, which lists it beside the other keys.
While the game is running, the terminal is put into raw mode so single keypresses can be read without waiting for Enter. The same goes for the “would you like to play” prompt, which takes one keypress and leaves nothing behind in the input buffer. Raw mode also means Ctrl+C arrives as an ordinary keypress rather than as an interrupt signal, so the game handles it itself: the terminal is restored to its normal mode and the game exits with a goodbye message instead of a traceback.
An answer piped in rather than typed has no terminal mode to set, so the prompt reads it straight from the pipe.
The Timer and the Move Counter
A status line under the maze reports how long the game has been running and how many steps have been taken. When the maze holds collectibles, it counts those too:
start
* ***
*o$ *
*** *
* *
*** *
end
time 0:12 moves 8 collected 1/2
Use arrow keys or WASD to move. 'h' for a hint, 'q' to quit.
Only steps that moved the player are counted, so walking into a wall costs nothing but the time it took. Asking for a hint is not a move either.
The two tallies are independent of each other, both ways round. The game comes back to the screen four times a second whether or not a key was pressed, so the clock counts on while the player stands still: a second that passes with nothing pressed is a second and no move, exactly as a step into a wall is a move that never happened. A screen with nothing new on it is still written as nothing, so three of those four visits leave the terminal alone - the status line counts whole seconds, and only the one the second turns over on has anything to say.
Reaching the exit prints the same tallies as an end-of-game summary:
🎉 Congratulations! You solved the maze! 🎉
Time: 1:12
Moves: 84
Collected: 3 of 4
Press any key to exit...
The clock stops the moment the maze is won, so the summary reads the same
however long it is left on screen. Quitting with q prints the summary for
the game so far, and the Collected line is left out of a maze that had no
collectibles in it.
The summary is printed into rows kept back for it rather than onto whatever
is left under the maze. A console the play screen fills has nothing left:
every line printed under a full screen takes the screen up a row as it goes
out, and the start marker at the top of the frame is what goes. The maze
window gives up the rows the summary needs instead, exactly as it does for a
console shrunk under a running game, so the maze and the tallies are read
together however tight the fit.
Interrupting with Ctrl+C is given the same rows. Its goodbye is two lines rather than a summary, so it asks the maze window for fewer of them, but it is the third way out of the same maze and nothing is drawn after it: a scrolled frame would be the last thing on screen. An interrupt pressed on a summary already waiting for a key asks for two rows more than that summary was given: the maze window gives up whichever of them it still has, and the summary and the goodbye are written back together. Cutting the frame for both moves its foot up by the rows it gave, so the tallies are rewritten that far higher than they were being read - on a console still drawing the maze, two rows where nothing was spare under the summary, one where a single row was and none at all once there were two - and stay whole and on screen, with nothing scrolled off the top to pay for them.
A console that cannot draw the party poppers gets the plain congratulations instead, so the message arrives whatever the terminal can show:
Congratulations! You solved the maze!
That covers two different terminals. A legacy code page cannot encode the
glyph at all; a classic console window - what cmd.exe and a desktop
shortcut open - encodes it perfectly well and still cannot draw it, because
a cell of its screen holds too little to store a character that wide and
shows a replacement character in its place. Both get the plain wording.
Windows Terminal announces itself in an environment variable, and a terminal that announces itself is taken at its word: it is documented as drawing a character that wide, so it gets the glyphs. That is the one thing here read off the documentation rather than measured on the screen, because Windows Terminal is a packaged application and will not start from the unattended runs this project checks itself with.
Every other Windows console reads plainly. The test is the announcement and
nothing else, so a terminal emulator that is neither the classic host nor
Windows Terminal - ConEmu, or an editor’s integrated terminal - is handed
the plain wording even where it could have drawn the glyphs. That is the
side of the guess that costs a reader nothing: the message arrives either
way, where guessing the other way would put two replacement characters on
the screen. Setting WT_SESSION yourself is what overrides it.
Chase Mode
--mode chase is the same maze, the same keys and the same summary, played
with an antagonist behind the player. Nothing follows them at first: the
chaser waits at the entrance until they have walked far enough in, then
appears as an X and starts after them.
start
* ***
*o *
*** *
*X *
*** *
end
time 0:09 moves 6
Use arrow keys or WASD to move. 'h' for a hint, 'q' to quit.
It walks the route the solver finds from itself to wherever the player is standing, worked out again on every step, so it never crosses a wall and it turns when they turn. It moves on the clock rather than on the keyboard, at a preset number of moves a second, which means standing still is what a chase punishes: the maze is where it was, and the chaser is not.
Being caught ends the run the way the exit does, on the same screen with the same tallies. What tells them apart is a line naming which of the two happened:
💀 Caught! The chaser reached you. 💀
Time: 0:48
Moves: 61
Outcome: caught by the chaser
Press any key to exit...
A chased summary is a row taller than a plain one for carrying that line, and the maze window is a row shorter on a console that has no spare row for it.
Walking out instead reads Outcome: reached the exit under the same
congratulations the plain game prints. The line belongs to the chase: a
plain game has one way out, so a summary saying which was taken would say
nothing, and it is left off. Quitting is neither of the two, so it is left
off there as well.
A console that cannot draw the skulls gets Caught! The chaser reached
you. on its own, on the same terms as the congratulations above: a code
page that cannot encode them and a console window whose cells cannot hold
them both read the plain wording.
How far in the chase begins and how fast the chaser moves are both settings:
--chase-point takes a share of the maze from 20 to 90, and --chase-speed
one of six presets from 0 for one move a second to 5 for six. Left alone
they are a point past halfway and a speed a player who keeps moving matches
without hurrying, so the chaser alone never catches somebody still walking.
The two are covered in full under
game modes.
Quest Mode
--mode quest is the same maze with the way on shut. A door (#) is a
cell you cannot pass yet rather than a corridor, and it is drawn heavier
than the * wall beside it to say so: it stays closed until the matching
item is carried,
and the item is in a box (&) hidden in the part of the maze that can
already be reached:
start
* ***
*o& *
***#*
* *
*** *
end
time 0:07 moves 3 items 0/1
Use arrow keys or WASD to move. 'h' for a hint, 'n' for the guide, 'q' to quit.
Walking onto the box opens it. Walking into the door before that costs nothing - the step is refused the way walking into a wall is, and no move is counted - and walking into it afterwards opens it for good.
The status line carries the inventory while the maze holds a quest, and the summary reports it the way it reports collectibles:
🎉 Congratulations! You solved the maze! 🎉
Time: 1:04
Moves: 88
Items: 3 of 3
Press any key to exit...
The maze has one way out, so there is no outcome line: finishing a quest is reaching the exit, and the exit is behind the last door.
A guide (@) waits just inside the entrance. Pressing n asks it what
the errand in front of you is, from anywhere in the maze: it lights the cell
up the way a hint does, and says what it is where the controls line usually
sits. The cell comes down a moment later and the words stay for six seconds,
which is long enough to read a sentence; the maze is still being played
under them either way. Game modes has the whole of it,
including what --braid does to a quest.
Hints
Pressing h during play lights up the next step along the solution with a
?, then redraws the maze without it a moment later:
start
* ***
*o? *
*** *
* *
*** *
end
time 0:04 moves 1
Use arrow keys or WASD to move. 'h' for a hint, 'q' to quit.
The path is solved from wherever the player is standing rather than from the entrance, so a hint still points the way after a wrong turn, and asking for one in a dead end points back out of it. At the exit there is nothing left to hint at, so nothing is highlighted.
n is a second kind of hint rather than a second way to ask for the first. h points at the next step of the way to the exit; the guide points at the box or the door the quest wants next and says nothing about where the exit is.
Redrawing the Screen
The first frame is drawn whole, in a single call, so the maze, the status line and the controls arrive together and the screen never stands empty part-way through a redraw.
Every frame after it draws only what moved. The frame that would be shown is compared line by line against the frame already on screen, and each line that differs is written on its own, addressed by the row it belongs on. A step rewrites the maze rows it touched - two for a step up or down, one for a step left or right - and the tally that counted it; the blank spacer and the controls line are left standing, because neither has changed since the game began. A step into a wall changes nothing, so nothing is written at all, and a hint lights up one row rather than redrawing the screen to show it and redrawing it again to take it away.
Every line is addressed by the row it belongs on, the first frame’s included, and no newline is written at all. A newline written on the bottom row of the screen scrolls everything up one, and a frame as tall as the terminal ends on that row - which is what a maze capped to fit any terminal with an even number of rows draws, the lines the frame adds around the maze leaving no row for the cursor below it. Addressing the rows rather than ending the lines is what keeps the row a line was drawn on true for the rest of the game.
When the Screen Is Shorter Than the Frame
A terminal has no row below its last one to carry anything onto: an address past the bottom of the screen lands on the bottom of the screen. So a frame taller than the console is not simply cut off at the fold - its exit marker, its tally, its blank spacer and its controls line are each written onto the last row in turn, and the one that reads is whichever went out last.
The maze is capped to the terminal when it is generated, so this is a
console shortened afterwards: a window dragged smaller under a running game,
or a maze read out of a save file that was drawn on a bigger screen than the
one playing it now. The frame is therefore cut to the console it is being
drawn on rather than to the one the maze was carved for, and what a short
console costs is maze rather than the foot of the screen. The maze rows
drawn are a window onto the maze, and the window follows the player, so the
row being played on is on screen however little of the maze is. The end
marker, the tally, the spacer and the controls line stay on the bottom four
rows, where a player reads them.
A window says that it is one. start and end are the only labels on the
play screen and they name the two ends of the maze, so on a cut window they
would be sitting against whichever interior rows the window happened to stop
at - and the exit is the gap in the bottom wall, which can then be off screen
for the whole of a game with nothing saying so. An edge the window does not
reach is named with the rows between the two instead:
start 2 rows above
* * *
* *** ***** *
* * * * *
end 3 rows below
time 0:12 moves 9
Use arrow keys or WASD to move. 'h' for a hint, 'q' to quit.
Both markers read as the bare words on a console with rows to spare, which is every console that can hold the whole maze.
Give the console its rows back and the frame grows into them again, since the terminal is measured every frame rather than once at the start.
When the Row a Line Sits on Cannot Be Trusted
Addressing a row holds only while the first line of the frame is on the first row of the screen, and the game not scrolling the screen itself is not the same as nothing scrolling it. Two things still do, and neither of them shows up in the lines of a frame:
- The window is resized. Shrinking a terminal pushes its content up, and nothing tells the game it happened.
- A line is wider than the screen. Only the maze is measured against the
terminal’s columns, so the two long lines under it are the ones that can
run past the last one. The controls line is 60 characters, 79 in quest
mode, and no option makes either shorter; the status line grows with what
it has to tally, reaching 47 characters for a quest played with
--collectibles 9. A terminal narrower than either carries it onto the row below it, and on a screen the frame fills that row is the bottom one, where the carry takes the whole screen up exactly as a newline would. The maze a run carves is sized with the rows both lines will actually occupy, so a console wide enough for the mode being played gets a maze it can show whole; the window above is for the widths where no reserve can make one fit.
Both of those lines are broken on a space rather than in the middle of a
word. A console wraps on the column it runs out of, so the guide’s answer in
quest mode read as ...you can already r with each underneath it, and the
quest tally on a 29 column console read as collect with its number on the
row below; each line is padded out to the end of its row instead, which
leaves the next word to start the row below and costs the maze nothing. A
line whose words need more rows than the console’s own break does keeps that
break: the padding is only ever free when it fits in the rows the line was
already occupying, and a maze row is the worse thing to spend.
The break can fall inside a run of spaces rather than on a single one, and
the rest of that run goes with it. The tally separates its fields with three
spaces, so a 20 column console breaks it on the second of the three; the
spaces still to come would have led the row below and pushed collected 0/1
out of line with the field above it, and the row starts on its word instead.
Which space the break falls on is a choice of its own, because the tally has
two kinds of them. Three spaces separate one field from the next and a single
space separates a field’s name from its value, so the last space that still
fills the row is as likely to be inside a field as between two of them: a 44
column console used to break the quest tally after items and leave 0/3
alone on the row below. The run between two fields is the break that is
taken wherever the row holds one, which is what keeps a name and its value
together. The exception is a run early enough in the row to leave it well
short of the last column, where breaking there would need a row the console’s
own break did not - 17 columns is where that happens - and there the single
space is kept and the field is parted after all. A maze row is still the
worse thing to spend.
A line carried onto the row below also keeps that row. The lines of a frame are addressed by the screen rows they belong on rather than by their place in the frame, so the blank spacer under a wrapped status line is written below the row the tally was carried onto rather than over it.
So the terminal is measured once a frame. When its size has changed since the last frame, or when a line of the frame runs past its last column, the whole frame is drawn rather than the difference: every row is written, so the picture is put back wherever it drifted to rather than being repaired one changed line at a time. A frame with nothing new to say is still written as nothing, so a still screen on a narrow terminal stays still.
A frame too wide for the screen is redrawn whole every time it changes, which is the best a terminal with no room for the line can be given: the picture is the frame it ought to be, sitting one row higher than it would on a terminal wide enough to hold it. Output that is piped or redirected has no size to compare against and no last column to run past, so it is drawn exactly as it always was.
Terminals that read escape sequences are drawn on this way, which is every
terminal on Linux and macOS and every Windows console that takes virtual
terminal processing (Windows 10 and later). Where the escapes would be
printed as text instead, py_maze falls back to clearing the screen through
cls or clear and writing every frame whole, exactly as it always did.
Setting TERM=dumb forces the fallback.
The same escape wipes the screen between the frames of --animate, so an
animated search no longer starts a shell for every frame it draws.
Example Maze
start
**** ************
* * * *
**** * *** * ****
* * * *
* ****** *** ****
* * * * *
* * **** ***** **
* * * *
* ******** ******
* *
**************** *
end