Example 2: Cloak of Darkness

Turberfield-dialogue is a screenplay system; it’s not intended by itself to be a game framework.

Nonetheless, it strikes me that you might want to use it as a first port of call when prototyping ideas for a large game. Especially if that game is to contain a decent amount of dialogue.

So I decided to explore the degree to which this is possible. And here is my implementation of Cloak of Darkness.

The Hello World of adventure games.

There is a programming tradition that early on you try a trivial example to test that your computer is working properly. You type in some very simple code to get started.

That example is usually just to print “Hello World” to the screen.

Adventure games have their own version of Hello World, which is a little more complicated. Cloak of Darkness is a scenario which exercises several features which all game frameworks really should support. By implementing the game, the framework author demonstrates how those features are achieved.

The game consists of three rooms. In one room is an invisible prize. But the prize is damaged by the player every time he visits the room. It turns out that the player is carrying an item which when dropped in one of the other rooms, allows the prize to be seen.

Interludes

An adventure game is interactive. The action frequently pauses to allow the player to input a command. In classic text adventures, that command is typed in to the console.

Turberfield-dialogue has a feature called interludes. You may have spotted that earlier; it’s the fifth argument to a Folder. In Example 1: Battle Royal we put repeat(None) which is a way of declining to use interludes.

An interlude is a function which gets called at the end of a scene script file. You can define a different function for each if you like. The function sees the folder you’re using. It knows which of the scene files has just finished. It also gets to see all the references you passed in to the performance. An interlude function’s return value is always a folder object. Returning the current folder in play is like saying continue. Or you can return another one and branch the story.

Design

My first instinct was to create a scene file for each of the three rooms. Then all the action for a room goes in to the same file. We will repeat the sequence of three scenes over and over, with an interlude between them to take user input.

Both the player and the cloak can move location. So we will need a state to represent that. Also, the message must change every time it is damaged. That could require another state, but it turns out that a simple Python attribute will suffice.

I want the game to run in Rehearsal. Here’s where the main problem arises. The default behaviour is to run sequentially through scenes, and deliver any dialogue possible. In this case, we only want the action to take place in the location of the player. Otherwise, we will get ghostly voices leaking in from other rooms. So we need to ensure that all other roles remain uncast outside of the current location.

In the previous example we used an integer state variable to mark the fighters as alive or dead. I’ll reuse that concept. By default, every entity is masked out. As the player moves around, certain objects become active, and others go quiet.

Implementation

Logic

We’re going through the code now. If Python is new to you, don’t worry. My intention is just to introduce some essential concepts which you can work to understand later on.

The top of a Python module is where imports go:

import enum
from itertools import repeat
import random

from turberfield.dialogue.model import SceneScript
from turberfield.dialogue.types import DataObject
from turberfield.dialogue.types import Stateful

Next we declare an enumeration state which will define the location of the player and the cloak:

@enum.unique
class Location(enum.Enum):
    foyer = 0
    bar = 1
    cloakroom_floor = 2
    cloakroom_space = 3
    cloakroom_hook = 4

There are no Personas in this game; none of the voices has a name. But they do have state, and one of them needs attributes. The useful types to inherit from will be Stateful and DataObject.

Each of the entities in the game gets its own class declaration:

class Narrator(Stateful):
    pass

class Cloak(Stateful):
    pass

class Prize(Stateful, DataObject):
    pass

So now we can declare an ensemble of entities, setting attributes and initial state where appropriate:

ensemble = [
    Narrator().set_state(Location.foyer),
    Cloak().set_state(Location.foyer).set_state(1),
    Prize(message="You win!")
]

We will be taking user input and trying to interpret commands. Here is the world’s dumbest text parser. It returns the first letter of the last word typed into the console:

def parse_command(cmd):
    try:
        return cmd.strip().split(" ")[-1][0].lower()
    except:
        return None

We want user input at the end of every turn. That’s done in a single interlude function. Should the game grow any larger, it would be better to give each file its own custom function, but this is good enough for an example. I’m just going to throw the code at you and see how you get on:

def interaction(folder, index, ensemble, *args, cmd="", log=None, **kwargs):
    narrator, cloak, prize, *others = ensemble
    locn = narrator.get_state(Location)
    action = None
    if locn == Location.foyer:
        while action not in ("s", "w", "q"):
            action = parse_command(cmd or input("Enter a command: "))
        if action == "s":
            narrator.set_state(Location.bar)
            if cloak.get_state(Location) == locn:
                prize.set_state(0)
            else:
                prize.set_state(1)
        elif action == "w":
            narrator.set_state(Location.cloakroom_space)
            cloak.set_state(1)
        else:
            return None
    elif locn == Location.bar:
        while action != "n":
            action = parse_command(cmd or input("Enter a command: "))

        narrator.set_state(Location.foyer)
        prize.message = prize.message.replace(
            random.choice(prize.message), " ", 1
        )
        prize.set_state(0)
    elif locn == Location.cloakroom_space:
        while action not in ("c", "h", "e"):
            action = parse_command(cmd or input("Enter a command: "))
        if action == "c":
            if cloak.get_state(Location) == Location.cloakroom_space:
                cloak.set_state(Location.cloakroom_floor)
            else:
                cloak.set_state(Location.cloakroom_space)
        elif action == "h":
            cloak.set_state(Location.cloakroom_hook)
        else:
            narrator.set_state(Location.foyer)
            if cloak.get_state(Location) != locn:
                cloak.set_state(0)

    if cloak.get_state(Location) == locn:
        cloak.set_state(narrator.get_state(Location))
        cloak.set_state(1)

    return folder

So now we can declare the objects turberfield-rehearse needs to see; a collection of all our Python references and a folder object with details of the game:

references = ensemble + [Location]

folder = SceneScript.Folder(
    pkg=__name__,
    description="The 'Hello World' of text games.",
    metadata=None,
    paths=["foyer.rst", "bar.rst", "cloakroom.rst"],
    interludes=repeat(interaction)
)

Coding.

Python is a pretty easy language to read, and so far I’ve been relying on that to communicate the essence of how all this works. We have reached a point now that you may need to take time over certain aspects of the code to fully understand what is going on.

I recommend you explore the Python manual. First, get to know its structure; how it separates the fundamentals of the language from details of specific modules which you discover when you realise you need them.

To begin with, check out the random module which is very straightforward. After that, use the module index to find the documentation for Enum.

Dialogue

Here’s where I stop explaining each component of the game. When it comes to understanding the dialogue, it’s best just to study the .rst files in demo/cloak. As a taster, here’s what the dialogue for the first room looks like. It’s probably the simplest of the three.

.. entity:: NARRATOR
   :types: logic.Narrator
   :states: logic.Location.foyer

.. entity:: CLOAK
   :types: logic.Cloak
   :states: logic.Location.foyer

After the fire, a Magician returns
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

From where you stand
--------------------

[NARRATOR]_

    This place no longer looks much like a hotel. This would have been the foyer, though.
    You can see the footprint of a grand reception desk running down one side
    of the floor.

[NARRATOR]_

    The room has been stripped of all it once contained.

Checking your person
--------------------

[CLOAK]_

    You are wearing a long cloak, which gathers around you. It feels furry,
    like velvet, although that's hard to tell by looking. It is so black
    that its folds and textures cannot be perceived.

[CLOAK]_

    It seems to swallow all light.

.. memory:: logic.Location.foyer
   :subject: NARRATOR

   The Player visited the foyer.

Looking around
--------------

[NARRATOR]_

    To the North, the door by which you first entered is stuck fast.

[NARRATOR]_

    There are other doors to the South and West.

Action

You can run the game in a similar manner to the previous example:

cd demo/cloak
~/py3.5/bin/turberfield-rehearse @rehearse.cli

Memory

We saw for the first time above the use of a Memory directive. The game scatters them throughout the action. The result is that a record of the player’s progress builds up in the dialogue database.

The database Turberfield uses for this is SQLite3. You can access this database via Python’s own SQLite3 module. Or you can install a command line tool and issue queries that way. Try this to get a report of the passage of a game session:

sqlite3 cloak.sl3

sqlite> select s.name, state.name, note.text
   ...> from state join touch on state.id = touch.state
   ...> join entity as s on touch.sbjct = s.id
   ...> left outer join entity as o on touch.objct = o.id
   ...> left outer join note on note.touch = touch.id;

Narrator|foyer          |The Player visited the foyer.
Cloak   |bar            |The Player wore the cloak in the bar.
Narrator|foyer          |The Player visited the foyer.
Cloak   |cloakroom_floor|The Player dropped the cloak.
Narrator|foyer          |The Player visited the foyer.
Cloak   |bar            |The Player wore the cloak in the bar.
Narrator|foyer          |The Player visited the foyer.
Cloak   |cloakroom_hook |The Player hung the cloak on a hook.
Narrator|foyer          |The Player visited the foyer.
Prize   |bar            |The Player read the message as " Yo  w n! ".
Narrator|foyer          |The Player visited the foyer.