Zaytun Handbook

Playing a compiled game

A .zaytunb plays in either of two players. They are separate implementations of one runtime, kept identical by a test that plays a seeded game through both and diffs the transcripts, so the same game answers the same way in both — and a save written by one loads in the other.

The exceptions are the lines where the interface itself differs, because each host names its own controls:

terminalbrowser
the last line of HELPType QUIT to stop playing.— the browser has a Restart button instead
a save with no game name on itThat save is not for "Y".That save is not for this game.

The ending is not one of them. Both say The story has ended. Restart to play again., and a save that names a different game is refused in the same words by both — That is a save for "X", not for "Y". The ending line used to name each host's own restart control, which was harmless while it only appeared after a command nobody scripted and became a difference in every transcript the moment it appeared at the ending itself. Worth knowing if you script a game through both and diff the transcripts, as this project does.

In a browser

The browser player is a separate program in its own repository — zaytun-web-player — and there is nothing to install to use it. It is hosted at https://zaytun-project.codeberg.page/zaytun-web-player/: open it, choose your .zaytunb with the file picker, and play. Nothing is uploaded; the page reads the file in your browser and the game runs there.

If you would rather run it yourself, clone that repository and open its index.html. It is static HTML and JavaScript with no build step and no dependencies, so it works from disk or from any web server you like. One caveat when opening it straight off disk: browsers will not let a file:// page read another file on its own, so the file picker is the way in. The player says so rather than showing a bare error.

In a terminal

make -C zaytunplay
./zaytunplay/zaytunplay mygame.zaytunb

The player takes commands from you until quit, exit or end of input. It also runs them non-interactively, which is how you test a game while writing it:

./zaytunplay/zaytunplay mygame.zaytunb -c "use stone" -c "north" -c "east"

The line you type is editable, and the player remembers what you typed before:

↑ ↓the previous and next command you entered
← →move within the line
Home End, or ^A ^Ejump to either end
Backspace, Deleteremove a character either side of the cursor
^U ^Kclear the line, or clear from the cursor to the end
^Cabandon the line and start again
^D on an empty linequit, as end of input always has
Tabfinish the word: a command, or something you can name from here

Tab completes against the game, not against a word list. On the first word it offers the commands this game understands — what HELP would name, the game's own verbs, the ways out of this room, and a vehicle's own words while you are inside her. After that it offers what you could name from where you are standing, chosen by the verb:

> take la<Tab>                  lamp, lantern -- the words this lantern answers to
> open <Tab><Tab>
chest
> ask smith about <Tab><Tab>
armour  shield

One candidate finishes the word and adds a space; several finish as far as they agree, and Tab again prints them in columns. It never offers what the game would then refuse: a thing in a dark room, a spanner lying outside the car you are sitting in, a lantern to OPEN, or a command the game has hidden with hide. The lists are the runtime's own — what EXAMINE resolves against, what TAKE looks at, whom you can reach — so they cannot drift from what the verbs do.

zaytunplay --complete "take la" prints the same list on standard output and exits, which is how another front end can ask, and how the suite checks it without a terminal.

History lasts for the session and is not written to disk — the player leaves no file in your home directory. Multi-byte characters count as one character, so backspacing over the ú in talk lúthien removes the letter rather than half of it.

None of that applies when the input is not a terminal. Piped input, a here-document and -c behave exactly as they would with no line editing at all, because a script is not an interactive session and the escape sequences that redraw an edited line would corrupt its output.

Saving

In the terminal, save and restore take an optional path and default to zaytun.save:

> save
> save saves/before-the-tower.json
> restore saves/before-the-tower.json

In the browser, a save is a JSON string you copy out and paste back — there is no localStorage dependency, so it behaves the same everywhere, including from a file:// page.

Both write the same shape, so a save made in one player restores in the other. A save records which game it came from and a player refuses one belonging to a different game, rather than loading it and stranding you in a world the save does not describe:

> restore x.save
That is a save for "Game A", not for "Game B".

What it records is the title:, not the file. So a save survives recompiling, adding rooms, and fixing prose — but renaming the game invalidates every save of it, because the title is the only thing being compared. Worth knowing before you change a title late.

A save also has to name a room the game still has, which catches one that was edited or truncated. Beyond those two checks it is trusted, so a save from a build whose flags have since been renamed loads with those flags at their declared defaults rather than complaining.