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.
The choices property
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:
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.
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
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
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
Character and System
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 –
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
canChoosemethod; if that method returns
false, 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
aftermethod. As the name implies, that method is just like
before, but it gets called after content is printed.
optionTextis 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; the
optionTexton 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.