Enter Three Witches: a writer-focussed game engine
To get the best writing, writers must take the helm
In 2017 I missed giving the talk Natural Script Writing with Guile due to sickness and Christine stepped up for me and turned it into a wonderful presentation. The talk gave an overview of existing game scripting syntax and presented the first version of Enter Three Witches that worked well – a followup on one example from my 2016 talk wisp: simplest whitespace Scheme. This article is a second followup after 10 years with Enter Three Witches, showing how to use it for your own games, both on the commandline and on the web.
This article is both the project site and the documentation of
enter-three-witches.
WIP: This is still a work in progress. Sections marked as TODO are unfinished. You can already try it, but until this article is finished, you’ll have to figure out the missing pieces by looking at the code of dryads-wake. All the to-be-described features except for the multi-file-format are already in practical use there.
The cleaned up enter-three-witches repository is not yet finished,
so the examples here do not run yet.
Shared early as documentation driven development.
To skip explanations and get to work right away, jump to How to start? Writing your first scene.
Enter : First Witch Second Witch Third Witch ;; this line is a comment First Witch When shall we three meet again In ,(color 'cyan) thunder, ,(color #f) . ,(color 'yellow) lightning, ,(color #f) . or in ,(color 'blue) rain? ,(color #f) Second Witch :resolute When the hurlyburly's done, (we ,(+ 1 2)) When the ,(color 'red) battle's ,(color #f) ;; leading period continues the line: . lost and won. Third Witch That will be ere the set of ;; the .. becomes a plain period: . ,(color 'yellow) sun ,(color #f) ..
Goals of Enter Three Witches:
- Make game scripting read like natural writing
- Enable writers to tinker from the start and till the end
- Be accessible by default
- Make it easy to publish a game on cheap infrastructure
- Solidify the platform of my own games
Non-Goals of Enter Three Witches (for now):
- Build a graphical framework to rule the world
- Build a market place or publishing platform for games
Why this? How the writing of Starcraft 1 got so good
writers could tinker till the end
Starcraft 1 followed an ingenious strategy: it used long-lived and hard to change videos only for flavor. All plot was conveyed in ingame text that could be changed by changing simple text files and in briefings that had a voice-line but no added dependencies.
The plot of Starcraft 1 was intense, brutal, and clever and its characters were powerful and a perfect match to the harsh world they inhabited. The writing was so strong that hearing a sentence like “I'm pretty much the Queen Bitch of the Universe” did not feel cheesy or cheap but showed how the power dynamics had shifted.
Starcraft 2 abandoned that. Its video cut-scenes told the central parts of the story and when I first watched a cut of those videos to decide whether to buy it, I was severely disappointed. I almost skipped it due to that. When I finally bought it, I found that the ingame story made up for the weak videos. Again that story told with ingame dialogues could be changed far more easily than the videos.
For a recent example: Baldur’s Gate 3 has some awesome writing and it managed to capture me emotionally on a level few games reached before. My level of emotional investment was similar to that when I played Suikoden – and when I played Suikoden I was around 15 and easier to impress (I think).
Baldur’s Gate 3 has fully voiced dialogues, but the speakers recorded continuously and text in all parts of the game got changed until and even after release.
What these have in common: writers could tinker till the end.
If you want to enable the best writing, then the writing must be what you can change at all times and writers must be the ones who can experiment the most.
Enter Three Witches tries to enable just that: let writers take the helm. Writing directs the plot and holds everything together. It does not build on locations that ask for some text to show. Instead it builds on dialogue that can be enriched where and when needed.
Does it work out? Read on, then give it a try.
Why like this? Minimize ceremony by starting from theater scripts
Code encodes intent. Since it is written not only for humans but also for machines, it always sprouts some ceremony: structures and patterns that are only needed by the machine, but rather obfuscate than clarify the intent.
To minimize that ceremony, enter three witches started with an investigation how dialogue is encoded for humans. Specifically: how people digitized Shakespeare’s plays in plain text.
There are two main approaches:
- Speaker-prefix: Speaker (sometimes capitalized) starts the line with continuation indented (1993 version from Project Gutenberg) and
- Speaker-heading: Speaker (sometimes capitalized) starts the paragraph that ends on an empty line. (2025 version from Project Gutenberg)
Both always introduce people with Enter.
Example for Speaker-Prefix:
Enter BERTRAM, the COUNTESS OF ROUSILLON, HELENA, …, all in black
COUNTESS. In delivering my son from me, I bury a second husband.
BERTRAM. And I in going, madam, weep o'er my father's death anew;
but I must attend his Majesty's command, to whom I am now in
ward, evermore in subjection.
Example for Speaker-heading:
Thunder and Lightning. Enter three Witches. FIRST WITCH. When shall we three meet again? In thunder, lightning, or in rain? SECOND WITCH. When the hurlyburly’s done, When the battle’s lost and won. THIRD WITCH. That will be ere the set of sun.
The modern format uses centered speaker names instead, but centering code on a fixed size page feels so alien to programming that I discarded that. But for completeness let’s add that.
Example of the traditional style from the Dramatists Guild (From Tennessee Williams’ Not About Nightingales):
BOSS
(removes cover from basket)
Speak of biscuits and what turns up but a nice batch of
homemade cookies! Have one young lady – Jim boy!
(Jim takes two.)
You can see reminiscences of the first two examples in the final format of Enter Three Witches:
Enter : First Witch ;; introduce with Enter Second Witch ;; continue with indentation Third Witch First Witch ;; speaker starts the paragraph When shall we three meet again In thunder, lightning, or in rain? Second Witch When the hurlyburly's done, When the battle's lost and won. Third Witch That will be ere the set of sun.
A personal note: my kids told me that this does not read like code. They didn’t realize that their words were the highest praise. Because that’s part of the point: make the code read like natural language (without limiting its power), so you can use your existing feeling for text. If it looks good in the code, it should look good in the game.
Since we’re also writing for computers, reading like text written for humans is an important part, but not a sufficient system to write games.
Who can tinker? Keeping writers at the helm
The clear syntax makes it easy to create the text to be shown, but a game is more than linear text. You need to ask questions and show a different story based on the results. Or track effects of decisions and make them affect the game. Or start a minigame.
To enable controlling those aspects, a system for game scripts can provide specialized commands, but then writers who want to go beyond the expected have to request features to be added or must change very different parts of the game, so most writers would be blocked and would have to wait for others before they could go beyond these limits.
Instead of this, Enter Three Witches embeds its syntax into Scheme, one of the most flexible programming languages.1 This puts all capabilities of a full programming language into your hands without risk of being overwhelming, because you only need to touch those capabilites where you really need them.
This way there’s no need to wait: the writer controls what happens and when it happens, and everything that can be done via code can be done by writers. And is used just like the helpers already provided by Enter Three Witches. That gives you independence from the framework.
Everything is driven by the writing and writers can tinker from the start up to the very end.
To enable you to tinker with confidence, plot analysis tools give safety against breaking the plot. They build on the easy code introspection of Scheme to show how changes affect the overall picture of the plot.
TODO How to start? Writing your first scene
This section helps you setup enter-three-witches (on GNU Linux) and explains how to write a branching story similar to a game book.
TODO Installing and creating a setup for development
Requirements:
- GNU Linux (Guix is easiest, but others work, too)
- Mercurial: https://mercurial-scm.org
- Guile 3.0.10+: https://gnu.org/s/guile
Install the template:
TODO: finish extracting the code and upload the template.
hg clone https://hg.sr.ht/~arnebab/enter && \ cd enter && \ ./game.w
Showing a theater script incrementally
To show a linear story, just create a file with .w as suffix, e.g.
script.w. Fill it with text like the following:
Enter : The Narrator The Narrator Welcome to the dark forest. This is where dreams ,(slower) may come to pass. ,(faster) ;; Print shows a line of description Print The sound of rustling leaves fades. ;; Say is spoken text without speaker Say The path leads into shadow.
Execute the script with
./game.w --execute script.w
and watch as the text is shown letter by letter:
The Narrator
Welcome to the dark forest.
This is where dreams
may come to pass.
The sound of rustling leaves fades.
The path leads into shadow.
Syntax: what to write, how it looks
This section describes the regular syntax. The multi-file format is a simpler but less powerful alternative to convert existing stories.
Enter introduces speakers. It must be at the start and all
speakers must be introduced together at the start.
A speaker name introduces a block of lines to speak letter by letter.
Comma and an opening parenthesis like ,(faster) call a command that
ends with the closing parenthesis. For this to work, you must have a
space before the comma (,).
;; starts a comment until the end of the line.
Print shows lines without a speaker.
Say can continue spoken lines after Print without showing the speaker again.
Some useful commands:
,(color 'red)– switch to color. Available colors:'black'blue'yellow'red'cyan'magenta'green'white'purple'brown,(color #f)– reset color.,(slower),(faster)=,(set-speed-extremely-fast!)
,(set-speed-very-fast!),(set-speed-fast!)
,(set-speed-normal!),(set-speed-slow!)
,(set-speed-very-slow!),(play-sound "path-to/file.opus" "description")
Background: how it works
Enter is a macro that creates macros: the speakers. The colon (:)
after Enter is equivalent to putting the name in the next line with
indentation.
The lines to speak are treated as data, split into letters and printed
letter by letter, except if you use , to interpret something as
code.
Code (usually within ,(…)) is executed when it is processed, so
,(play-sound …) plays the sound when it would be shown if it were a
word.
If code returns text (a string), that text is shown letter by
letter. If it returns #f, nothing is shown and it skips right to the
next word.
Enter Three Witches builds on Wisp, a Scheme frontend that skips most parentheses. To understand it as programming language, see the book Naming and Logic: programming essentials with Wisp.
Asking questions and branching stories
To ask a question, use Choose:
Choose : question 1 answer 1 second line : question 2 answer 2
To write branching stories, define plot fragments as indented text.
Each fragment is self-contained, so speakers have to Enter at the
start of it.
define : deeper Enter : The Narrator The Narrator You step deeper into the forest. After several steps, darkness envelops you and the ground under your feet becomes softer. thank-you ;; continue in thank-you define : wait-or-deepen Enter : The Narrator The Narrator Where do you want to go? Choose : deeper into the forest As you continue, the shadows darken. ,(deeper) ;; continue in deeper : wait and watch ,(wait-and-watch) ;; continue in wait-and-watch define : wait-and-watch Enter : The Narrator The Narrator As you’re watching the forest, the sun sets, the light fades. and you smell water. thank-you ;; continue in thank-you define : thank-you Enter : The Developer The Developer Thank you! wait-or-deepen
Write this into a file like branch.w and call it with
./game.w --execute branch.w
to choose your path through the story.
Fragment names can contain all letters except for parentheses, comma, quote, double quote and hash. It is common to use lowercase words connected by dashes.
You can use any level of indentation inside fragments, but you have to
stay consistent. define for fragments must be at the beginning of
the line (no indentation).
Fragments can contain single empty lines, but no double empty lines. A double empty line always ends the fragment. A line with a comment is not empty.
You can call fragments defined earlier or later in the file by either
using them in dialogue via ,(fragment-name) or by writing the
fragment instead of a speaker.
At the end of the file, call the fragment that starts the plot without indendation.
Structuring the plot in set-pieces
To keep your plot manageable, you can structure the fragments like set pieces: branch out narrative fragments as branches and tie these branches back together into a small number of transitions between larger pieces of the plot.
The code above shows a single set piece starting at wait-or-deepen
and ending at thank-you, because both deepen and wait-and-watch
lead to thank-you:
component "set-piece" {
[wait-or-deepen] --> [wait-and-watch]
[wait-or-deepen] --> [deepen]
[wait-and-watch] --> [thank-you]
[deepen] --> [thank-you]
}
TODO How to convert existing stories? Use the multi-file format for a simpler start
(show format, show conversion)
TODO How to continue? Saving and loading savegames
Adding state
To enable people to play your story in smaller parts and take breaks in between, or to make it easy to release a story in episodes, you need savegames.
Saving a game and loading it later needs a name to identify the save. And tracking that name depends on passing a state to fragments:
define : into-the-void state Choose : Move alone into the silent night ,(into-the-night state) : Cower in fear Your adventure ends here define : into-the-night state Enter : Nothing Nothing Fear dissipates reality dissolves darkness looms in welcoming warmth ;; create the state define state : game-state-init! ;; start the game with the created state into-the-void state
The state must always be passed along. If a fragment ends without
calling another fragment, it can return the state with . state or by
ending with a call to a fragment that returns the state. The calling
fragment can update its state with set! state new_state.
Choose can return a state, too. If you set the state to the one
returned from Choose, each answer must return a state.
define : investigate state Enter : Narrator set! state ;; set state to the state returned from Choose Choose : Trace over the cracks in the table ,(trace-the-cracks state) ;; returns from fragment : Open the drawer It’s empty, except for a name scribbled into old dust. Craigh. Who may that be? . state Narrator Suddenly You hear footsteps. Get out! . state ;; state returned define : trace-the-cracks state Enter : Narrator Narrator They run deep in the polished stone in the shape of claw-marks from a feral beast, but which besat can cut stone? . state ;; state returned ;; newly created state is used directly investigate : game-state-init!
TODO Creating savegames with name and secret
(ask for the name, print the name and the secret, load from name and secret)
TODO Defining scenes as savepoints
define-scene
TODO How to stay in control? Use outcomes and analyze your game to keep ahead of complexity
You can now tell a path through a story with decisions, but all consequences are immediate. But a story is more interesting if there are long-term consequences.
Enter three witches provides three ways to keep state: outcomes,
character attributes, and wounds. All three are passed via the name
state between fragments.
Outcomes are named facts like insulted-the-miller or
restarted-the-generator. You can set them and check later whether
they were set.
Character attributes are values attached to names of people. They come with a ruleset which allows checking whether some action succeeds, and they can improve with usage or by increasing it manually.
(in a game, many people want to see the effect of their choices ⇒ outcomes, skills, and wounds) (show analyze plot and outcomes)
Adding outcomes
You can add outcomes to the state, remove them, and check for their presence.
To protect against typos, you should define them before you first use them, then you get warnings when you try to run the game with a non-defined outcome.
;; TODO: define-outcome, add it, test for it, remove it. ;; TODO: outcomes as set: remove unexisting has no effect, add twice has no added effect.
Outcomes make it easy to react to player decisions. Seeing that their decisions have an effect is one of the major motivations for playing games. But they make your story more complex, so most outcomes should be used soon. Therefore enter three witches provides analysis tooling to check visually which outcomes you added but did not use in a later scene.
;; TODO: analyze plot
TODO How to reach people? Deploying your game
TODO For the commandline
Publishing a repository
Publishing an appImage
TODO As Webservice
TODO With nginx for SSL, load-balancing, and failover
(smallest ionos server for 300 simultaneous users 2€/month, 600 for 3€)
TODO Where can I go? Looking beyond gamebooks
TODO Adding challenges with game rules, luck, and long-term consequences
(long-term consequences, …)
TODO Executing arbitrary code
(…)
In the commandline-version
In the webbrowser
TODO How to …? Common solutions (FAQ)
TODO Update enter-three-witches
(simply do hg pull && hg merge)
Who uses it? Games built with Enter Three Witches
TODO Where next? Future plans for Enter Three Witches
(accessible graphics and sound for the Web-Version, setup for somewhat safe access via telnet, Deployment to Linux distros, Graphical Chickadee-Game, integration with ifarchive https://babel.ifarchive.org/babel.html, integration in arcade or strategy games, dialogue snippets for ingame banter in world exploration RPG levels, …)
TODO Who are you? Can I prove my claims?
(DrArneBab, PhD, reference the talk Natural Script Writing with Guile again, also reference the 2016 talk, and a previous try — TextRPG — but say clearly that I don’t have industry experience with large-scale games, so the future will show how well this works out on the long run. But at the current state I’m confident enough in the design that I dare to recommend it to others, which is why I now wrote this tutorial. That’s also why along with this tutorial I’m releasing version 1.0 of Enter Three Witches. I consider it as stable now, so future changes should preserve backwards compatibility.)
TODO Summary
Parting Notes: Background and Motivation
Some notes that do not fit into the flow of the general article.
You can find some background at https://archive.fosdem.org/2017/schedule/event/naturalscriptwritingguile/
That’s where it started. Though I ended up not giving the talk myself because I was ill (so cwebber took over).
Though IIRC that misses one essential motivation I have to work on it: I realized that the difference between plotting in Starcraft 1 and Starcraft 2 is that in SC1 they could tinker with the story to the very end and in SC2 their high-quality plot-videos restricted the writers from doing last minute changes.
And I think the part of the game that will be most polished in the end is the one that can be edited at any time. That’s why enter-three-witches strictly drives the game from the story and dialogues.
If you decide to port it, also have a look at the outcomes system: https://hg.sr.ht/~arnebab/dryads-wake/browse/game-helpers.w?rev=tip#L171
That allows me to analyze branching plots with a simple code walker: https://hg.sr.ht/~arnebab/dryads-wake/browse/analyze-plot.w?rev=tip#L43
It generates plot diagrams.
That shows a scene-graph and lists the outcomes I did not yet check against, so I can be sure that player choice matters, because I include a consequence for every decision players take.
Translations are by the way the biggest hurdle with the enter-three-witches syntax. There’s no good way to translate that with gettext or such. The best bet would be to treat the text as actual prose and translate it in one go.
But I actually think that that’s what should be done for game plot text, because it is (and should be) complex prose and not some menu elements that can be translated in isolation.
The syntax for Enter is about the deepest dive I took into macros. The Enter macro generates new macros for the introduced people and those macros quasiquote their arguments and then process them word by word: each word is split into letters and printed with some delay and commands like ,(something) are executed once the word gets processed. So it should work to also use them to play sound effects or trigger graphic effects exactly when some word is spoken.
, is handled by either putting it at the end of words (where it works) or by escaping it as ., – same for period (.. ⇒ .). Those are sometimes needed when you want to unset color with ,(color #f) and then add a final period.
Footnotes:
Put in academic wording, it’s an EDSL: an embedded domain specific language.