Create a news game with Ink, React and Redux: Part I, scripting in Inky

Ændra Rininsland
Journocoders
Published in
11 min readMar 15, 2018

This is part one of a two-part series about creating news games using Ink, React and Redux. We start by writing our story, then rendering it using React and Redux in part two.

This part covers Ink and Inky; the second part adds React and Redux

News games can be incredibly complex, such as Bloomberg’s American Mall game, or far simpler, resembling classic adventure games of old with a linear storyline, such as in Financial Times’ The Uber Game.

In this tutorial (originally written for the Journocoders London March 2018 meetup), we will create a simple game using Ink, a Domain-Specific Language (DSL) for writing looping storylines. Then, in the second part, we’ll wire it together in JavaScript using InkJS, using React for component rendering and Redux for state management. If you want to jump to the finished codebase to see what it looks like, fork this Glitch project or clone this GitHub repo. We used Ink and InkJS at Financial Times to create The Uber Game — giving that a quick play will give you a sense of what’s possible with the Ink language.

Weaving a mystery with Inky

The open-source Ink language was created by a company called Inkle Studios, which has also released a full editing environment that we’ll make use of when writing our stories. Go to GitHub and download the latest release of Inky for your platform, then install and run it. You should get a screen like this:

On the left side is where you write your stories in the Ink language. On the right side is an interactive pane that allows you to test different story constructs. Above the right pane are two buttons that let you jump around in the story’s internal state. The left pane has some buttons above it that allow you to create multiple files in your story, if you feel so inclined to organise your work or some such.

We’re going to write an incremental game where the player contemplates life as a Cyberian bot farmer (Which, of course, is a reference to the 1994 PC adventure game Cyberia, and not at all a tongue-in-cheek reference to a particular nation state that may or may not be known for such bot farms, with any and all similarities purely coincidental). This will utilise a series of game loops and variables. Our endgame will be once the bot farmer controls 760 million bots, which is a tenth of the population of planet Earth. There is a lot of scope to expand the game — perhaps reaching the endgame triggers a totally different playing style as per something like Universal Paperclips. I’ll leave this as an exercise for the reader.

The Ink Language

Ink exposes a Domain-Specific Language (DSL), which is a programming language created with a specific use case in mind. The Ink DSL is very simple, with an eye towards easy text editing and story writing. In many ways it’s similar to Markdown in that it promotes readability, which is a useful trait when sharing project text with non-developer staff like editors and subeditors.

There’s no way I can possibly get to every nuance of the Ink language, which is fairly detailed and has a lot of advanced constructs useful for writing complex dialogue and storylines. I highly recommend reading through Ink’s fantastic documentation before venturing out into your own projects .

We’re going to mainly stick to constructs from the Ink language that will seem familiar to you if you’ve done even a bit of coding: variables, constants, functions and conditionals. Additionally, we’ll leverage a few things unique to the Ink DSL, namely knots, tags, choices, and diverts, which are used to tie bits of story together and create loops.¹

Opening Inky.app, start your new story with a multiline comment to let people know who wrote this fabulous game. Comments work the same way as they do in JavaScript and other POSIX-compliant languages:

/**
* Cyberian Bot Farmer!!!!
* 2018 Ændrew Rininsland (@aendrew)
*/

You can also use single-line comments:

// This is an inline, one-line comment

You technically don’t need to write comments to make Ink work, but if you’re sharing your work with editors and other journalists it’s really important that you explain what something does if it’s not readily apparent. Plus, like, they’re fun!

We’re going to kick everything off by using our first special Ink construct — a divert:

-> Beginning

This means “go to a knot labeled Beginning.” You can use them pretty most places, except functions, which is admittedly kind of annoying, but we’ll manage. We’ll write the “Beginning” knot in just a second.

First, though, let’s set up a bunch of global variables to track player state through the game:

// All of our filthy, disgusting global variables lololol
VAR canFarmFocebork = false
VAR canFarmTwurtur = true
VAR twManagers = 0
VAR fbManagers = 0
VAR totalFBBots = 0
VAR totalTWBots = 0
VAR totalRubels = 0
VAR totalFBDudes = 0
VAR totalTWDudes = 0
VAR fbManagerRate = 3
VAR twManagerRate = 6
VAR fbDudeRate = 3
VAR twDudeRate = 5
VAR twBotRate = 3
VAR fbBotRate = 8
CONST MAX_BOTS = 760000000 // INT_MAX in C++/Ink is 2147483647 lol.

As you can see, we have two booleans setting flags for what kinds of Bots can be farmed, giving the player access to the “Twurtur” bot recipe from the outset. We have two types of Managers and bot wranglers (hereafter “Dudes”) to coincide with two types of bots. Dudes, Managers and Bots all have rates attached to them, that you can tweak during play testing to get different types of game styles. Lastly, for our end-game criterion, we need a constant to hold 750 million.²

Let’s create that “Beginning” knot now. A knot is like a scene in your story — it’s where decisions are made and much of your story text is written. You define them with two equal signs:

== Beginning ==

You can add sub-sections to your knots (called stitches) using one equals sign, but let’s keep things simple and just totally not do that.

One useful thing to put beneath your knot declarations are tags. Tags contain metadata about constructs in the game. In our JavaScript code, we can read the tags and use them to do things like set background images. Add the following line:

# background: beginning

Okay, time for our first piece of descriptive text!

You have inherited a farm from your uncle {~Jim|John|James|Vladimir}.

When you want to do something fancy with code in your story text, you use curly brackets. In the above, what we’re doing is creating a list of options, and then (using the ~ operator) picking a random one. Time for more descriptive text!

Gazing across the frozen wasteland, you doubt anything will grow here. 
Luckily, you have decent Internet connectivity and have been told there's a growing industry for social media consultants in the area...

Ooh, atmospheric…

* [Go to your farm]
You head towards your farmhouse to plan what to do.
-> WhatToDo

And thus we arrive at our first choice, which are presented to the player in a list. Choice options are denoted by either a * character (which lets a decision be used only once) or a + character (for repeatable decisions), in a sort of unordered list format. You’ll have a better idea what I mean by that in a second. The choice text comes immediately after the aforementioned operator, and by default gets output once the decision is made, or suppressed by wrapping the text in square brackets (such as I do above and everywhere else in this story, given there’s no player dialogue). Afterwards, you can put some text to display to the screen if that choice is taken, transitioning to a new part of the story via a divert (such as we do above).

To be totally honest, after that first knot, there’s really not a lot more in terms of language constructs really left to talk about. We’ll get through the remainder in this next knot and then speed through the rest so we can get on with the rest of the tutorial.

Our next knot is going to be our “End” knot, which is where the player gets diverted during the end game state. We’re bizarrely going to put it at the beginning, because otherwise Inky’s going to throw errors the entire time we’re working on this.

== End ==
# background: ending
IT'S OVERRRRR CONGRATS GO HOME
-> END

It’s worth noting this is a knot called End that then diverts to a state called END. The latter has a special significance in Ink, as it implies the story loop has terminated and the game is over. In practical terms, we use this to throw an exception in our JavaScript code, which we catch and then use to trigger a much more elaborate ending scene. Which you totally should, because boring ending sequences are TOTALLY. THE. WORST.

Next we have our WhatToDo knot, where the player will return many times and enables travel between The City and the player’s farm.

== WhatToDo ==
# background: farm
{ tick() == true: -> End }You arrive back at your { farmhouseType() } farmhouse and ponder your next move.You have a few options here:
+ [Farm a bot]
-> FarmABot
+ [Go to The City]
-> GoToCity

The choices at the bottom should seem pretty familiar to you by now, but this time around we use a couple of functions to really get the game rolling, which we’ll define now. The farmhouseType() function is the simpler one so we’ll do that first:

=== function farmhouseType ===
{
- totalRubels < 1000:
~ return "shanty"
- totalRubels < 10000:
~ return "decrepit"
- totalRubels < 100000:
~ return "modest"
- totalRubels < 1000000:
~ return "swank"
- else:
~ return "incredible"
}

You can put the above anywhere. One option is to create a separate functions.ink file somewhere and then INCLUDE that, but this game is simple enough we really don’t need to bother — I’ve personally just stuck them all right beneath my global variables.

As you can see, functions are defined by three equal signs and the word function. They’re also different from knots in that you can return a value instead of just printing it (though you can definitely do that as well.). In this one, we have a simple if-then-else conditional that returns a descriptor of the player’s farmhouse depending on how much money they’ve made. Logic gets wrapped in curly brackets and conditional branches are separated by dashes and end in colons.

Next to add is the tick() function, which is intended to update the player’s status based on every turn:

{ tick() == true: -> End }

Our game is going to fundamentally differ from a lot of incremental games where a counter constantly goes up over time — in order to keep this example simple, we’re simply going to do a bunch of addition and assign it to global variables whenever the player moves to a new knot; if the tick function returns true, the game transitions to the final scene.

Here are all of our remaining functions. They’re really pretty self-explanatory, I could have jammed them all into tick() but separating them out reads more easily:

=== function totalBots ===
~ return totalFBBots + totalTWBots
=== function totalDudes ===
~ return totalFBDudes + totalTWDudes

=== function totalManagers ===
~ return twManagers + fbManagers

=== function fbIncome ===
~ return totalFBBots * fbBotRate

=== function twIncome ===
~ return totalTWBots * twBotRate
=== function tick ===
~ totalFBDudes += fbManagers * fbManagerRate
~ totalTWDudes += twManagers * twManagerRate
~ totalFBBots += fbDudeRate * totalFBDudes
~ totalTWBots += twDudeRate * totalTWDudes
~ totalRubels += twIncome() + fbIncome()
Currently you have ₽{totalRubels}, {totalBots()} total bots, {totalDudes()} dudes and {totalManagers()} dude managers working for you.
~ return totalBots() > MAX_BOTS

You may have noticed we didn’t even bother with arguments in any of these functions, because again we’re using a whole boatload of nasty global variables anyway and lol whatever #YOLO? If we did use arguments, though, they’d look like:

=== function herpa(derpa) ===

Okay, we just need to create a few more scenes and we’re done with the Ink part of this. I’m just going to dump them with minimal explanation — please feel free to highlight any bits you don’t find obvious and I’ll try to add a comment.

== FarmABot ==
# background: barn
{ tick() == true: -> End }+ { canFarmFocebork } [Farm a Focebork bot]
You farm a Focebork bot
~ totalFBBots += 1
-> WhatToDo
+ { canFarmTwurtur } [Farm a Twurtur bot]
You farm a Twurtur bot
~ totalTWBots += 1
-> WhatToDo
+ Go back home
->WhatToDo

This screen is reached from the WhatToDo knot. Again we have our tick() function and a bunch of repeatable choices. The player can farm either a “Focebork” and “Twurtur” bot (“any all resemblances purely coincidental &c., &c., &c.”), if they have the appropriate recipe (you may recall from earlier that we give the player the “Twurtur” recipe right off the bat; you could easily set that to false and require the player meet a certain character first, for instance, but we don’t here for the sake of brevity).

== GoToCity ==
# background: city
{ tick() == true: -> End }You make it to The City.
You have ₽{totalRubels} to spend.
What do you want to do?
+ [Hire somebody to make some bots]
You go to the local tavern
-> LocalTavern
+ [Buy a recipe]
You go to Bot Recipes R Us
-> BotRecipesRUs
+ [Go home to your farm]
You go home
-> WhatToDo

This is the main decision tree when the player visits The City from the WhatToDo knot. Just a tick() and a bunch of repeatable choices. Cool, easy.

== BotRecipesRUs ==
# background: store
{ tick() == true: -> End }The proprietor of Bot Recipes R Us welcomes you into his establishment."Welcome! Welcome!!!""I have the finest bot recipes!"You have ₽{totalRubels} to spend.* { totalRubels >= 90000 and not canFarmFocebork } [Buy a Focebork recipe (₽90000)]
~ totalRubels -= 90000
~ canFarmFocebork = true
You buy a Focebork recipe.
-> GoToCity
+ [Go back to the city.]
{ totalRubels < 90000: Your broke ass can't afford anything anyway. }
-> GoToCity

Here we have a tick(), some dialogue text and the option to buy a “Focebork” recipe if the player has over 90,000 credits and doesn’t already own the recipe. We also have a snarky bit of dialogue come up when the player needs to go back to the city and can’t afford a recipe.

This is the last big knot and it’s a doozy — it’s where you hire your Managers and Dudes:

== LocalTavern ==
# background: bar
{ tick() == true: -> End }A motley crew of surly men sit drinking vodka.You can:+ { totalRubels >= 10000 } [Hire a Dude Manager]
You can hire the following dude managers:
++ { canFarmFocebork && totalRubels >= 100000 } [Hire a Focebork dude manager (₽100000)]
You hire {~Dmitri|Boris|Ralph}
~ fbManagers += 1
~ totalRubels -= 100000
++ { canFarmTwurtur && totalRubels >= 10000 } [Hire a Twurtur dude manager (₽10000)]
You hire {~Dmitri|Boris|Ralph}
~ twManagers += 1
~ totalRubels -= 10000
+ { totalRubels >= 100} Hire a Dude
You can hire the following Dudes:
++ { canFarmFocebork && totalRubels >= 1000 } [Hire a Focebork Dude (₽1000)]
You hire {~Dmitri|Boris|Ralph}
~ totalFBDudes += 1
~ totalRubels -= 1000
++ { canFarmTwurtur && totalRubels >= 100 } [Hire a Twurtur Dude (₽100)]
You hire {~Dmitri|Boris|Ralph}
~ totalTWDudes += 1
~ totalRubels -= 100
+ [Go back to the city.]
- You leave the tavern.
->GoToCity

Here we introduce two new elements: nested choices and a gather. As you can imagine, choosing one of the choices denoted with one + symbol enables you to choose one of the choices beneath it denoted with ++. It should be noted at this point that in the Ink language, indention doesn’t do anything other than make your colleagues not hate you.

A gather, on the other hand, is a choice that happens after any of the above choices are selected, and are used to bring dialogue back together. In this instance, you can only hire one person per trip to the tavern, so you end up leaving after any decision.

Oh my freaking God, are we at the JavaScript bit yet?!

Yes. sorry.

Click here to go to Part II.

Ændrew Rininsland is on Twitter at @aendrew and recently redid his blog using React, Gatsby and p5.js to look totally sickening.

He is a senior developer with the Graphics team at Financial Times and any ridiculous antics, shady depictions of a particular foreign power, or otherwise poor takes of any sort, are clearly his alone and not that of his employer.

He also recently did a book about D3, which has been variously described as “not for newbies” and “impenetrable”.

--

--

Ændra Rininsland
Journocoders

Newsroom developer with Interactive Graphics at @ft. she/her or ze/hir. Rather queer. Opinions are mine, not employers. I'm hackers.town/@aendra on Mastodon.