Sere: Crossing Taun Devlog
One File, No Engine
Design notes on SERE — a 58-node text crossing built as a single, self-validating HTML artifact.
SERE is a prose adventure — a traveler crossing a ruined, still-working city from one side to the other — and the entire game is one HTML file: markup, styles, engine, and every word of content, about 94KB total, no build step, no dependencies beyond two font families with system fallbacks. This wasn't minimalism for its own sake. The piece is meant to outlive its tooling. A single file zips cleanly for itch, embeds anywhere an iframe goes, opens from a desktop double-click in 2036, and — because view-source shows you everything — carries its own documentation. The constraint shaped almost every technical decision that followed, so this is a walk through those decisions.
Text is data with three shapes
The world is a flat map of nodes. Each node owns its prose, its choices, and optional one-time entry effects:
n_cistern: { region: "THE OUTER BELT", text: function(S){ /* assembled from state */ }, choices: function(S){ return [ ...base ].concat(travelChoices(S)); }, enter: { flag: "..." } // fires on first arrival only }
The important decision is that text accepts three shapes: a plain string, a {first, again} pair, or a function of state. The pair exists because a persistent world has to answer revisits — a room described identically twice reads like a loading screen, but a one-line "again" text ("The alcove keeps. The air stands. It is good at this.") reads like a place that went on existing without you. The function shape is for nodes that accrete: prose paragraphs appended when you've read the ground, stood inside the herd, annotated the ledger. State-conditional text turned out to be the single highest-leverage feature in the file — most of the game's "memory" is just string concatenation guarded by flags.
Choices carry a small effects vocabulary (flag, relic, learn, imp, rest, read), visibility conditions (if) that can be declarative or a predicate function, and once for self-consuming options. That's the whole grammar. Every system in the game — collection, the ledger, the charge economy, checkpoints, secrets — is an arrangement of those six effects.
Two registers, one law
The fiction has two voices: the crossing itself (second person, serif) and the documentary layer (a predecessor's notes, the ledger, the surface reference — all monospace). The content rule is that they never blend inside a block, and the engine enforces it structurally rather than editorially: documentary text can only enter the page as a card or a panel, rendered by dedicated code paths with their own typography. A writer extending the game literally cannot leak the mono register into the serif prose without fighting the renderer. Encoding an editorial rule as a UI law is the cheapest content-integrity system I know.
An interaction grammar instead of puzzles
The game has no fail states and no combat, so engagement had to come from somewhere else. It comes from four patterns.
Reading reveals. The fiction says reading surfaces costs nothing, so mechanically it had to pay. Read-choices set a flag; the node's text function appends what you noticed; a previously invisible choice — break the seam, go down through the ring — appears. Two of the game's secrets are gated this way, and both use the same established grammar (a shell crust is a door) so the reveal feels like fluency, not a key.
Failure teaches doctrine. In the saevel field, the correct move — give up your line and move with the changing ground — is hidden unless you either learned the relevant ledger entry or tried the stakes once and got returned to where you started. The gate is learned("saevel") || flags.tried_stakes. Players who skipped the lore derive it experimentally in one safe failure. That one predicate is my favorite line of code in the file.
A heterogeneous economy. The implement has three charges and three sinks that buy different categories of thing — safety (crossing open ground unread), perception (seeing a creature your eyes normally skip, which permanently extends its ledger entry), and depth (sounding a cistern's throat and being read back). Three charges, three sinks, no refills: the decision is real, and "never spending it" is also an expressible choice the ending acknowledges.
State as currency. A trader's price for the implement is "a story about the road behind you" — and the stories you can offer are literally your flags and inventory: the hidden door if you found it, the herd if you met it, told differently if you stood inside it, with "the truth: that you have hurried" as the always-available floor. The same principle powers the ending, where you leave a note for the next crosser assembled from what you actually did:
if (S.relics.indexOf("r02") >= 0) h += "<p>Under the first stair there is a door… Go down before you go on.</p>"; if (S.flags.was_held) h += "<p>The keeper of the upper city read me once, and let me go…</p>"; else h += "<p>I spent " + (3 - S.imp) + " of the implement's three charges…</p>";
Seven conditional lines. It's the emotional payload of the piece and it's an if-chain.
Persistence without storage
Progress is a save code — the full state snapshot, JSON, base64 — that the player copies out and pastes back. No cookies, no localStorage. Partly this began as an environment constraint, but it earned its keep as a position: the page telling you it does not remember you is true, portable across devices and browsers, and pleasingly diegetic in a game about leaving written records for whoever comes next. Rest-points additionally snapshot to an in-memory checkpoint, so the session has safety without the page having memory. An autosave layer could be added for the itch build in ten lines; I've held off on purpose.
The harness
A world made of hand-linked nodes rots silently, so the file ships with its own verification. validateWorld() walks every node and choice checking that targets, relics, notes, and ledger references exist; a headless test (jsdom) then plays the game — two full runs, one maximalist and one speedrun — clicking real buttons by label, asserting reveals appear, gates hold, the economy drains, the note assembles, and the save code round-trips through a reset. Every content pass ends with those runs. Prose regressions still need a human ear, but structural regressions haven't survived a commit.
The authoring surface
The top of the script is a guide comment documenting the node, choice, and effect schemas, because the file is the repo. Extending the game — a new region, a new secret — means adding entries to four data objects and nothing else. A #dev URL hash exposes a node-jump menu, a charge grant, and the validator. Fifty-eight nodes, eighteen carriable things, eleven notes, one file.
Files
Sere: Crossing Taun
Taun is not your concern. Getting through is. You will not fight.
| Status | Released |
| Author | Hyperdense |
| Genre | Adventure, Interactive Fiction |
| Tags | Casual, Exploration, Fantasy |

Leave a comment
Log in with itch.io to leave a comment.