Look Forward

WeBook: Collaborative Choose Your Own Adventure books

I grew up in a time and place where being identified as nerd put me at risk of abuse. Constant teacher surveillance in class meant I was likely to only be teased or, at worst, have a chair pulled out from under me during lessons. Once outside, though, the gloves came off. I needed to monitor my social interactions very carefully to avoid being piled onto.

Confronted with the cruelty of suburban youth, I retreated into fantasy. My eldest uncle bought me the Lord of the Rings when I was 10; I spent a year reading the first book, and finished it only to realize I’d understood none of it. I cut my teeth instead on other fine works of literature like the Sword of Shannara and Dragonlance.

Whenever I was borrowing my latest collection of 600-page tomes from the local library, I’d often throw a Choose Your Own Adventure book or two in for fun. I wasn’t crazy about them or anything, but I found them to be a pleasant distraction.

The library also stocked some fantasy gamebooks in the same section as the CYOAs. I have fond memories of Grailquest and Wizards, Warriors and You (which I was tempted to read because of the NES game with a similar name), but I generally didn’t find these very entertaining. I didn’t like how much was left to chance, and I felt like I could just be playing a videogame instead.

Growing up like this, I quickly learned who the other nerds are. I might have kept my distance from them to avoid further reinforcing my own status as an outsider, but I certainly shared hobbies and book recommendations with them when no one else was looking. And on the weekends, it was totally cool to hang out at their houses. After all, no one knew I was there!

During one of these weekend hangs, in between sessions of trying to brute-force ourselves out of some inscrutable puzzle in King’s Quest IV, one of my uneasy allies offered me this tome:

Duelmaster: Challenge of the Magi book cover

I took the photo you’re looking at myself, just now. I still have my copy of this book almost 30 years later.

At first glance, there’s nothing too compelling about it. There’s a winged cloven-hoofed monster, some skeletons in a heap, a cave that opens into a blood-red sky. It’s standard pulpy fantasy stuff.

However, if you look a bit closer, you’ll see a curious thing etched in the top-right corner.

A Two Player Fantasy Gamebook

You see, the book that my friend handed me was just one of a pair. He held onto this one:

Duelmaster: Challenge of the Magi companion book cover

Alas, I had to crib this photo off the internet, as I’ve never acquired the companion book.

In the weeks that followed, I endured much castigation from my parents for tying up our single phone line playing game after game of Duelmaster. Each session involves travelling from realm to realm to gather resources before you confront your opponent in a magical duel. When you do something that alters the state of the game world, you record a key-word that the book will prompt your opponent to query for if they end up in the same location later on.

This fad only lasted for a month or two, but the memory stuck with me for far longer than that. Even as a child, I was enamoured of the way that the books tracked the state of the world with a few simple mechanics. I also loved how it had multiple distinct realms that you could travel between. As I grew up, I often toyed with the idea of porting the books to a computer game of some kind.

At the start of 2021, I’d been studying for a few months and felt the itch to actually make something. I stumbled across my copy of Duelmaster, and thought this might be a good time to give it a shot. As I thought about it more, I became excited by the idea of making a series of collaborative reading experiences; Duelmaster was cool when I was a kid, but I’m less enthused now by the idea of making a game where the point is for two friends to fight each other to the death.

After sketching out a few ideas for stories that involved teamwork rather than competition, I thought it might worth putting time into building a framework that would let me write and share collaborative stories. Since I don’t like writing game engines and games at the same time, I thought I’d use my dog-eared Duelmaster copy as a prototype, and build an engine that could “run” that book as the first milestone.

I’m not quite there yet, but I’m sufficiently far along that I thought I’d document the state of the project as it is now.

I present to you: WeBook, the library for collaborative Choose Your Own Adventure books.

How It Works

Let me just show you:

How I Built It

My primary functional requirement for this project was that I wanted the author workflow to feel more like writing a book than building a computer program. I haven’t written enough interactive fiction (IF) to know if this is a good idea or not, but it’s how I wanted to start.

I researched some really incredible IF languages like Ink and Dialog, but these tools provide options that are way beyond my current gamebook-writing skill. They also herd me into technical decisions that I don’t want. Ink, for example, requires Unity unless you want to write your own integration for it.

I figured with some Markdown, template programming, and few regular expressions, I could get pretty far on my own. (I just now imagined hearing the cries of anguish from HackerNews as I typed that last sentence.)

After a bit of sketching, I came up with a good-enough format for me.

Writing A WeBook

Here’s a glance at the first lines of the WeBook port of Duelmaster: Challenge of the Magi.

# Challenge of the Magi
## Mark Smith & Jamie Thomson
## v0.1
## challenge-of-the-magi
<a id="blurb"><h2>Blurb</h2></a>
Fight a real-life opponent in a deadly struggle for mastery over the magical Rainbow Land.

Confront your challenger face-to-face in a thrilling contest for the title of Duelmaster.

<a id="start"><h2>Intro - The Challenge</h2></a>
# The Challenge

You are a powerful mage, one of the Inner Council of Magicians, all of whom
have mastered the greater magics. Each of you specializes in one of the five
colours of magics. 

The head of your arcane Council, Emeritus the Sorceror, is dead and his
successor among the councillors must be chosen in the time-honoured fashion—by
ritual duel in the Rainbow Land. 

As one of the Inner Council, you have put yourself forward as a claimant. Only
one other has dared to oppose you, but you know not which of your illustrious
colleagues feels mighty enough to challenge you.

- [Learn about Rainbow Land](#intro-rainbow-land)
- [Skip to character creation](#intro-charcreate)

<a id="intro-rainbow-land"><h2>Intro - Rainbow Land</h2></a>
# Rainbow Land

The Rainbow Land is a mystical realm of magic ... 

It opens with some metadata about the book itself (title, authors, book version, and a slug that’s used to identify the book.) Scenes are defined with an HTML anchor link, where the id uniquely identifies the scene. The markup in the anchor link is discarded by the compiler, but I use it to title the scene in English so I remember what it’s about.

The blurb scene is special, and is used to provide a dust cover blurb for the book. The start scene is also special, as readers will always begin reading there upon starting a session with another reader.

The rest of a scene is written in Markdown. Choices are represented by Markdown links; these can be placed anywhere, but they’re parsed separately and will always appear at the bottom of a scene when you’re actually reading the book. The anchors they refer to are other scene IDs.

Let’s look at a slightly more interesting scene, where the reader visits an oasis in a vast desert.

<a id="sands-oasis"><h2>The Scorched Sands of Akhneton</h2></a>
# The Scorched Sands of Akhneton

Beyond the dunes is a desert oasis. The palm trees are charred and blackened;
most of the hardy shrubs that surround it are completely destroyed. It seems
some terrible fire has ravaged the area. However, the water seems safe enough
to drink. 

(You refill your bottle with fresh water.)

{{setFlag "has-water" true}}

There seems to be nothing else of interest here and rather than brave any more
of the desert heat, you decide to Invoke the Portals.

{{wait}}

- [Invoke the Portals](#portals-sands)

This scene has two script blocks in it. The first calls setFlag to set a Boolean specific to this reader; in this case, we’re recording that the reader’s water bottle is now full. The second calls wait to indicate that the reader cannot make a choice until their partner also arrives at a wait block.

Here’s another, more complicated scene in the desert:

<a id="sands-dunes"><h2>The Scorched Sands of Akhneton</h2></a>
# The Scorched Sands of Akhneton

You set out for the dunes. However, the shimmering heat causes a confusing
mirage—the dunes are much farther away than you first believed. The heat is
terrible. Soon you are dry, dusty, and desperate for water. The terrain is
inhospitable. Nothing can live in this heat.

{{if getFlag "has-water"}}
    {{setFlag "has-water" false}}
    You drain your water bottle of its contents. 
{{else}}
    {{loseLP 5}}
    {{loseCP 2}}
    You become terribly dehydrated. (You lose 5 LP and 2 CP.)
{{end}}

{{if lost}}
    The heat robs you of the last of your strength. The Council sends someone
    to rescue you, but you are forced to concede the contest. You have lost.

- [Concede the contest.](#end-lose)
{{else}}
    {{sandsKillSteed}}

    What will you do?
    {{if getWorldFlag "oil-sands-burned"}}
    - [Press on towards the dunes.](#sands-oasis)
    {{else}}
    - [Press on towards the dunes.](#sands-oil)
    {{end}}
- [Invoke the Portals and leave this blasted place.](#portals-sands)
{{end}}

Whoa! This one has a lot of stuff going on in it. Right off the bat, the reader is going to need to some water to tolerate the intense temperatures of this scene. If they have some, they drink it; otherwise, they lose Life Points (LP) and Concentration Points (CP).

The lost predicate tells us whether the potential damage from the heat overcame the reader. I grew up playing many of the classic Sierra adventure games (I’ve already mentioned one in this post!) Most of these games treated losing like a minigame; it was a treat to discover all the ways you could fail, since you’d get a custom description and a unique animation for each loss. I kept some of that flavour in this Duelmaster port by requiring the author to describe losses manually wherever they might occur, instead of shuttling the reader to some generic “you lose” section right away.

Even if the reader survives, their steed might not! The desert is a brutal place for animals, and the sandsKillSteed function will randomly decide whether a steed dies from the heat.

Finally, getWorldFlag checks if either reader has committed arson at the oil-covered oasis; if so, the water is now clean, and they can proceed to the oasis scene above. If not, they’re stuck with the mucked-up version. (I admire this rather simple ecological model: Fire applied to oil spill = clean water.)

The World in getWorldFlag is important; these variables quite literally represent some aspect of the “game world” by letting the author define variables that can be read and changed by both readers.

There are two kinds of script tags in WeBook:

  1. The “standard” tags provided by WeBook itself, which provide flow control, variable assignment, and other stuff you’ll probably need in any book.
  2. Custom tags that are implemented by the custom book engine for the book you’re writing.

We’ve seen several custom tags in these examples already. All of loseLP, loseCP, lost, sandsKillSteed are provided by the Challenge of the Magi engine, rather than by the core WeBook server itself.

Works For Me

If you’ve ever used template languages to dynamically alter text, the WeBook format may be giving you a headache. Many of the tags exist only to change the state of the reader’s session; they might generate text as a side-effect to let you know what happened, but their primary purpose is to implement the “game” behind the book. In contrast, most template functions in text-templating libraries avoid causing side-effects.

There are certainly many other ways to approach this problem, but this format checked off a few important boxes for me:

  1. It lets me write the book as a single, continuous text, which means I don’t need too many fancy tools to do the writing.
  2. I can dump the whole thing in a Markdown renderer, and the scene anchors let me click on the choices to jump around the book in a reasonable facsimile of what the reader would experience.
  3. I can borrow an existing text template processing language to implement the script blocks. (In this case, I’m using Go’s text/template.)

This all sounds great and minimalistic, but I was somewhat worried of how difficult it would be to test these scenes after I’d written them. I thought I might end up needing to build a lot of complicated tools to support this relatively simple format, which sort of defeats the purpose of keeping it simple in the first place.

Well, I’m happy to say that this hasn’t been my experience so far. Two changes made testing quite easy:

  1. The server has a “wizmode” that lets the reader skip several constraint checks, such as enforcing that a choice was available from your current location. This means I can use an HTTP client to easily jump around to different scenes and test all the branches of a scene in one go. (More on the server later.)
  2. Much like the “debug rooms” of old, I can quickly write scenes into the book that are used to change the reader or world state in any way I like. Using (1), I can jump to those “debug” scenes anytime, and then jump back to the scene I was testing.

Here are some examples of debug scenes that have already seen much use:

<a id="gold"><h2>Cheat.</h2></a>
# Cheat

{{giveGold 1}}
Have some gold. You have {{getInt "item-gold"}}.

<a id="water"><h2>Cheat.</h2></a>
# Cheat

You have water now. {{setFlag "has-water" true}}

<a id="steed"><h2>Cheat.</h2></a>
# Cheat

You now have a horsey. {{setFlag "steed-horsey" true}}

System Architecture

The core of WeBook is the WeBook command-line tool, which is written in Go. I run it locally to preprocess books before running them, and it also runs the HTTP server. The server uses Go’s net/http package to implement the API.

I run the server on Heroku, because it is easy and cheap.

I use MongoDB for persistence, hosted on Atlas.

Finally, the client is an Expo React Native app,

Software Architecture

The WeBook software architecture is sketched below. 2 means Game owns two Players, and * means a Book has many Sections. Underlined entities are aggregate roots, which roughly means they’re responsible for the persistence of their object tree.

WeBook software architecture diagram

After writing a WeBook, you use the WeBook executable to compile a Book into its Sections. “Compilation” is an overwrought term for this process; it really just cuts the book into the sections demarcated by the HTML anchor headings, does some naive checking for dumb mistakes, and persists them.

Once the WeBook Server is running, clients can start a Session to read a specific book with a partner. The Session is responsible for the application-level concerns of reading together, such as managing how your partner joins the session, persisting the state of the game after each choice, and keeping both readers notified of important changes.

The Game actually reads your WeBook. If I was to revise this aggregate, I would probably call it WeBook and rename Player to Reader, because this is closer to how I think about it. (This is the problem with writing too many half-finished game engines; I end up thinking in games even when I didn’t really mean to.)

When you first start a session for a specific Book, it creates the Game and instantiates the specific Engine that implements the ruleset for that Book.

A Game can be in read mode or engine mode. read mode is a pretty straightforward implementation of Choose-Your-Own-Adventuring: When the reader makes a choice, the Game interprets that choice and returns its Outcome. (Outcome was omitted from the diagram; you can think of it as a response object for a WeBook request.)

The Outcome is generated by loading the relevant Section of the Book and interpreting its script. The base interpreter is equipped with a standard set of script functions, which are merged with a library of game-specific functions provided by the Engine.

Some of these script functions can kick the Game into engine mode. This mode is used when it’s easier for the book author to write code to handle choices instead of programming it into the book itself. For example, the Engine for my Duelmaster port implements character creation and the battle system in Go code.

What I Found Energizing

There were several things I loved about doing this project.

Messiness

I let myself make a mess. I wanted this to be a creative project, not an exercise in software engineering. I was especially loose with the Duelmaster implementation, because I wanted to play with how it felt to actually create in this odd little framework that I was creating.

I was also quite messy in how I worked. I was on an extended break during this project, so I just sort of let it take over for a while. There were many days where I had my hands on the keyboard by 8am, and didn’t really stop until 2 or 3am. It was extremely refreshing to work with this kind of mania, especially after having gone through a long period of personal difficulty even before the pandemic started.

Modern Development Tools

I got a huge thrill from the out-of-the-box workflows for almost every technology I used in this project. I was running Linux for the first time in ages, and pretty much every underlying system I’d relied on 15 years ago had undergone a major usability upgrade. I’ve written several substantial apps in Golang before, and I always appreciate how simple it is to write great, developer-friendly tools with it. Finally, I was extremely happy with how conscientious the tooling around React, React Native, and Expo are. It was magical to see so many toolkits offer an eject option, which let me start with training wheels and then strip away these dependencies if and when I felt like it.

Everyone Understands It

It was really fun to work on something that my friends and family understood. They may not have appreciated why a 40-year adult would be doing such a thing on his spare time, but this wasn’t from a lack of comprehending it.

Expo makes it super easy to boot my working app on anyone’s phone, which further contributed to this terrific feeling of connection; I went on a couple of walks where someone asked me what I’d been up to that afternoon, and it took me just a minute or so to set up a WeBook session that we could experience together. In a time when it was hard to find ways to relate to people, this was a very inspiring thing to have at my disposal.

What I Found Exhausting

Like any creative project, there were a couple of things that I found very draining about working on WeBook.

Impedance Mismatches

I first ran into the concept of an impedance mismatch in grad school, when studying the simultaneous development of object-oriented programming and relational databases. I then had many, many years of experiencing this particular sort of mismatch first-hand in object-relational mappers.

There were two impedance mismatches that I found particularly frustrating when working on this project. I haven’t dug too deeply, but my more-experienced friends assure me that the Javascript-industrial complex has generated an intense amount of discussion on both of these already, so I won’t spend too much elaborating on them. I trust that the problems are in good hands. (This isn’t sarcasm; I really do think this situation will materially improve in the next while.)

The first impedance mismatch that drove me somewhat berserk was how React’s state management model interacted with other resources exposed through SDKs or APIs. I’m often quite impressed with how easy native APIs for things like Websockets or local browser storage are; I rarely feel the need to write my own wrapper over them, like I might have needed to for similar things 5-10 years ago.

However, React’s state allergy often meant that I needed to write an awful lot of code to bridge the more stateful API design that some of these resources used natively. I found this profoundly irritating in a project where I was trying to keep things as simple as possible; often times I would just give up and yarn add some library that exposed what I needed through a hook or a prop, only to discover that I was pulling literally thousands of lines of code into my project. In these situations, I had one problem, but it required importing a tool that solved dozens of other problems I didn’t have yet. For a one-person toy project, this tradeoff puts a bad taste in my mouth, because my perception so far is that Javascript libraries fall out of maintainership very, very quickly.

The second impedance mismatch was between Typescript and third-party libraries that require significant integrations into my project. I mostly loved working in Typescript; when working on code that I wrote myself, or when using other stuff that was natively implemented in Typescript, it’s really quite delightful. I was rather surprised at how powerful the typechecker was, especially since it kept the basics quite accessible.

My difficulty showed up when trying to write or use annotations of libraries that were either not designed with Typescript in mind, or that relied heavily on the dynamism of Javascript by design. I was pretty surprised to see how often the examples I was looking at approached these situations by doubling-down on the typechecker’s power; I often found myself getting to the end of 200 line component only to realize that 150 lines of it were making the typechecker feel safe.

I have personally always had a bit of an issue with typecheckers that act as a “membrane” on top of a language that doesn’t natively support it; I had a similar reaction to mypy when it first started to become popular. The reason why I ended up using Typescript in this project is that I was ironically getting a bit tired of massaging my IDE to give me more insight into the dependencies I was using in the first place.

For Javascript in particular, I still think this state of affairs is much better than what we had, say, 10 years ago. However, it’s frustrating in a different way; one that feels more like drowning in complexity rather than gnashing my teeth at limited options.

Weaponizing This Project Against Myself

I have a penchant for stick-to-it-ness. I like to finish things. There was a point in this project where one part of me implicitly decided that it was time to stop messing around and finish the thing. As you might guess, this took most of the joy out of it.

The decision itself wasn’t a bad one, but I am upset that I made it unconsciously. I don’t even remember specifically what day it happened; I just recall that one week, I was playing around, and the next week I was sketching timelines into my notebook.

It didn’t take me long from there to realize I wasn’t getting what I wanted out of the experience. This led me to take a bit of a break from it, during which time I decided I wanted to at least document it for posterity.

Which leads us to the question: What now?

Future Work

After taking a few weeks to think about it, I came to the conclusion that, at this stage in my life, I want my side projects to have discrete and recognizable stopping points. That means that I want to work on things where I can say “I’m done!” in a reasonable amount of time, and then more or less forget that the thing exists for as long as I want afterwards.

I wrote WeBook as sort of a “platform” for collaborative gamebooks, and now that it’s mostly done, the idea of maintaining an app like this for even a year or two feels exhausting. Most of this comes from the fact that a library app doesn’t feel alive unless that library is growing regularly, so shipping it makes me feel obligated to keep it primed with new books at a rate that certainly requires more toil than I want from a side project.

Before writing this post, I had more or less decided to leave WeBook unreleased. I was feeling pretty satisfied at the idea of writing a long, meandering post about the experience and then letting it go.

However, the act of writing this report helped me realize another option. I think it will be better for both readers and for me if each book is released as a stand-alone mobile app. I can keep the server mostly intact, and the current WeBook platformy-style app can be “white-labelled” and re-released for each new book. This simplifies many things: Readers don’t need to spend as much time wrapping their heads around what the app does, and it’s easier to charge for each book if I want to go that route, since I can just charge for the app.

So, I expect to return to this project pretty soon. It was fun to make a mess for a while, but I get much more enjoyment out of completing things, and I think I’ve discovered the right way to do that for myself here.