Worked examples
Five reproducible stories spanning the design space: DAG and bidirectional hypertexts, with and without puzzles, plus the murder mystery. Every one buildable from the command line at a pinned seed, and playable here.
The axes
A hypertext can be acyclic — every link moves you strictly forward, you
never revisit a passage, the whole story is a partial order — or
bidirectional, where corridors run both ways, loops close, and the player
can and will come back to somewhere they have been. The two want different
primitives, different text, and different things from an exporter. An acyclic
story never needs text.repeat; a bidirectional one is unreadable without it.
Orthogonally, a map can be puzzle-free — the shape of the graph is the
whole of the content — or puzzle-inclusive, where some links are gated on
state the player has to go and acquire. Adding a lock to an acyclic graph is
harder than it sounds, and dag-locked exists to show one way to do it.
| puzzle-free | puzzle-inclusive | |
|---|---|---|
| DAG hypertext | dag-plain |
dag-locked |
| bidirectional hypertext | maze-plain |
maze-locked |
-
The One-Way Wood
dag no puzzles
dag-plain, canonical seed42. Other pinned seeds: 1729, 8675309. -
The Sealed Gate
dag puzzles
dag-locked, canonical seed42. Other pinned seeds: 1729, 8675309. -
The Warren
bidirectional no puzzles
maze-plain, canonical seed42. Other pinned seeds: 1729, 8675309. -
The Cindermoor Vault
bidirectional puzzles
maze-locked, canonical seed42. Other pinned seeds: 1729, 8675309. -
A Note in Another Hand
bidirectional puzzles
mystery-daily, canonical seed1024651441. Other pinned seeds: 42, 1729.
Building them yourself
Every example is a module under examples/ matching one small interface
(see the catalogue spec), and every artefact on this page
came from bin/story.js:
bin/story.js --list # the catalogue, with budgets
bin/story.js --example maze-locked --seed 42 --format ir --out story.json
bin/story.js --example maze-locked --seed 42 --format twine --out story.twee
bin/story.js --example maze-locked --seed 42 --format choicescript --out story-cs/
bin/story.js --example maze-locked --seed 42 --format inform7 --out story.ni
bin/story.js --example maze-locked --seed 42 --format play --out play/graph.js
bin/story.js --example maze-locked --seed 42 --format dot --out story.dot
make examples # all of the above, for all five
Reproducibility
For a fixed library version, --example E --seed S produces byte-identical
output on every machine, in every format. Four things hold that up: a single
seeded Mersenne Twister as the only entropy source; a theme derived from the
seed rather than sampled; no wall-clock timestamp anywhere in the output (the
Twine IFID is a hash of id + seed, not a fresh UUID); and a defined ordering
for IR passages and links.
That is what makes a "seed you can share" meaningful, and it is why the daily mystery can key off a date.
What each one demonstrates
dag-plain — the shape of a branching story
Fork and join, nothing else. Two arms diverge from a passage and rejoin at the next, so a choice is real without fragmenting the story into two disjoint futures that both have to be written. There is no state at all: no variables, no conditions, no locks. If an exporter can round-trip this one, its passage and link machinery is correct, and everything after this is about state.
dag-locked — a lock the player cannot walk back to
The interesting acyclic problem. In a bidirectional map a key-lock is easy: you hang the key off a side branch, and if you reach the door without it you walk back. With no way back, that construction is unavailable.
dagKeyLock solves it with a fork whose two arms rejoin before the lock:
┌── key room ──┐
a ───┤ ├─── join ──[locked]──▶ b
└── skip room ─┘ │
└──▶ (a losing ending)
The choice at a is a genuine commitment made before its consequence is
visible, and the failure is legible in hindsight. That is a real trade: you give
up recoverability, and what you buy is a puzzle solvable by reasoning rather
than by exhaustive walking — which is exactly what a daily puzzle wants.
Key and lock as graph transformation
catalogues the alternatives.
maze-plain — why text.repeat exists
Two-way midpoint rooms, dead ends with explicit backtracks, parallel routes, and cycle-closing returns. No locks anywhere. The point is that a map with no puzzles in it at all is still worth walking, provided the writing knows you have been here before. Every room and every passage in this example carries its own narrative slot; a generated map where they do not is the specific failure this example exists to catch.
maze-locked — the full apparatus
Key/door side-branches with shared pairIds, nested locks, gated returns that
turn the tree into a Metroidvania graph, health potions, Markov monster battles,
and multiple-choice puzzle gates. This is the file to copy when starting
something new; its header comment walks through all six stages.
mystery-daily — the destination
Suspects, secrets, and social locks. The keys are facts rather than brass, and the correspondence is exact: the butler's silence is the door, knowing about his affair is the key, confronting him with it is the act of unlocking, and his conceding that the pantry is worth a look is the door swinging open. What lies beyond need not be a room at all — it can be another secret, which is how a set of suspects becomes a conspiracy with a shape.
The seed derives from the date, so everyone gets the same puzzle on the same day. The design paper is the long version.
Playing them
The play page runs any of these in the browser: a text pane with the narrative and its link affordances, a live map of the neighbourhood you are standing in, and a status bar with HP, inventory and a trace dump. Pick a story from the menu, or link straight to one:
play/index.html?story=maze-locked.42
The text you will see is placeholder prose — [gothic_horror:describe_room#room_4]
— unless the story was built with --sonnet. That is the debug mode working as
designed: it shows exactly which narrative slot each piece of text will occupy,
which is far more useful when developing a grammar than plausible-sounding
filler would be. See the narrative slot taxonomy
for what the slot names mean.