- Common Loki Misconfigurations
- Iterating Through a List in Ink
- Debugging Misconfigured Container Networks
- Minimum Viable EC2 in Terraform
- Storylets in Ink
- Interactive Fiction Tooling Overview
- In-Place Resizing for Digitalocean Droplets
- Unity Demonstrates the Importance of FOSS
- Target Labels in Prometheus
- My View of AI is the Same
- Verify DNS Ownership with TXT Records
- Sane Droplet Defaults
- Editing Made Easy with Vim
- Gatsby Gotchas
- Concatinating Default AWS Tags in Terraform
- Easily Updating the Default Github Branch
- Lifetimes in Rust
- Checking for Bad Links
- Maybe TypeScript and React is Bad
- Static Asset Management in React
- Bundler Down Time
- Using React Context for Localization
- JS Implementation of a Sticky Footer
- Custom Aliases
- Trying Out the 7drl Challenge
- Trash Opinions
- Building Your First Program in Rust
- Fixing mongod reports errors related to opening a socket
- Improving Open Source Maintenance
- Technical Interviewing Tips
- Housekeeping Note
- Dynamic Programming Basics
- The Oddity of Naming Conventions in Programming Languages
- An Experiment Using Machine Learning, Part 3
- Debugging with grep
- An Experiment Using Machine Learning, Part 2
- An Experiment Using Machine Learning, Part 1
- The Value of while
- National Day of Civic Hacking
- OpenAI and the Future of Humanity
- Creating a Whiteboard App in Django
- Creating Meaningful, Organized Information
- Towards A Critique of Social Media Feeds
- Setting up Routes in Django
- Developing a Messaging Component for Code for SF
- Dream Stream 2.0
- Keyed Collections in Javascript: Maps and Sets
- Blog Soft Relaunch
- Scraping with Puppeteer
- Looking Ahead to Dream Stream 2.0
- Solving West of Loathing's Soupstock Lode Puzzle
- Installing Ubuntu
- Interview with David Jickling Evaluation
- Compare Text Evaluation
- Dream Stream Evaluation
Storylets in Ink
Emily Short defines storylets as having the following qualities:
- they are a piece of content
- they have conditions for when they can occur
- there are effects on the world state that result after they have played.
In another post, she describes the algorithm in plain English:
“Go through all the storylets, and list the ones whose requirements are currently satisfied. Next present a random sampling, or a complete list to the player to pick from. When the player picks one, display its content and execute the results, then go back to the beginning.”
Providing all the options available is easy enough, although could represent a UI challenge depending on how large the list becomes, but a random selection is an interesting idea I like. The ink language has many limitations, but it has sufficient data structures and routing capabilities to provide for this. We start with a list.
LIST Deck = (card1), (card2), (card3), (card4)
I’m calling this list variable Deck
based on Emily Short’s idea that storylets can work as a kind of deckbuilding game, but whatever name is appropriate can be replaced here. If your narrative is organized by location then you might call it Places
, or if it is character driven Characters
, or more generically Scenes
as they do in King of Dragon Pass, one of the more notable examples of a game that utilizes the storylet format.
As a quick review, a LIST
variable is not like a list in Python, or even a linked list (although you can easily add some functions to give it some linked list properties), but is actually a boolean set. So each element in the LIST type is just a boolean variable. When we include the element in parenthesis it is assigned to true
. Game state in any reasonably complex system can be difficult to track, so this is a useful data structure to use as a canonical source of truth. In our case we use the list for determining if a particular storylet is available for the player. If it is, we set it to true. In this simple example all storylets are available.
The loop of our game will involve a knot that acts as a container knot for our knot that provides available storylets.
=== main ===
-> storylets(Deck)
The main
container knot is important because we need to pass our list as a parameter to the knot. We’re going to perform some operations that we don’t want to affect the actual Deck
list.
The storylets knot is where the magic happens.
=== storylets(Storylets) ===
~ temp draw1 = LIST_RANDOM(Storylets)
~ Storylets -= draw1
~ temp draw2 = LIST_RANDOM(Storylets)
~ Storylets -= draw2
~ temp draw3 = LIST_RANDOM(Storylets)
+ [{choice(draw1)}] -> link(draw1)
+ [{choice(draw2)}] -> link(draw2)
+ [{choice(draw3)}] -> link(draw3)
The operations at the top involve selecting a randomly available card from the Deck
we passed early as the Storylets
parameter, and assigning it to a temp variable draw1/3
. After a temp variable is assigned we set that variable to false in Storylets
so that it can’t be selected again. Then we repeat the process until we have the number of options we want to present the player. In this example, I have designed it for three choices made available to the player.
Next comes the choices presented. As you can see, we rely on a function called choice
to provide the choice text (without this you’d be stuck with variable names for the choice). Then there is a divert to a knot called link
. Both take the draw
variables as parameters. So let’s take a look at what these are doing.
=== function choice(x) ===
{x:
- card1: The story of card one
- card2: The story of card two
- card3: The story of card three
- card4: The story of card four
}
Ink doesn’t have data structures like arrays or hash tables unfortunately, but it does have case switching blocks, so we can use those to provide the simple task of translating a variable name to a value that we want to use.
=== link(x) ===
{x:
- card1:
-> card1_knot ->
- card2:
-> card2_knot ->
- card3:
-> card3_knot ->
- card4:
-> card4_knot ->
}
A similar thing is happening with our link
knot. Note how it is using the “tunnelling” syntax, this way it is able to go from storylets
to link
to the actual destination without knowing in advance what storylets are going to appear, which it can’t since they are random.
And that’s the entirety of the plumbing for this structure. At this point you just write the knots that contain your content, and make them available by adding them to the deck, and enumerating them in the choice
function and link
knot.
Note that our storylets
knot contains no conditional logic about whether a storylet is available or not. As stated earlier, we are treating our list as a source of truth. So our list will need to be updated somewhere in the storylets themselves. A typical structure will often involve a player making a choice of some kind, which will have affects like making certain storylets available, and other unavailable.
If you’re interested in storylet implementation, be sure to look at this alternative approach by @smwhy here. There are many similarities in the implementation (I think out of necessity), but there are also some key differences. In this one the list is not treated as a source of truth for storylet availability, but instead as a First-In-First-Out priority queue of storylet variables. As a result, this requires an additional function to enumerate conditional arguments to see if a choice is available. Depending on the goals of your particular narrative design, this storylet construction could be a better model for what you are trying to accomplish.