pawsy

One thing I’ve been going back and forth about is the virtue of trying to write IF with Python. On one hand Python is a great language for writing games; on the other hand I much prefer the Inform 7 syntax for a simple reason — in IF you write a lot of prose, and anything that makes that process both simpler and nicer to look at is a huge win. However that very same syntax becomes something of a stumbling block for me (well, to be honest — a tortuous maze) when you start doing anything complex. In this respect I like Python’s syntax much more.

Python already has a well developed IF system in PAWS. However as you can see in the Cloak of Darkness implementation the source itself reads much more like a TADS 3 file than Inform 7. I’m not saying that doesn’t make sense, only it’s not how I’d like to write ideally.

This got me to wonder — how would I like to write IF ideally? So here is a hacked up version of the Cloak of Darkness source above. Let’s see how the WP sourcecode markup works with this — after the cut:


"Cloak Of Darkness Sample Game" by Roger Firth (implemented by Neil Cerutti, adapted by me)

Copyright 1999-2008
Version 1.5

from PAWS import *      # Game Engine (already written for you)
from Universe import *  # Game World Library (already written for you)

.. the game begins here

Introduction

Hurrying through the rainswept November night, you're glad to see the bright lights of the Opera House. It's surprising that there aren't more people about, but, hey, what do you expect from a cheap demo game?



.. Scene 

Foyer "Foyer of the Opera House" Start

    North = "You've only just arrived, and besides, the weather outside seems to be getting worse."
    South = Bar
    West = Cloakroom

You are standing in a spacious hall, splendidly decorated in red and gold, with glittering chandeliers overhead. The entrance from the street is to the north, and there are doorways south and west.


Cloakroom

    East = Foyer

The walls of this small room were clearly once lined with hooks, though now only one remains. The exit is a door to the east.


Bar "Foyer Bar" dark

    North = Foyer
    
    def going(self, direction):
        if direction == 'North':
            return
        elif Bar.IsLit:
            return "There's no exit in that direction."
        else:
            Sawdust.trampled += 1
            return "Blundering about in the dark isn't a good idea."
            
The bar, much rougher than you expected after the opulence of the northern foyer, is completely empty. There seems to be some sort of message scrawled in the sawdust on the floor. 


Sawdust message/floor +Bar

    def read(self):
        Global.GameOver = True
        if self.Trampled < 2:
            return "The message, neatly marked in the sawdust, reads..."
        else:
             return "The message has been carelessly trampled, making it difficult to read. You can just distinguish the words..."
    


Cloak velvet/black/satin/dark/handsome +Player

    Take = "{Bar.SetIsLit(False)}Taken."
    
    def drop(self, Multiple=False):
        return "This isn't the best place to leave a smart cloak lying around."
        
    Bulk = 1
    Weight = 10
    
A handsome cloak, of velvet trimmed with satin, and slightly spattered with raindrops. Its blackness is so deep that it almost seems to suck light from the room. 


Hook small/brass hook/peg +Cloakroom

    Take = "The hook is screwed to the wall."
    
    def enter(self, Object):
        if Object == Cloak: Bar.SetIsLit(True)
        return inherited
        
    MaxBulk = 1
    MaxWeight = 10

It's just a small brass hook {"with a cloak hanging on it" if Cloak in Contents else "screwed to the wall"}.



Conclusion

*** {"\n You have won \n" if Sawdust.Trampled < 2 else "\n You have lost \n"}. ***
                   

Now for some explanation. The goal here is to make the prose of the IF as easy to write as possible, while keeping the syntax of the Python code when you need it. To that end a parser will run on the source text to produce the game file, which the Python interpreter will then execute normally.

I think this intermediary step makes a lot of sense — if I want to make the source text as easy to write as possible over and over, why not offload a lot of the work to the IF system, instead of putting it on the IF author? The key would be not to obfuscate the Python code or write a ‘custom Python’ — I want this as close to Python as possible.

First, take everything before the Introduction. This looks a little like I7. The first line is the game title and the author. Following that are a series of keywords followed by their values. A blank line separates this header from the imports you wish to make (I’m assuming I’m building this on PAWS here; you could also import other modules you would want to use here). Another blank line, then a sample comment not to be included in the game file, then the Introduction keyword.

This text is what’s printed before anything else, and is optional. After this we have object definitions, so to look at one more closely:

Foyer "Foyer of the Opera House" Start

    North = "You've only just arrived, and besides, the weather outside seems to be getting worse."
    South = Bar
    West = Cloakroom

You are standing in a spacious hall, splendidly decorated in red and gold, with glittering chandeliers overhead. The entrance from the street is to the north, and there are doorways south and west.

I’ve taken some inspiration from PyLit in how the formatting works.

One thing I like about TADS 3 is the conciseness of its object definitions (which take advantage of a template feature I believe); I’m using a similar idea here. The first line includes the code name and then printed name of the object, in this case “Foyer of the Opera House”. The Start keyword means this is the start room.

Notice at no point have I actually said this is a room at all, and I’m not sure I need to. All objects could implement the features of a room, though not all objects might be enterable. I guess this could have some problems with game file size, but I’m not sure this is too important right now.

Also you could have an object with no printed name — this just creates a generic object where its source name is its printed name (like Cloakroom).

On the definition line you can declare starting location with a ‘+’ prefix as in Tads 3.

Following the definition line is an indented block, which means code. These are attributes on the object, and the parser would recognize these as exits with destination objects.

Now, none of these other objects are declared yet, and in Python this could be a problem (if this code was in an initialization block for example). But if the system parses this file first it could (somehow, I’m hand waving at this point) fill in the relationships on a ‘as I go’ basis. I think this improves the readability of the code a great deal (and is the standard in IF-specific domain languages like Inform and TADS 3).

After the indented block is another prose block, which is the description of the object. I realize there are a few things I’m missing here, like first-time descriptions for items.

These object definitions lay out the world model. Room and item definitions can be interleaved (see how Sawdust follows Bar). To look at a function definition within an object:


Bar "Foyer Bar" dark

    North = Foyer
    
    def going(self, direction):
        if direction == 'North':
            return
        elif Bar.IsLit:
            return "There's no exit in that direction."
        else:
            Sawdust.trampled += 1
            return "Blundering about in the dark isn't a good idea."
            
The bar, much rougher than you expected after the opulence of the northern foyer, is completely empty. There seems to be some sort of message scrawled in the sawdust on the floor. 

I wanted that to look as much like Python code as possible — the only difference is it’s not contained in a class definition. A big part of this idea is many of these function names and keywords are reserved by the system (here I override the going function) but this is a big part of any IF system anyway.

That return inherited thing is a little bit of a hack in the enter method for the hook — I needed some way to continue the action. The parser would pick this up somehow.

Text substitution is always a big deal in IF, here’s an example:

It's just a small brass hook {"with a cloak hanging on it" if Cloak in Contents else "screwed to the wall"}.

This shows that prose blocks can include code — this is a Python equivalent of a condition/true result/false result ternary operator. The curly brackets cause the parser to execute this code and return the result to the prose block.

I think a key component of this parser would be its ability to create objects, either whole objects or instance attributes, on the fly if they didn’t exist already.

One open question that intrigues me is how to include additional functionality for objects. In this example I’m just kind of assuming all objects, when parsed, inherit from some monolithic game object class that includes all the functionality I want. However this probably isn’t a great way to go about things — how would the author, in this file to be parsed, include this functionality? I guess that’s where the rubber meets the road in a system like this.

One possibility would be to create objects with the required functionality (maybe they override system methods or whatever) and then include these objects in new object definitions. Something like:


A Big Foo :: Foo

    def bar(self):
         return "Now we're barring"

Biff :: "Foo"

Finally is the Conclusion keyword and another example of the ternary operator. The conclusion would trigger when the game state was marked as over, which happens when the player reads the message in the sawdust.

What I like about this format is the simplicity of writing the prose combined with the Python syntax — granted, it may not be easy at all to parse this into actual Python code.

This does look like an unholy union of Python, TADS 3, and Inform 7, doesn’t it. I don’t know if that’s scary or awesome to be honest.

Another thing pushing me to Python would then be the possibility of writing the interpreter in pyglet, with all the benefits pyglet would provide. I’m thinking mainly of a really cool text interface, not really all the other graphics capabilities you’d get, though that’s certainly there, and of course you get this on all platforms. One drawback would be in how to present games on the web — but zcode games solve this with separate interpreters, so this would be the road you’d have to take anyway.

edit: I couldn’t resist messing around with the example source, and the changes are significant enough to post it here:

"Cloak Of Darkness Sample Game" by Roger Firth (implemented by Neil Cerutti, adapted by me)

Copyright 1999-2008
Version 1.5

from PAWS import *      # Game Engine (already written for you)
from Universe import *  # Game World Library (already written for you)



.. the game begins here

Introduction

Hurrying through the rainswept November night, you're glad to see the bright lights of the Opera House. It's surprising that there aren't more people about, but, hey, what do you expect from a cheap demo game?



.. Scene 

Player "Regina" in Foyer



Foyer "Foyer of the Opera House" 

You are standing in a spacious hall, splendidly decorated in red and gold, with glittering chandeliers overhead. The entrance from the street is to the north, and there are doorways south and west.

    North = "You've only just arrived, and besides, the weather outside seems to be getting worse."
    South = Bar
    West = Cloakroom



Cloakroom

The walls of this small room were clearly once lined with hooks, though now only one remains. The exit is a door to the east.

    East = Foyer



Bar "Foyer Bar" 

The bar, much rougher than you expected after the opulence of the northern foyer, is completely empty. There seems to be some sort of message scrawled in the sawdust on the floor. 

    IsLit = False
    North = Foyer
    
    def going(self, direction):
        if not direction == 'North' and Bar.IsLit:
            return "There's no exit in that direction."
        else:
            Sawdust.trampled += 1
            return "Blundering about in the dark isn't a good idea."
            


Sawdust message/floor in Bar

    def read(self):
        GameOver = True
        if self.Trampled < 2:
            return "The message, neatly marked in the sawdust, reads..."
        else:
             return "The message has been carelessly trampled, making it difficult to read. You can just distinguish the words..."
    


Cloak DontDrop velvet/black/satin/dark/handsome in Player

A handsome cloak, of velvet trimmed with satin, and slightly spattered with raindrops. Its blackness is so deep that it almost seems to suck light from the room. 

    Take = "{Bar.SetIsLit(False)}Taken."        
    Bulk = 1
    Weight = 10
    


DontDrop 

    def drop(self, Multiple=False):
        return "This isn't the best place to leave a smart {noun} lying around."



Hook small/brass hook/peg in Cloakroom

It's just a small brass hook {"with a cloak hanging on it" if Cloak in Contents else "screwed to the wall"}.

    Take = "The hook is screwed to the wall."
    
    def enter(self, Object):
        if Object == Cloak: 
            Bar.SetIsLit(True)
        
    MaxBulk = 1
    MaxWeight = 10



Conclusion

*** {"\n You have won \n" if Sawdust.Trampled < 2 else "\n You have lost \n"}. ***

I realized it makes more sense to put the object description before the indented code block (then the next object definition comes after the indented block).

Instead of using a ‘+’ to denote containment I’ve opted for a more readable ‘in’ keyword. Also there is now an example of adding functionality to the core library object:

Cloak DontDrop velvet/black/satin/dark/handsome in Player

A handsome cloak, of velvet trimmed with satin, and slightly spattered with raindrops. Its blackness is so deep that it almost seems to suck light from the room. 

    Take = "{Bar.SetIsLit(False)}Taken."        
    Bulk = 1
    Weight = 10
    


DontDrop 

    def drop(self, Multiple=False):
        return "This isn't the best place to leave a smart {noun} lying around."

This works a lot like a TADS 3 mix-in (and the PAWS class system). The Cloak object includes whatever code you put into DontDrop; the idea now is that the first keyword in the definition is the object, and after that come objects mixed in, then synonyms.

I got rid of the return inherited idea; now I’m thinking that if the overridden function doesn’t return something explicitly, the action continues.

On a minor note Global.GameOver is now TheEnd.

Also there’s a player definition now, and a removal of the idea of a ‘start’ keyword.

Advertisements

7 comments so far

  1. Pacian on

    Sounds like this would be quite an undertaking… Good luck if you choose to go ahead with it! šŸ™‚

    “One thing I like about TADS 3 is the conciseness of its object definitions (which take advantage of a template feature I believe);”

    This is the number one reason I found I7 difficult to work with. Defining scenery objects sometimes seems to involve typing more words that the player can’t see than those they can.

  2. Conrad on

    Well, I can see you’re enthusiastic about it, and I think that’s great. But it’s not clear to me what real advantage this has over T3 or I7 (or I6, for that matter), which are, most of them, mature, stable, well-debugged IF development systems.

    What can I do in this system that I can’t do in one of those?

    Conrad.

  3. georgek on

    @Pacian

    Yeah, I wonder if I7 could work with a similar template system somehow?

    @Conrad

    To be honest I’m not so much asking myself that question with this thought experiment, rather it’s, “how can I do in this system what those other systems do, but in a way I enjoy writing it?”.

    As a hobbyist programmer the syntax of programming languages has a great impact on much I enjoy doing stuff with them. With both T3 and I7 I enjoy them up to a point, right where they get complex. Though as I’ve gone about mocking up the source for this thought experiment, I’ve noticed that it starts looking like the same sort of syntactical maze as you combine code with prose. Maybe this is just fundamental to IF code in general; it kind of makes sense, right?

    I should add that I do think that IF is programming; but the goal here is a domain specific language that does the best job of combining prose and code that I can think of.

  4. Conrad on

    Aah, I see. Rock on.

    C.

  5. Tuomas Kanerva on

    While this is an idea I certainly wouldn’t mind seeing implemented, and an interesting extension to PAWS’ default interface, I’d imagine the source gets difficult to maintain as the complexity of the game increases. But I’ve never written anything in TADS or Inform – TADS’ learning curve seems unreasonably steep and any natural language -like syntax makes me want to run to the hills screaming (I’m looking at you AppleScript) – so I wouldn’t really know how big the large projects get.

    I’ve been working on an IF engine in Python myself, and have come to the conclusion that what I really want to do is separate the prose from the game logic completely. Right now I’m defining the world model through a separate XML file that does little more than map some tags into classes defined in the source code. The resulting XML-files are very readable even to the non-programmer and the source gets a lot clearer.

  6. georgek on

    Tuomas, what do you do with prose that is not part of the world model per se, such as failure and success messages of various sorts, and conversation perhaps (though I could see conversation defined in a schema of its own like the world model).

    In any case the large IF projects can get huge, and you’re right on target that as you add complexity the source gets knotty (I tried to mock up a conversation for example, and was faced with the question of how to parse conversation when I don’t want to restrict authors to one implementation of conversation).

    Splitting source files is of course the convention for many things so your approach sounds like a good one (though personally I would avoid XML just for the extra verbiage; maybe YAML instead?). I’m curious if your system is intended to be text only or if it has a graphical component?

  7. Tuomas Kanerva on

    My items are plain to the point that, they don’t even display descriptions by default. All re-usable functionality is added through property classes, which keep their own standard responses in a dict. It’s possible to assign to these dicts at runtime or override their values for single objects in XML, but my preferred method is to subclass the property in question and override its responses.

    NPC conversations are also kept in a dict where the key is the topic and the value is the response. Since you don’t need to reuse these values, you can easily define them through XML. I don’t know how other IF engines support NPC conversations by default, so I’m hoping a model as simplistic as this is okay.

    As for the markup language, I was playing around with something similar to YAML for a while, but switched over to XML – partially because importing modules as XML namespaces is nifty, but mostly because it’s just a better understood language. Everyone knows at least what HTML looks like.

    Right now there’s a command line interface and a basic GUI developed in wxPython. Both of these understand some HTML tags and multimedia support for the GUI is planned. If you want to design a custom interface, that’s simple too: just pass input to the game object and print the output.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: