Writing IF with Raconteur, part 2: choices, choices
Last we left off, I had explained setting up Raconteur and writing a very basic Situation: Enough to make simple stories. This tutorial is going to delve a little deeper into using Undum and Raconteur to write more complex stories.
Background information on Raconteur.
The choices property
The situation
factory that Raconteur uses will actually create a situation object that has all of the properties you pass on to it in your spec, but a good number of them are special. Today we’re going to look at four of them: before
, choices
, optionText
, and canView
. Let’s start with choices.
Last time, I showed how you could use explicit links to connect one situation to another. But there’s another way:
situation 'start',
content: """
You stand at the mouth of a damp, dark limestone cave. Two passages,
left and right, stretch out into the darkness.
"""
choices: ['go_left', 'go_right']
situation 'go_left',
content: """
The tunnel narrows dramatically as you enter it, forcing you to
squeeze in sideways between the slick limestone walls.
"""
optionText: 'Go left'
situation 'go_right',
content: """
The tunnel opens up into a wide chamber, dominated by a bright
reflecting pool.
"""
optionText: 'Go right'
Use this as the situations in a Raconteur main.coffee
story and run it, and you’ll see what happens:
What happened here? Raconteur and Undum have taken the list of situations you supplied in the choices
property of start
and used it to build a list of options that follows your situation’s content. So far, this is only a different presentation from just writing direct links. To find out what more we can do with this approach, we have to look into canView
and Undum’s implicit choice feature.
canView
Let’s make a simple edit to one of our situations:
situation 'go_left',
content: """
The tunnel narrows dramatically as you enter it, forcing you to
squeeze in sideways between the slick limestone walls.
"""
optionText: 'Go left'
canView: () -> false
() ->
is CoffeeScript syntax for a function definition; canView is our first function. For those of you who come directly from JavaScript or other programming languages, here’s a quick refresher on CS functions:
hello_world = () -> # The parens are mandatory even if there's no arguments
console.log "Hello, world!" # Function calls in CoffeeScript only need
# parens if they have 0 arguments.
hello_world() # -> Prints "Hello, world!" to the console.
echo = (argument) -> # The argument list goes inside the parens
# Like in Python, indentation defines blocks in CoffeeScript
argument # CS is expression-based, so the last statement of a function
# is also its return value.
echo "Hello!" # -> returns "Hello!"
CoffeeScript’s cleaner function syntax is one of its selling points for writing IF stories. Add the line:
canView: () -> false
To the ‘go_left’ situation, and watch what happens when you run the game. Your list of options should only show the other option – “Go right”.
Whenever Undum is building a list of choices from a situation’s choices
array, it consults each situation listed to see how it should appear. It calls their optionText
property to get what text it should display. And it calls their canView
method, to find if it should display that option at all.
canView
should return true
or false
. If it returns true
(Or any “truthy” value), then the situation is displayed as an option.
canView
, like most situation methods, is passed three arguments: The Character
object, the System
object, and the name of the current situation. In our previous example, we didn’t bother to define arguments for our canView
method because it didn’t use any – it just returns false
, always.
Character and System
Character
and System
are two objects that are never created or accessed directly in your code; instead, Undum passes it as an argument to various methods you define. System
holds most of the methods we use to interact with Undum’s low-level API, and will be the subject of another tutorial.
Character
holds most of your game state – qualities (the subject of another tutorial), and the “sandbox”. The sandbox is a general-purpose area for storing game state; initially, it’s just an empty object. We can safely modify the sandbox however we like:
situation 'start',
before: (character) ->
character.sandbox.hasLamp = false
content: """
You are standing at the mouth of a deep, dark cavern. There is a
brass lamp here.
"""
choices: ['brass_lamp', 'enter_cave']
before
is another property of Raconteur situations; in this case, it’s a function that gets called before content
is output. It’s a good place to put any initialisation code or side-effects of entering a situation. Normally, we would set the initial values of sandbox
properties inside our undum.game.init
function – but for the sake of this example, it’s fine to put it in before
. The function gets passed our usual three arguments – Character
, System
, and the name of the previous situation; but since we are only using the first one, we only need to define the first one – JavaScript doesn’t check that a function is called with the right number of arguments, so instead it just discards the other two.
Here are the other situations for this second example:
situation 'brass_lamp',
before: (character) ->
character.sandbox.hasLamp = true
content: """
You pick up the brass lamp and light it.
"""
choices: ['enter_cave']
optionText: 'Pick up the lamp'
situation 'enter_cave',
content: """
You walk into the dark cave. There are two passages, to the right and
left; the right one glows with a strange shimmer.
"""
optionText: 'Enter the cave'
choices: ['go_left', 'go_right']
situation 'go_left',
content: """
The tunnel narrows dramatically as you enter it, forcing you to
squeeze in sideways between the slick limestone walls.
"""
optionText: 'Go left'
canView: (character) -> character.sandbox.hasLamp
situation 'go_right',
content: """
The tunnel opens up into a wide chamber, dominated by a bright
reflecting pool.
"""
optionText: 'Go right'
If you run the complete example, you’ll note that you can only get to the left passage if you previously picked up the lamp. “go_left” has a canView
method that returns the value of character.sandbox.hasLamp
, which is normally false
, but gets set to true
if you visit the “pick up the lamp” passage. This simple mechanic – referencing whether a previous story event happened to shut off or open up passages of the story – is the basis of what Dan Fabulich calls delayed branching.
If you’ve gotten this far, congratulations: You can now use Raconteur and Undum to write games with complex branching stories similar to ChoiceScript stories, where events earlier in the narrative can impact events later even if the branches in between were “merged”.
In fact, you can use sandbox
to track more complex things than true/false conditions such as whether the lamp was picked up; you can store any value in a sandbox property. Consider the example of a dating sim: You can use the sandbox to track the feelings of the various love interests towards the player character, to decide which interactions will be available later on. You’ve officially gone beyond the point of what a pure CYOA book can do.
Before we wrap up, a few notes:
- Situations can also provide a
canChoose
method; if that method returnsfalse
, then the option will be visible on an option list, but greyed out, and the player won’t be able to click on it. - Situations can have an
after
method. As the name implies, that method is just likebefore
, but it gets called after content is printed. optionText
is an exception to a number of things in Raconteur: It’s not Markdown, and it can’t be a function. In fact, it’s not even html; theoptionText
on your situations has to be just plain text. This is an underlying limitation of Undum.- Sam Kabo Ashwell had a very helpful post earlier this year about hypertext IF structure, and it’s worth reading. Particularly the part about branch-and-bottleneck structures: You’re now equipped to write games with that kind of structure.
- Join me in a few days when I’ll write about adaptive text and varying what is written to the player.