Writing interactive fiction in Python (#1)

Newsgroups: comp.lang.python
From: Neil Cerutti
Date: Mon, 13 Aug 2007 13:37:26 GMT
Local: Mon, Aug 13 2007 5:37 am
Subject: Re: Adventure-Engines in Python

[….]

As far as I know, no full game was ever written in PAWS. Once you try it, you’ll possibly see why no interactive fiction system distributed as a library for a general-purpose language has ever caught on. Every system that’s enjoyed even moderate success has been a language+library implementation.


Neil Cerutti

This is my jumping off point for an exploration of writing IF with Python.

Every so often on RAIF someone will make a comment about why people “don’t just use the power of a general programming language” instead of bothering with boutique languages like Inform and Tads. I think it’s clear that IF languages solve a problem probably unique to writing iF — how to weave prose and code — not to mention that their years of development have resulted in very strong libraries that make writing IF much easier.

Nevertheless, as someone who enjoys IF, and programming with Python, I want to look at this more closely. More after the cut.

Rather than go on with some hand waving, I think the best place to start is by writing some IF.

Thanks to a comment posted here a little while ago I found the PyF library by Tuomas Kanerva, the newest and most actively developed Python IF lib.

PyF is a cross-platform interactive fiction development library for Python. It aims to make writing interactive fiction easy and efficient without a need to learn a specialized IF language.

PyF uses XML to model the game world but allows the actual game logic to be written in pure Python. This separation of logic and actual game content aims to improve the workflow of writers and programmers alike.

And the goals of PyF:

Why choose PyF?

* Because PyF allows separating the world model from the game logic, the game source code is easy to read.

* PyF parser is very flexible. It can parse and match sentences of virtually endless complexity. Wildcard matches allow you to customize default responses very easily.

* PyF is written in an interpreted language. That means that PyF is available on every platform that Python is ported to. So you can write a game and have it run on Windows, OS X and Linux without changing anything.

* Unlike some other IF engines, PyF doesn’t require you to learn a specialized language that you can only use to write IF.

* In PyF you can easily handle any input not supported by the standard library. Adding new synonyms for your game is trivial.

* PyF is designed to be completely modular. You can customize your game engine as much as you like, from changing the standard responses to completely rewriting the input handling process.

Many of these points are shared with IF languages, such as platform compatibility, a flexible parser, and lib customization.

Separating the world model from the game logic is perhaps unique to PyF, though you could argue that you can do the same thing in Inform just by how you structure your source, because unlike Python, in many IF languages you generally don’t have to worry about the order of source code definitions. This freedom is a major selling point of writing IF with IF languages (though that’s not to say that good organization in source code is a bad thing).

Following the Cloak of Darkness example I divided my game into three parts, a code source, a world model script, and a script to run the game (which creates a .pyf file containing the source and the script).

I have to admit that I have a gut aversion to writing XML; the PyF developer told me that using a different encoding like YAML is a possbility, though, not to mention that by using a markup language for the script you ease the way for an IDE application in which to create the world structure. Anyway writing the XML for a small game wasn’t too bad. Here’s that file:

<?xml version="1.0" encoding="UTF-8" ?>
<!-- getjoke.script.xml -->

<Game xmlns:props="pyf.props">
    <attr>
            <name>Get Joke</name>
            <author>George Oliver</author>
            <version>1.0</version>
            <description>IF walks into a bar....</description>
                    
    </attr>

        
    <Bar name="The Bar at the End of Time">

        <exits>
        </exits>

        <ldesc>Have you ever wondered about the bar in all those bad jokes? Wonder no more.... The Joker is here.</ldesc>
        
        <props:Room />
            
        <Player>
                
            <ldesc>As handsome as ever.</ldesc>
                
            <Ring name="ring" adjective="">
                    <ldesc>The ring they gave you when you graduated high school.</ldesc>
                    
                    <props:Mobile />
                                                    
                    <props:Wearable >

                            <attr>
                                    <worn type="bool">True</worn>
                            </attr>
                    </props:Wearable>
                    
            </Ring>
        </Player>
        
        <Umbrella name="umbrella">
            <ldesc>A black umbrella.</ldesc>
            <props:Mobile />
        </Umbrella>
        
        <Joker name="The Joker, joker">
            <ldesc>He painted a smile on his face.</ldesc>
            <props:NPC>
                <attr>
                    <answers type="dict">{"j": "[self.get_joke()]"}
                    </answers>
                </attr>
            </props:NPC>   
        </Joker>
        
    </Bar>
        
</Game>

In the source code file you define the classes (that are similarly declared as nodes in the world script markup), and the source ends up looking like this:

'''Python source. Used to define the actual game logic.'''
import random

from pyf import items, game, states, props, actor, script

from pyf.errors import *

import twython

""" Instantiate Tango with no Authentication """

    
class Game(game.Game): 
    def ending(self, output):
            output.write("You have won!")
                
class Player(actor.Actor):
    pass

class Bar(items.Room):
    pass
        
class Umbrella(items.Item):
    pass
    
class Ring(items.Item):
    def init(self):
        '''Listen to move events.'''
        self.addEventListener(self.EVT_MOVED, (self, ringMoved))
def ringMoved(self, event):
    pass
    
class Joker(items.Item):
    def __init__(self):
        twitter = twython.setup()
        search_results = twitter.searchTwitter('#joke', rpp='50')
        self.jokes = []
        for tweet in search_results['results']:
            self.jokes.append(tweet['text'])
        super(Joker, self).__init__()
            
    def get_joke(self):
        return random.choice(self.jokes)
            
        
    
class MyTalking(states.State):
    request = "?"
    def __init__(self, actor, npc):
        states.State.__init__(self, actor)
        self.npc = npc
        
    def handle(self, sentence, output):
        if sentence == self.npc.ENDING:
            self.restoreState()
            output.write(self.npc.responses[self.npc.CONVERSATION_ENDED], close=False)
        else:
            try:
                self.oldState.handle(sentence, output)
                if str(output) == game.actor.responses['unhandled']:
                    output.closed = False
                    output.lines = []
                    self.npc.converseAbout(sentence, output)
            except OutputClosed:
                pass



states.Talking = MyTalking

'''Create the game world based on our script.'''
game = game.createFromScript(open('getjoke.script.xml'), locals())


'''Set Player object as the actor.'''
game.actor = Player.inst


if __name__ == '__main__':
        '''Finally we need an interface so that you can actually play the game. '''
        import pyf.interface
        pyf.interface.runGame(game)

You can run this in the shell, or use the simple wx app that Tuomas put together. Here’s what that looks like:

Voila, a twittering jokester in IF, surely this day will go down in history (and I mean way, way down).

The experience of writing the game was very ‘codey’ for lack of a better word, and it made me appreciate the benefits of something like Inform 7 even more.

However I should note that you don’t need to separate the game into source and script, and if you were to integrate a pre-parser or alternate markup into PyF the experience would be much different. In fact a literate programming style pre-parser could be a cool addition to the lib. Furthermore Python is eminently readable as code, and though I don’t understand everything the lib is doing I could still understand and modify the lib as I needed to.

Though this is a pretty dumb demo, it does demonstrate the extensibility and flexibility of writing with Python. I think you’d have to go through some contortions in Inform or Tads to do the same thing I did with Twython in a few lines of code (and more importantly, with just a few minutes of comprehension).

I really like this idea of opening up the possibilities with the huge variety of things that people already have done in the language.

Here’s the script you need to run the game above. You should name your source code file __init__.py, the script getjoke.script.xml, and put everything in one directory. The PyF module goes in the same directory as these three files.

from pyf.interface import package, gui

package.Packer.pack('.', 'getjoke.pyf')

import zipfile
f = zipfile.ZipFile('getjoke.pyf')

gui.runGame(f)

3 comments so far

  1. toeholds on

    The Twittering Joker is very cool. This makes me think about IF that makes use of general current events or pulling in (random but themed) content from the web. Thanks for sharing.

  2. kerray on

    Randomly googled this when looking for ways to write IF with Python, thanks for the info!

  3. […] Writing IF in python – a fun look at just using a general-purpose language for IF […]


Leave a comment