Tobias Reithmeier

Blog

Why I Built My Own Language for Gamebooks

A gamebook is almost entirely text. So it ought to be written like text: in a file any editor can open, that version control can track and a diff can compare. That is why I built Story Weaver, a small language for adventure gamebooks.

What it is for

The books in question are the ones from the eighties. You read a numbered section, make a decision at the end of it and turn to the number it names. Series like Fighting Fantasy added dice: Skill, Stamina and Luck are rolled at the start, and a fight is settled round by round against those numbers. I played books like these as a teenager.

Story Weaver covers exactly that. A book is a Markdown file, or a folder of them. A heading is a section, a list item is a choice, and a link is the "turn to 137". The story logic follows ink, the rules follow the gamebooks of the time. How any of it looks on screen is not part of the book; that is up to whatever plays it.

The project started in August 2026. Story Weaver is open source on GitHub, under the Mozilla Public License 2.0.

What already exists

ink is the scripting language the studio inkle wrote for its own games, Sorcery! and 80 Days among them. It is MIT-licensed and plugs into game engines such as Unity as middleware. The text leads, and code and logic are written into it. Story Weaver borrows most of ink's ideas and swaps its punctuation for Markdown's, with exactly one spelling per concept: where ink offers several ways to do the same thing, one survives. You do not need to know ink to write a book.

Twine is an open-source tool for interactive, nonlinear stories. What a story can do, and how its logic is written, depends on the story format you pick: Chapbook, Harlowe, Snowman or SugarCube.

Both are general-purpose, meant for any kind of branching narrative. ink has no built-in layer of stats, items, checks and combat; if you want one, you write it yourself. Story Weaver is narrower. It covers fewer kinds of game, but for the one kind it does cover, the adventure gamebook, the rules layer is part of the language. A fight needs no custom code.

What a book looks like

The smallest example is The Crypt Under the Thorn. Its default language is German, so this is how it opens, straight from the source file:

# Am Waldrand {#begin}

Der Pfad teilt sich vor einer Weißdornhecke, rechts von dir hörst du das Gluckern eines kleinen Bachs. {&Ein Krachen|Ein Knirschen|Stille} im Unterholz.

Du hast {gold} Goldstücke bei dir und ein Glück von {luck}.

+ [Das Dickicht am Ufer untersuchen](#thicket)
+ [Zum Bach gehen](#brook)
+ {has("lantern")} [In den Spalt unter der Hecke klettern](#crypt.crypt) Du zwängst dich hinein, die Laterne voran.

The heading starts a node and gives it the id begin. Each list item is a choice, and its link is where the choice leads. A plus sign marks a choice that stays available; an asterisk marks one that disappears once taken. Whatever follows the link is printed only after the choice is made. Curly braces do several jobs: {gold} prints a value, {&...|...} cycles through its variants on each visit, and {has("lantern")} is a condition. Only readers carrying a lantern are offered the way into the gap. On GitHub, the file renders as ordinary Markdown, a heading and a list.

The rules layer

Stats, items and enemies are declared once, in YAML; for the crypt, that is a file called book.yaml. The prose only refers to them. An excerpt:

stats:
  skill:   { name: { de: Geschick, en: Skill },    start: "roll(1,6) + 6",  max: start }
  stamina: { name: { de: Ausdauer, en: Stamina },  start: "roll(2,6) + 12", max: start }

enemies:
  goblin: { name: { de: Goblin, en: Goblin }, skill: 6, stamina: 6, flee_after: 3 }

death:
  when: "stamina <= 0"
  goto: crypt.death

From this the runtime builds the character sheet, rolls the starting values and checks after every change and every combat round whether the character is still alive. For a fight, the text only names the enemy and where a win, a loss and an escape lead:

!combat goblin
  win  -> chamber
  lose -> death
  flee [Zurück durch den Spalt fliehen](#start.begin) Du lässt mehr als deinen Stolz zurück.

The defaults are the Fighting Fantasy rules: a check rolls two dice and succeeds if the total does not exceed the stat. Nearly all of that is an expression a book can override, though. A d20 against a threshold as in modern role-playing games, percentile rolls as in the role-playing games of the early eighties, or a book that gets by almost without dice and runs on state and a clock: each of these variants has been compiled and played.

Two languages, one structure

Most of the example books come in German and English, and one rule holds throughout: translations carry text, not logic. The default language owns the structure, meaning nodes, choices, conditions and targets. Every other language is a catalog of paragraphs and choices, in the same order as the original. So the English version of the forest edge contains not a single link:

# At the Forest Edge {#begin}

The path forks before a thorn hedge; to your right you hear the gurgle of a small brook. {&A crack|A crunch|Silence} in the undergrowth.

You carry {gold} gold pieces and a luck of {luck}.

* Search the thicket by the bank
* Go to the brook
* Climb into the gap under the hedge

You squeeze in, lantern first.

The asterisks here are just list items. Whether a choice stays or disappears is decided by the German version. That way a translation cannot quietly rearrange the story. Where a language genuinely needs its own logic, say for a plural, it may replace a whole node, and the linter points at every place where that happens.

Checking instead of hoping

You cannot proofread a branching book the way you proofread a novel. Two commands help. lint reports unreachable nodes, broken links and dead ends. simulate plays hundreds of games with random choices and reports how they end. That is how the fear stat in The House Behind the Moor turned out to punish every second visit to the same room.

The tool has no dependencies and no build step; Node 20 is enough. A book exports to a single HTML file, plus its images if it has any, and runs without a network connection. There is also a VS Code extension that plays the book from the node the cursor is in, and a Swift package that lets an iOS app embed a book.

The books on this site

Four books can be played here in the browser:

  • The Crypt Under the Thorn: the starter, two chapters, one goblin, two endings, done in a few minutes.
  • The House Behind the Moor: 48 sections, four endings and a fear stat that keeps count.
  • Brüder dunklen Rufes: 69 sections, three endings, and the only one without an English translation. The adventure comes from episode 47 of the German role-playing podcast Over the Hills. I retold it; no sentence from the episode appears in the book.
  • The Intercept: an imported book, more on that below.

Next to each game sits the book's Markdown source, read-only and always scrolled to where the story currently is, so you can see which lines produced the paragraph on screen. This site plays a fixed build of the project, not necessarily the latest one. For an overview of the language itself, see the language behind the books.

The Intercept: someone else's book as a test

Writing the language and the stories at the same time makes it easy to bend a story until it fits. That is why one of the examples was not written for Story Weaver: The Intercept, a small demo game inkle made for ink. It is MIT-licensed, copyright 2016 inkle Ltd.

It was not retyped. It came in through story-weaver import, which reads an ink file and writes the same story in Story Weaver. Whatever has a direct equivalent is carried over. Anything without one, such as ink's threads, lists and external functions, is reported with its ink line number and left out.

The result has 133 sections and 14 endings. Every section is reachable, and there are no dead ends. It is the only example without a rules layer: no dice, no fights, no items, only state that remembers how you answered during the interrogation. That makes it a test of the narrative half of the language on writing nobody produced for it. The English original stays untouched word for word, and the German version sits beside it as a translation. The licenses stay separate too: the book carries inkle's MIT notice, the runtime Story Weaver's MPL-2.0.

What is deliberately missing

A Story Weaver book contains no presentation. No fonts, no colors, no label for the attack button, no hint about how a stat should be drawn. That belongs to whatever plays the book, whether a web page, an app or a terminal. A book that carries its own layout stops being easy to play anywhere else.

The language is not finished, either: the specification is a draft, version 0.8. If you want to write a book, start with the crypt and keep the spec open alongside it.


Disclosure: Story Weaver is my own project. This is a look behind the scenes, not a neutral review.

Sources