- Allen Short <washort@twistedmatrix.com>
- Glyph Lefkowitz <glyph@twistedmatrix.com>
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
Thing
s 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 Subparser
s.
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:
should obviously resolve to a
single action, in this case blow whistle
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 Actor
s 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 Movable
Actor
(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 IWalkTarget
s (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
- Jason Asbahr, Beyond: A Portable Virtual World Simulation Framework, Proceedings of the Seventh International Python Conference (1998).
- Pavel Curtis, LambdaMOO programmer's manual, 1997.
- Jim Fulton, Zope Component Architecture
- Brandon Gillespie, ColdC Reference Manual, 2001.
- Glyph Lefkowitz, and Moshe Zadka,
The Twisted Network Framework
, Proceedings of the Tenth International Python Conference (2002): 83. - Graham Nelson, The Inform Designer's Manual. 4th ed. (St Charles, IL: Interactive Fiction Library, 2001).