UP | HOME | CONTENT

Enter Three Witches: a writer-focussed game engine

To get the best writing, writers must take the helm

(dark mode)🌓︎

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.


PDF (drucken)

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:

Non-Goals of Enter Three Witches (for now):

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:

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:

1

Put in academic wording, it’s an EDSL: an embedded domain specific language.

ArneBab 2026-02-22 So 00:00 - Impressum - GPLv3 or later (code), cc by-sa (rest)