Storylets and domain-specific languages: Notes on improving SimpleQBN

There are very few open source implementations of storylets. That has slowly gotten better over the last few years, but I can name three of them and I’ve written a fourth. (There are probably more! But I didn’t know of when I wrote this post.) When I was initially working on SimpleQBN in August 2020, the only other QBN library I knew of was TinyQBN. I based much of my design on Grams’ work and even used the same format for writing expressions.

Since that initial work, I’ve learned of some of the limitations of the operator-operation-operator format — what I started calling ‘the Grams format’ at some point in the past and is now how I refer to it in my own notes — that have made me want to find something different for my library. For its context of using tags (and using text in a passage!), TinyQBN works really well, but I wanted something more for SimpleQBN, since it is not bound to Twine 2 or use within the SugarCube story format.

Expressing prerequisites

A common definition for the storylet model comes from Emily Short in a 2019 post called Storylets: You Want Them. In the post, Short (2019) details storylets as consisting of three general parts I summarize as the following:

  • Content.
  • Prerequisites.
  • Effects on (world) state.

(If you prefer alliteration, you might also think of them as Content, Constraints, and Changes.)

The issue of data representation haunts interactive fiction. Within the concepts defined by Short (2019), there are no details on how prerequisites might be expressed. This is not a fault in any way of Short (2019), who is explaining storylets, but something indicative of a long history of interactive fiction and various forms of data representation. No matter how a story, for however that might be defined, is defined within data, it both allows certain forms of access and creates barriers to others. This is a function of language itself: no matter how many connections and associations a word creates, it always closes some in the process of usage. Concepts are slippery in that way.

The problem of expressing prerequisites is a problem all implementations of the storylet model has to face. Most, as I am about to cover, introduce their own form of what is called a domain-specific language (DSL).

Operators, when lambdas, and conditional statements

TinyQBN implements prerequisites in the Grams format. Harlowe 3.2 uses something Leon calls when lambdas, and the StoryletManager system uses conditional statements and global values supplied by SugarCube in Twine 2. What these all have in common is a language created to solve a specific domain of problems. More generally, such a language is called a domain specific language (DSL). Technically, for example, the coding parts (macro usage) of story formats like Harlowe and SugarCube are all DSLs, as they only exist within the domain of usage of Twine 2. If you are implementing storylets, it seems, you have to either create your own DSL or use someone else’s work on one.

Bruno Dias (2017) hits on this exact problem in a post called Attempted: Building a general-purpose QBN system. Dias (2017) writes that using a DSL “adds complexity and learning curve; new authors can’t benefit from preexisting programming knowledge, while new authors who aren’t programmers still have to learn a programming language” (para. 13). Any use of a DSL comes with its challenges. It has to be easy enough to use with an implementation without too much work, but also not too complicated. Few people like learning a new language for use with only one library. They can’t “benefit from preexisting programming knowledge,” as Dias (2017) writes.

Revisiting (storylet) history

Last month, there was a brief dustup on Twitter that branched out into multiple conversations. I was actively tracking two of these branches. The first was a dig into earlier examples of storylet design and concepts (which I documented here) and the other had to to do with conversations about DSL and the problem of prerequisite representation. Since that time, I’ve been privately playing with some different models and possible solutions. After a few different attempts, I settled on a solution I really like — and have now updated SimpleQBN to use it.

The problem, as outlined by Dias (2017), has to do with not wanting to learn a new language. Any solution, I thought, has to be capable of using complex comparisons, has to be something relatively easy to learn, and able to easily mix with JavaScript. Structured Query Language (SQL), for example, is (perhaps debatably) a DSL. It works really well to define complex search queries and is somewhat easy to learn. (Parts of it are definitely hard to learn, but I’ve taught it a few times.) However, mixing SQL with JavaScript for a storylet project seemed major overkill. There is the SQLite3 package for Node.js, but there isn’t a need to define how the data is defined or removed. That can be handled by the internal state management. All that is really needed is a fast query language.

I teach MongoDB as part of some of my web development courses. Something I really like about its query language is how close to JavaScript it is. It has its own operators, but most simple searches look like the following:

{score: {$gte: 10}}

The above object uses the $gte operator to test if there is a field (think property for JavaScript objects) that exists, is score, and that its value is greater than 10. Other than learning the specific operators defined in the MongoDB Query language (MQL), most of the learning work happens in understanding objects and properties in JavaScript. This makes it ideal for working with full-stack development using Node.js as a base. It’s all objects (well, technically JSON for communication) up and down the stack!

This idea of using MQL led to me to Mingo. It implements most of the MQL operators, but is designed to work with objects and arrays in JavaScript. In fact, it specifically doesn’t include operators like $set used with updating operations with a MongoDB API method. It is just the query language part.

Mirroring state in prerequisite expressions

One of the side effects of using MQL led to an interesting discovery. Simple equating statements in MQL mirror state expressions! For example, I have now changed my State class in SimpleQBN to use an internal object with properties for its keys and associated values. This means adding a new State key-value pair of “score” and 15 produces the following output:

{score: 15}

The use of the key score would retrieve the corresponding value of 15 from the internal state based on the output shown in the above example. For a Card with the quality (prerequisite expression) using MQL via Mingo, it would also be the same:

{score: 15}

In MQL, any use of key-value pairs without operators is assumed to be equation. Thus, the above example would test if the internal score within State is equal to 15. (And it is, based on the previously set value!) The condition of the State would be mirrored in prerequisite expressions. This makes them much easier to understand from the user’s point of view, I think. For a selection of content (what SimpleQBN calls a Card), the MQL expression would be close to the state of the project. Testing for equation would look the same as setting up the internal values!

If content is text and prerequisites require a DSL, do effects also need a DSL?

TinyQBN and StoryletManager both benefit from being used with Twine 2 and the SugarCube story format to make state changes. Harlowe 3.2 can use the built-in DSL of its own keywords and macro usage to update values and react to them. In SimpleQBN, well, that’s something I’m still struggling with deciding if it is needed or not.

Currently, SimpleQBN uses the method set() to change values in its State class. This works well for its purpose of setting state, but does not provide a way to put that same effect inside of the content itself. Based on the availability of content (Cards within a larger Deck in SimpleQBN), content can be selected based on changing State values, but a Card does not affect State. In fact, it is the other way around: a Card is only available if the values in State match the (MQL) expressions within its collection (called a QualitySet in SimpleQBN).

As Dias (2017) concludes, “variable content and scripting is very important to making the whole thing work” (para. 15). That’s the same problem I’ve now encountered. I can build a Deck, select one or more Cards, but the content of each Card do not have any effects.