Writing IF with Raconteur, part 1: a difficult situation

Undum is a system for writing hypertext interactive fiction, similar to Twine. It’s probably one of the most powerful, versatile, better-looking systems, but it’s also pretty complicated to use; Undum stories are written by editing a JavaScript file, and essentially you write an Undum game by modifying and adapting Undum itself to your needs.

Raconteur is Undum with batteries included,” a set of tools and libraries that speed up Undum development and give it a gentle learning curve. Raconteur, like Undum, has an API documentation out; but API documentation is great as a reference, not so much for learning something new. And Raconteur, while (I hope) still substantially easier to work with than Undum, still has a learning curve.

This is the first in a series of posts walking through the authoring of an IF game with Raconteur/Undum. This one goes from setting up a development environment to writing down your first situation.

Before we begin

There’s no escaping the fact that this is a programming tutorial. It’s a pretty gentle programming tutorial, but if you’re totally unfamiliar with JavaScript, now might be the time to brush up. I’ll assume at least a basic level of programming knowledge. Raconteur is designed for CoffeeScript, a language that compiles to plain JavaScript. CoffeeScript has cleaner syntax and some useful features, but its semantics are pretty much the same as JavaScript’s; if you have used JavaScript before but not CoffeeScript, a quick read of the CoffeeScript page is really all you need to get started.

Raconteur’s toolchain” is built on a number of command line tools. OS X and Linux users are probably more comfortable using those; if you’re on Windows, I recommend installing PowerShell or cygwin to get a better command-line interface that’s more like the Unix interface I’m using and will be referencing in this tutorial. Lines beginning with a $ symbol are command lines, as is tradition.

Setting up Raconteur

Raconteur relies on several tools to work. Most of them will be installed automatically by npm, but first we have to install Node.js. You can download a Node installer directly and install it (Recommended for Windows and OS X users). On Linux, your distribution probably packages Node, and you can install it with apt, yum, or pacman (or one of the GUI frontends for those commands, like Ubuntu’s software center). OS X users can also install Node through Homebrew.

The node package manager, or npm, is bundled with Node itself, though if you installed Node through a Linux package manager, for example on Ubuntu, you may also have to install npm as a separate package. Once you have npm working on your system, you’ll want to install Gulp.

Gulp is Raconteur’s build system. You’ll want Gulp installed globally” (Really, locally to your user account) for ease of use, since we’ll be using Gulp as a command-line tool:

$ npm install -g gulp

Finally, we can set up Raconteur itself. Download the scaffold zip file and unpack it; you can move or rename the raconteur-scaffold-master directory anywhere you like. From inside that directory, do:

$ npm install

This will install a local copy of Raconteur for your game, along with all of the tools and dependencies it uses. It’ll take a while to install everything. Once it’s done, however, you can start up the development server:

$ gulp serve

If everything is working correctly, you should see something that looks like this on your terminal:

terminal screenshot showing successful build

If you open your browser and point it at localhost:3000 (Which should happen automatically), you will see a Raconteur Scaffold” title card, and you’ll be able to click it and see the first situation of the scaffolded game. You’re good to go!

The Files

The scaffold has the following file structure, ignoring folders like the node_modules directory where npm installed all of the dependencies:

.
|-- game
|   `-- main.coffee
|-- Gulpfile.js
|-- html
|   `-- index.html
|-- img
|   `-- storyteller.jpg
`-- less
    |-- main.less
    `-- mobile.less
  • main.coffee is the entry point for your game. You can add other files (And require them), but the assumption is that your game will mostly live on this one file, which will define your game’s story and mechanics. Mostly we’re going to be dealing with this file.
  • Gulpfile.js is the build system configuration file. Feel free to peek inside if you’re curious, but you don’t need to edit this.
  • index.html is the actual html page for your game. You’ll want to make a few straightforward edits to this to add your game’s name, legal information, and so on. It’s commented with EDIT in caps just before everything that needs to be changed. If you want you can leave this alone for now, too.
  • storyteller.jpg is an example image included with the scaffold. Any files with the extensions .png, .jpg, and .jpeg that you put in the img/ folder will be copied directly to your game distribution; obviously this is where you can put any images you want to include in your story.
  • main.less and mobile.less are Less files. Less is a language that gets compiled down to CSS. It has things like variables, mixins, and functions; you can think of it as a more convenient version of CSS, though all valid CSS is also valid Less. main.less is the file that Gulp is set up to compile; it includes mobile.less, which contains mobile-specific CSS definitions. For now, don’t worry about them.

Main.coffee

Open up main.coffee in your preferred (programming) text editor. As the file extension implies, main.coffee is a CoffeeScript file. CoffeeScript is a language that compiles down to JavaScript; for us, the main benefit is that it has a cleaner syntax and supports string interpolation (we’ll get to those in a moment). But first, I’ll walk you through the contents of that file.

# Require the libraries we rely on
situation = require('raconteur')
situation.exportUndum() # Ensures our Undum object is the same as Raconteur's
$ = require('jquery')
oneOf = require('raconteur/lib/oneOf.js')
elements = require('raconteur/lib/elements.js')
qualities = require('raconteur/lib/qualities.js')
a = elements.a
span = elements.span
img = elements.img

This is basically boilerplate. require() is a function (in this case defined by Browserify) that essentially does the equivalent of an import” or include” statement in another language. All of this is stuff you don’t really need to touch for now, but it’s helpful to look at because you can see the names of everything we’ve imported: situation, $ (JQuery), oneOf, elements, qualities, a, span, and img. Deliberately, this is all of Raconteur; if at the end of your project you find you didn’t use some part of it, you can take out the require() statement to make the bundle smaller.

The line saying situation.exportUndum() is a call to a function in Raconteur that makes sure there is a global Undum object which is the same global Undum object everything is using. How this works might change in the future, but for now, Undum relies on that global object existing.

# ----------------------------------------------------------------------------
# IFID and game version - Edit this
undum.game.id = "my.game.id"
undum.game.version = "0.1"

Every Undum game should have an unique id and version. Those are used for save games; if your game has the same ID as another game, then your saves will collide. Very bad. I strongly recommend using an [UUID], which are pretty much guaranteed to be unique and are also valid IFIDs.

# ----------------------------------------------------------------------------
# Game content
situation 'start',
content: """
![a storyteller](img/storyteller.jpg)
# Welcome to Raconteur
If you're seeing this, you've successfully installed the Raconteur game
scaffold. Get writing!
Raconteur lives at a [Github Repository], where you can report issues or
send feedback.
[Github Repository]: https://github.com/sequitur/raconteur
"""

This is where it actually begins: The first situation. I’ll explain what all of this means in a moment, but first let’s finish the tour of the game file.

# ----------------------------------------------------------------------------
# Qualities
qualities
stats:
name: 'Statistics',
strength: qualities.integer('Strength', {priority: '001'}),
dexterity: qualities.integer('Dexterity', {priority: '002'}),
constitution: qualities.integer('Constitution', {priority: '003'}),
intelligence: qualities.integer('Intelligence', {priority: '004'}),
perception: qualities.integer('Perception', {priority: '005'}),
charisma: qualities.integer('Charisma', {priority: '006'})
possessions:
name: 'Possessions',
gold: qualities.integer('Gold'),
sword: qualities.wordScale('Sword', ['dull', 'sharp']),
shield: qualities.yesNo('Shield')
options:
extraClasses: ["possessions"]

Qualities are one of Undum’s most unique features, an explicit list of items that represent your character, which is very malleable. Here I have an example cribbed from fantasy gaming, but really qualities can represent anything: The Play used them to represent the moods of the various performers; Living Will, the various legal fees and bequeaths; Almost Goodbye used it as a combination checklist (of people to talk to) and indicator of the player character’s mood.

I will talk more about qualities in a future tutorial.

#-----------------------------------------------------------------------------
# Initialise Undum
undum.game.init = (character, system) ->
# Add initialisation code here
character.qualities.strength = 10
character.qualities.dexterity = 12
character.qualities.constitution = 10
character.qualities.perception = 14
character.qualities.intelligence = 16
character.qualities.charisma = 8
character.qualities.gold = 100
character.qualities.sword = 1
character.qualities.shield = 1
# Get the party started when the DOM is ready.
$(undum.begin)

You should set undum.game.init once and only once. It gets passed character and system, Undum’s two state objects (which are not globals but rather are passed as arguments into your code). This function gets called once by Undum, right when the game begins, Here as an example I am initialising all of the game’s qualities by setting them initial values.

The last line is a little mysterious, probably because it’s a bit of a hack. undum.begin() is a nonstandard addition of the modified version of Undum used by Raconteur; it tells Undum to start running. Passing a function as an argument to JQuery binds that function to run when the DOM is ready, ie when everything has been loaded by the browser and is ready to go.

Meet situation()

situation() is the heart of Raconteur, a new way of defining Undum situations. It has a lot of complexities, but it’s designed so that you don’t have to know about all of them until you need them.

What it is is a function that takes two arguments: The name of a situation, and an object literal to act as a spec” for that situation. CoffeeScript’s syntax is a little spare, so it might be hard to see exactly what is going on. Here’s the same situation side by side in CoffeeScript and the JavaScript it compiles to:

situation 'example',
content: 'The quick brown fox jumps over the lazy dog.'
situation('example', {
content: 'The quick brown fox jumps over the lazy dog.'
});

As you can see, CoffeeScript lets us eschew parenthesis and curly braces. Instead, it relies on indentation to tell where things are supposed to go. Let’s go back and look at the example situation the scaffold starts out with:

situation 'start',
content: """
![a storyteller](img/storyteller.jpg)
# Welcome to Raconteur
If you're seeing this, you've successfully installed the Raconteur game
scaffold. Get writing!
Raconteur lives at a [Github Repository], where you can report issues or
send feedback.
[Github Repository]: https://github.com/sequitur/raconteur
"""

We’re passing situation() two arguments: start” and an object literal with one property, content. From that, the function will make an actual Situation object and pass that on to Undum, adding it to the list of game situations.

A situation, in Undum, is a basic story building block. They’re equivalent to Twine’s passages or ChoiceScript’s scenes. While all of the text from previous situations remains on the page they are functionally dead”; only the current” situation is considered by Undum. A situation isn’t just a description of some content to print; it also holds game logic. For example, whenever a special link (an action link”) is clicked, Undum asks the current Situation object how to proceed.

When a situation becomes the active situation (is entered”), Undum calls its enter() method. Raconteur supplies (a very advanced version of) that method for you, so you don’t have to define it by yourself. That method gets passed three arguments: character and system (who are objects holding Undum’s state and properties, and we will get to know them better later) and the name of the previous situation, which for the first situation entered is just going to be null instead.

This situation, called start” has special status: Undum looks for such a situation to begin the game. It is the only situation that has that kind of special status. An ending” is really just a situation that goes nowhere, for example, and there is no demand that the start situation, or any situation for that matter, be visited only once.

The only property of our situation is content. content, as the name implies, is the main content of the situation: What is printed when the situation is entered. We can change the content and watch as the game itself changes:

situation 'start',
content: """
# At the Mouth of the Cave
You stand at the mouth of a dark limestone cave. It is dawn, and you
hear rushing water.
"""

If you have gulp serve running, saving the file should cause it to rebuild and reload your browser. Start your game again and note what happens.

content, like most (but not all) strings in Raconteur, is formatted using [Markdown]. So the line starting with a # becomes a header, a <h1> element; and the other two lines, separated by a blank line, become a paragraph.

We can add more situations:

situation 'start',
content: """
# At the Mouth of the Cave
You stand at the [mouth](enter_the_cave) of a dark limestone cave. It
is dawn, and you hear rushing water.
"""
situation 'enter_the_cave',
content: """
You lift up your brass lantern and walk into the dark cave. You can't
see very far; the ruddy light shimmers off the slick, wet limestone
walls. Inside, the cave forks into two distinct passages,
[left](left_passage) and [right](right_passage).
"""
situation 'left_passage',
content: """
You walk down the left passage. It gets narrower as you go, until you
have to squeeze yourself sideways in the narrow gap between the walls.
"""
situation 'right_pasage',
content: """
You walk down the left passage. It eventually widens into a grand
chamber, filled mostly by a majestic reflecting pool that shimmers
with a riot of colour.
"""

A situation name should be a valid identifier - that is, it should consist only of the characters a-zA-Z0-9_. One nice thing about Undum is that if you use the name of a situation as the target of a link, it will become a situation link, connecting to the next situation. [example](http://example.com) is Markdown syntax for a hyperlink; it translates to <a href="http://example.com">example</a> in html. So you can save your main.coffee file with that set of situations, and it should produce a rather short story.

Undum automatically disables all links when a situation is exited, so you don’t have to worry about players being able to click old links and backtrack. This means that the second situation, enter_the_cave”, is a branching point; the player has to choose to go left or right, and they will only be able to pick one, since the other link will be disabled as soon as they click on one.

If you’ve gotten this far: Congratulations, you now know just enough to write a Raconteur story. With just simple situations and basic links, you can write a Choose Your Own Adventure-style branching story. In the next part of this tutorial, I’ll talk about more complex choice and adding logic to a Raconteur story, so that you can have more complex mechanics than a pure CYOA-style game.