Interactive Fiction Programming

Or, some thoughts on how to implement Interactive Fiction in modern languages.

The first thing to note is: it's difficult. Really difficult. But the worst part is, it starts out being easy...

Getting Started

"So, we need some kind of player, and some kind of world. The player's going to be holding things, and the world will have rooms, and the rooms will have things, and the player might just want to pick up those things - let's make it Object Oriented!"

This was my first thought. It's all too easy to envision something like this (Python-ish pseudocode):

# input: "Pick up sword"
item = location.items["sword"]  

This works really well if all you're doing is wandering from room to room, picking things up. Everything's just a dictionary (or, as I've seen suggested, a collections.Counter).

The First Real Problem

This is a strong model. It's the model that languages like Inform 6 were built on, and it's no accident: object-oriented design allows programmers easily to realise real-world objects into their worlds, give them descriptions as attributes and so on.

However, it all falls down when it comes to interaction. What happens, say, when you try to put key in box? Well, that's easy enough: we remove key from location.items and append it to box.items. But what if we try put chair in box? Since the "remove" and "append" stages are completely separate using this model, we can't tell that the box is too small for the chair.

A Possible Solution?

Clearly, then, there should be some kind of method called on each of the items that interact. Maybe there should be both chair.max_size and box.size attributes, and a chair.insert method which takes the box as input?

Unfortunately, this doesn't hold up when the object needs extending. What happens, say, when an electrified lever needs to electrocute the player whenever the player contacts it? Do we have to add "touched", "grabbed", "pulled" etc. methods to the lever?

Adding all these extra methods and checking for them must necessarily be overly verbose and tiring for the programmer, and involve a huge amount of code repetition. What, then, if all the methods like "grab" and "pull" could inherit from a base method like "touch"? The methods can override the specific behaviour if they so wish.

But wait, there's no notion in object-oriented programming of methods inheriting from each other. Perhaps there'd have to be some new class of object known as ContactTriggerLever, which inherits from BaseLever and adds a callback to a dictionary of possible ? But this seems clumsy, and at a glance does not seem to lend itself easily to being changed (for example, if the electricity is being turned off, every callback would need to be removed - not to mention the ContactTriggerLever reverting to a BaseLever or ActionOnPullLever).

These methods seem clumsy; I get the feeling IF programming is just one special-case after another. No matter how much I implement in the way of base classes, there'll always be more situations I can't anticipate.

Having an"action", world_state, player_state) method for every class seems like the best way to go, but it just brings up what we were trying to avoid in the first place: a huge-ass switch-case statement which checks for all possible action types and executes the appropriate. In Python, that'd probably take the form of a million if-else statements, or an ugly (and huge) dictionary mapping action/state tuples to callback lambdas. But how is this any different from just doing away from object-oriented design at all?

A More Realistic Solution

It seems, then, that object-oriented design fails in a number of different ways when it comes to the basic implementation of objects with real-world interactions and attributes. How, then, does an Event-Driven model fare?

Every object, in this model, would be a type of Reactor: an object which is passed data and responds to it using a set of callbacks. This has a distinct advantage over the previous idea of using a callback dictionary: there is already a namespace created and ready to use, so there should be no issues with using constructs like self.items without it being necessary to pass the world-state in as arguments.

However, this again falls down when it comes to interactions with the player. Should we pass in a reference to the player along with every callback, regardless of whether it's needed or not?

Some Kind of Hack?

Maybe the best option is to present the eventual game programmer with a nice API to use, which is (comparatively) ugly behind the scenes. So, there could be a change_behviour(CALLBACK_TYPE, callable, overwrite=False) function made available, which does all of the messy instance-patching where necessary (or adds to a dict of callbacks and responses, whichever's easier to implement). It'd be relatively trivial to enumerate all possible interactions that any object could have, and (after classifying the user's input) pass the relevant code into the object's handle_interaction method (along with any possibly-relevant states: player, world etc.). This method would check its dict of {interaction type:callback} and run any present. This dict should be freely accessible via the change_behaviour function: a small wrapper to alter the callbacks dict.

Some example code:

from collections import namedtuple  
Callback = namedtuple("Callback", "func args kwargs")

class Object:  
    def __init__(self):
        self._callbacks = {}
    def handle_interaction(i_type, player, location):
        if i_type in self._callbacks:
            for cb in self._callbacks[i_type]:
                cb.func(*cb.args, player=player,
                        location=location, **cb.kwargs)

    def change_behaviour(self, i_type, callback, args=(), kwargs={}):
        callback = types.MethodType(callback, self)
        cb = Callback(callback, args, kwargs)
        if i_type in self._callbacks:
            self._callbacks[i_type] = [cb]

# Now we can write our own function to handle arbitrary interactions
# - remember to add the 'self' keyword.

# Make a new object to test it out with
sword = Object()

def sword_picked_up(self):  
    output("You pick the sword up!")

sword.change_behaviour(PICKED_UP, sword_picked_up)  

The Main Issue

Unfortunately, the issues discussed so far have all been relatively minor as compared to this single, overshadowing threat:

This system will not be easy to use.

This seems, at this point, to be inevitable. Instead of a beautiful, near-English syntax like Inform (7, not 6), we have only succeeded in creating a twisting maze of function calls and API quirks. There is far too little room for extending the functionality of the system, and what little room there is has been implemented with no end of difficulties.

The question is: is it worth it?