Twisted Reality: A Flexible Framework for Virtual Worlds

  1. Abstract
  2. Motivation
  3. The Twisted Component Model
  4. Parsing System
  5. Actions System
  6. Composing Simulations with Adapters
  7. Future Directions
  8. Conclusions
  9. Acknowledgements
  10. References

Abstract

Flexibly modelling virtual worlds in object-oriented languages has historically been difficult; the issues arising from multiple inheritance and order-of-execution resolution have limited the sophistication of existing object-oriented simulations. Twisted Reality avoids these problems by reifying both actions and relationships, and avoiding inheritance in favor of automated composition through adapters and interfaces.

Motivation

Text-based simulations have a long and venerable history, from games such as Infocom's Zork and Bartle's MUD to modern systems such as Inform, LambdaMOO and Cold. The general trend in the development of these systems has been toward domain-specific languages, which has largely been an improvement. However, a discrepancy remains between systems for single-user and multiple-user simulations: in single-user systems such as Inform, incremental extensibility has been sacrificed to allow for complex interaction with the world; whereas in multiple-user systems, incremental extensibility is paramount, but it is achieved at the cost of a much simpler model of interaction. Twisted Reality aims to bring the sophistication of Inform's action model to multiuser simulation.

The Twisted Component Model

Twisted's component system is almost identical to Zope 3's. The primary element is the interface, a class used as a point of integration and documentation. Classes may declare the interfaces they implement by setting their __implements__ attribute to a tuple of interfaces. Additional interfaces may be added to classes with registerAdapter(adapterClass,originalClass,interface); when getAdapter(obj, interfaceClass) is called on an object, the adapter associated with that interface and class is looked up and instantiated as a wrapper around obj. (Alternately, if obj implements the requested interface, the original object is simply returned.)

Componentized

In addition to the basic system of adapters and interfaces, Twisted has the Componentized class. Instances of Componentized hold instances of their adapters. This storage of adapter instances encourages separation of concerns; multiple related instances representing aspects of a simulation object can be automatically composed in a single Componentized instance.

Componentized is the heart of Twisted Reality; it is subclassed by Thing, the base class for all simulation objects. Functionality is added to Things with adapters; for example, the Portable adapter adds the abilities to be picked up and dropped.

By separating aspects of the simulation object into multiple instances, several improvements in ease of code maintenance can be realized. Persistence of simulation objects, for example, is greatly eased by Componentized: each adapter's state can be stored in a separate database table or similar data store.

Parsing System

The key element missing from multiuser simulations' parsing systems is an abstract representation of actions. Current systems proceed directly from parsing the user's input to executing object-specific code. For example, LambdaMOO, one of the most popular object-oriented simulation frameworks, handles input using a non-customizable lexer which dispatches to parsing methods on simulation objects. The ColdCore framework, a similar effort, improves on this model by providing pattern-matching facilities for the lexer, but performs dispatch in essentially the same fashion. In contrast to these systems, Twisted Reality separates parsing from simulation objects entirely, keeping a global registry of parser methods which produce objects representing actions, rather than directly performing the actions. Adding this layer allows for more sophisticated parsing and sensitivity to ambiguity.

The parser in reality.text.english uses a relatively simple strategy: it keeps a parser registry which maps verbs (i.e., substrings at the beginning of the user input) to parser methods, and runs all methods whose prefixes match the input, collecting the actions they return. Parsing methods are added to the system by registering Subparsers.

class MusicParser(english.Subparser):
    def parse_blow(self, player, instrumentName):
        actor = player.getComponent(IPlayWindInstrumentActor)
        if actor is None:
            return []
        return [PlayWindInstrument(actor, instrumentName)]

english.registerSubparser(MusicParser())

english.registerSubparser collects methods prefixed with parse_ from the subparser and places them in the parsing registry.

a Room
You see a rocket, a whistle, and a candle.
Exits: a door, north
bob: blow whistle
You play a shrill blast upon a whistle.

Here is one of the simplest cases for the parser: blow whistle should obviously resolve to a single action, in this case PlayWindInstrument.

The parser calls MusicParser.parse_blow with the actor and the remainder of the input, and adds the list of actions it returns to the collection of possible actions. If only one action is possible, it immediately dispatches it. This strategy allows the parser to examine the state of the simulation before committing to a decision about what the player means. For example, the check for the actor interface is a simple form of permissions; if you don't implement the required interface, you aren't allowed to perform the action.

Since this sort of parser is quite common, it has been generalized to a simple mapping of command names to actions:

class FireParser(english.Subparser):
    simpleTargetParsers = {"blow": Extinguish}

english.registerSubparser(FireParser())
bob: blow candle
You blow out a candle.

The real test of any parsing system of this nature, of course, is its ability to handle ambiguity. Since two possibilities for parsing a command starting with blow now exist, the parser has two potential actions to examine: PlayWindInstrument and Extinguish. Obviously, only Extinguish makes sense, and the parser determines this by examining the interfaces on the targets and rejecting actions for which the target is invalid.

class ExplosivesParser(english.Subparser):
    simpleToolParsers = {"blow": BlowUp}

english.registerSubparser(ExplosivesParser())
bob: blow door
You fire a rocket at a door.
*BOOM*!! 
The door shatters to pieces!

The other common case is actions with three participants -- actor, target, and tool. The parser generated here is intelligent enough to look around for an appropriate tool (again, by examining interfaces) and include it in the action.

Despite these techniques for disambiguating the user's meaning, situations will inevitably arise where multiple actions are equally valid parses. In these cases, the parser formats the list of potential actions and presents the choices to the user.

You see a short sword, and a long sword.
bob: get sword
Which Target?
1: long sword
2: short sword
bob: 1
You take a long sword.

Actions System

Actions in Twisted Reality, as in Inform, are objects representing a successful parse of a player's intentions. Actions are classified according to the number of objects they operate upon: NoTargetAction (actions such as Say or Look), TargetAction (e.g. Eat, Wear), ToolAction (e.g. Open, Take). When actions are defined, interfaces corresponding to the possible roles in the action are also created. When an action is instantiated, it asks the participants in the action to adapt themselves to the actor, target, or tool interfaces, as appropriate. When dispatched, the action may call handler methods on the adapted objects or dispatch subsidiary actions.

IDamageActor = things.IThing
class Damage(actions.ToolAction):
    def formatToActor(self):
        with = ""
        if self.tool:
            with = " with ", self.tool
        return ("You hit ",self.target) + with + (".",)
    def formatToTarget(self):
        with = ()
        if self.tool:
            with = " with ", self.tool
        return (self.actor," hits you") + with + (".",)         
    def formatToOther(self):
        with = ""
        if self.tool:
            with = " with ", self.tool
        return self.actor," hits ",self.target) + with + (".",)
    def doAction(self):
        amount = self.tool.getDamageAmount()
        self.target.damage(amount)

class Weapon(components.Adapter):
    __implements__ = IDamageTool    
    def getDamageAmount(self):
        return 10

class Damageable(components.Adapter):
    __implements__ = IDamageTarget
    def damage(self, amount):
        self.original.emitEvent("Ow! that hurt. You take %d points of damage."
                                % amount, intensity=1)
class HarmParser(english.Subparser):
    simpleToolParsers = {"hit":Damage}

english.registerSubparser(HarmParser())
components.registerAdapter(Damageable, things.Actor, IDamageTarget)

actions.ToolAction, via metaclass magic, creates three interfaces when subclasssed, named after the subclass: in this case, IDamageActor, IDamageTarget, and IDamageTool. However, since IDamageActor already exists, the metaclass does not clobber it. Setting IDamageActor to IThing indicates that any Thing may perform the Damage action. The other elements of the action are represented here by Weapon and Damageable as the tool and the target, respectively. The HarmParser adds a hit command, and the call to registerAdapter ensures that any Actors who do not already have a component implementing IDamageTarget will receive a Damageable when needed.

room = ambulation.Room("room")
bob = things.Actor( "Bob")
rodney = things.Actor("rodneY")
sword = things.Movable("sword")

sword.addAdapter(conveyance.Portable, True)
sword.addAdapter(harm.Weapon, True)


for o in rodney, bob, sword:
    o.moveTo(room)

In this example, we create instances of MovableActor (subclasses of Thing), a Room, then adds a Portable adapter to the sword, allowing it to be picked up and dropped, as well as a Weapon adapter, and finally moves all three into the room.

a room
You see rodneY, and a sword.
Bob: get sword
You take a sword.
Bob: hit rodney with sword
You hit rodneY with a sword.

The parser instantiates the Damage action with Bob, Rodney, and the sword as actor, target, and tool. The action is dispatched, calling Damage.doAction, which inflicts damage upon Rodney. From Rodney's perspective:

a room
You see Bob, and a sword.
Bob takes a sword.
Bob hits you with a sword.
Ow! that hurt. You take 10 points of damage.
rodneY:

The primary advantage of this actions system is that it provides a central point for dispatching object-specific behaviour in a customizable manner. This mechanism prevents order-of-execution problems: in other simulations of this type, combining multiple game effects is difficult since the connections between them are not made explicit. When confronted with ambiguity, TR's action system refuses to guess: all combinations of effects that make sense must be implemented separately. The Adapters system makes this manageable even in the face of arbitrarily extended complexity.

Also, it allows for centralized handling of string formatting, instead of having each actor or target handle output of event descriptions. For example, suppose there is a zone prohibiting PvP combat. The Damage action can suppress the usual messages describing combat (as well as the actual damage routines) since it is responsible for generating them.

Composing Simulations with Adapters

The combination of these features -- an incrementally extendable parser, actions as first-class objects, componentized simulation objects -- provide a powerful basis for the composition of simulations within a virtual world, often enabling extensions to the world and object behaviour without touching unrelated code. For example, to add armor that reduces damage to the simple combat simulation described above, we add an Armor class which forwards the IDamageTarget interface:

class Armor(raiment.Wearable):
    __implements__ = IDamageTarget,  raiment.IWearTarget, raiment.IUnwearTarget
    originalTarget = None
    armorCoefficient = 0.5
    def dress(self, wearer):
        originalTarget = wearer.getComponent(IDamageTarget)
        if originalTarget:            
            self.originalTarget = originalTarget
            wearer.original.setComponent(IDamageTarget, self)

    def undress(self, wearer):
        if self.originalTarget:
            wearer.setComponent(IDamageTarget, self.originalTarget)
            
    def damage(self, amount):
        self.original.emitEvent("Your armor cushions the blow.", intensity=2)
        if self.originalTarget:
            self.originalTarget.damage(amount * self.armorCoefficient)

Armor inherits from the Wearable adapter, and thus receives notification of the player wearing or removing it. When this happens, it forwards or unforwards the damage method, respectively.

a room
You see an armor, Bob, and a sword.
rodneY: take armor
You take an armor.
rodneY: wear armor
You put on an armor.
Bob hits you with a sword.
Your armor cushions the blow.
Ow! that hurt. You take 5 points of damage.

In this fashion, the combat simulation can be extended to deal with various types of weapons, armor, damageable objects, and types of damage, with little or no changes to existing code.

Now, let us consider a second type of simulation common to virtual worlds: shops. We wish to prevent unpaid items from leaving the shop, and to have a price associated with each item.

class IVendor(components.Interface): pass
class IMerchandise(components.Interface): pass

class Buy(actions.TargetAction):
    def formatToOther(self):
        return ""
    def formatToActor(self):
        return ("You buy ",self.target," from ",self.vendor," for ",
                self.target.price," zorkmids.")
    
    def doAction(self):
        vendors = self.actor.original.lookFor(None, IVendor)
        if vendors:
            #assume only one vendor per room, for now
            self.vendor = vendors[0]
        else:
            raise errors.Failure("There appears to be no shopkeeper here "
                                 "to receive your payment.")
        amt = self.target.price
        self.actor.withdraw(amt)
        self.vendor.buy(self.target, amt)

class ShopParser(english.Subparser):
    simpleTargetParsers = {"buy": Buy}
english.registerSubparser(ShopParser())

The basic behaviour for buying an object in a shop is simple: first, a vendor is located, the price is looked up, then money is transferred from the buyer's account to the vendor's.

class Customer(components.Adapter):
    __implements__ = IBuyActor

    def withdraw(self, amt):
        "interface to accounting system goes here"
        
class Vendor(components.Adapter):
    __implements__ = IVendor

    def shoutPrice(self, merch, cust):
        n = self.getComponent(english.INoun)
        title = ('creature', 'sir','lady'
                 )[cust.getComponent(things.IThing).gender]
        merchName = merch.original.getComponent(english.INoun).name))
        self.original.emitEvent('%s says "For you, good %s, only %d '
                                'zorkmids for this %s."' % (n.nounPhrase(cust),
                                                            title, merch.price,
                                                            merchName))

    def buy(self, merchandise, amount):
        self.deposit(amount)
        merchandise.original.removeComponent(merchandise)

    def stock(self, obj, price):
        m = Merchandise(obj)
        m.price = price
        m.owner = self
        m.home = self.original.location
        obj.addComponent(m, ignoreClass=1)

    def deposit(self, amt):
        "more accounting code"

The essential operations for management of shop inventory are Vendor.stock and Vendor.buy, which add and remove a Merchandise adapter, which stores the state related to the shop simulation for the object (in this case, its price, its owner, and the location it lives).

A weapons shop.  You see a long sword, and Asidonhopo.
Exits: a Secret Trapdoor, down; a Security Door, north
bob: get sword
You take a long sword.
Asidonhopo says "For you, good sir, only 100 zorkmids for this long sword."

To enforce our anti-theft policy, we put constraints on the exits to the shop.

class ShopDoor(ambulation.Door):
    def collectImplementors(self, asker, iface, collection, seen,
                            event=None, name=None, intensity=2):
        if iface == ambulation.IWalkTarget:
            unpaidItems = asker.searchContents(None, IMerchandise)
            if unpaidItems:                
                collection[self] = things.Refusal(self, "You cant leave, "
                                                        "you haven't paid!")
                return
                
        ambulation.Door.collectImplementors(self, asker, iface,
                                            collection, seen, event,
                                            name, intensity)
        return collection

collectImplementors is the means by which queries for action participants are accomplished. It is a rather general graph-traversal mechanism and thus takes a few arguments: asker is the object that initiated the query. iface is the interface the results must conform to, collection is the results so far, and seen is a collection of objects already visited. The check done here is fairly simple: it refuses queries for IWalkTargets (the interface needed for walking between rooms) if the asker contains things that implement IMerchandise, in particular unpaid items. Otherwise, it passes on the query to its superclass.

bob: go north
You cant leave, you haven't paid!

Here, the Security Door examines the actor's contents for objects implementing IMerchandise. Since the sword still has a Merchandise adapter attached, the passage is barred.

bob: go down

However, relying on the exits to contain merchandise is potentially error-prone; it demands knowing about all forms of locomotion in advance. If an unsecured exit from the shop exists, or the player has the ability to teleport, this form of security can be bypassed. Therefore, it is advantageous to have the Merchandise adapter itself keep the item within the shop.

class Merchandise(components.Adapter):
    __implements__ = IMerchandise, things.IMoveListener, IBuyTarget

    def thingArrived(*args):
        pass
    def thingLeft(*args):
        pass
    def thingMoved(self, emitter, event):
        if self.original == emitter and isinstance(event, conveyance.Take):
            self.owner.shoutPrice(self, self.original.location)
        if self.original.getOutermostRoom() != self.home:
            self.original.emitEvent("The %s vanishes with a *foop*."
                                    % self.getComponent(english.INoun).name)
            self.original.moveTo(self.home)

When objects move, they broadcast events to nearby things (where nearby is determined, again, by collectImplementors) that implement IMoveListener. In this case, the Merchandise adapter listens for being picked up, and prompts the shopkeeper to quote the price, and also checks to make sure it is contained by its home room. If the player manages to leave the shop with unpaid merchandise --

The long sword vanishes with a *foop*.

then it sets its location to its home room and informs the prospective shoplifter he no longer has his prize.

Future Directions

Current development efforts focus on enlarging the standard library of simulation objects and behaviour, developing web-based interfaces to the simulation, and improving the persistence layer. Possible extensions include client-side generation of action objects, enabling the development of graphical interfaces, or adapting the text system to other languages than English.

Conclusions

As seen in these examples, Twisted Reality provides features not found in other object-oriented simulation frameworks. The component model allows automatic aggregation of related objects; the actions system provides a mechanism for precise control of game effects; and the parser enables incremental extension of user input handling. Combined, they provide a powerful basis for modelling virtual worlds by composing simulations.

Acknowledgements

Thanks to Chris Armstrong and Donovan Preston for contributions to Twisted Reality, and to Ying Li for editorial assistance.

References