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.
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.
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.)
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.
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:
should obviously resolve to a
single action, in this case blow whistlePlayWindInstrument.
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 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 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.
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.
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.
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.
Thanks to Chris Armstrong and Donovan Preston for contributions to Twisted Reality, and to Ying Li for editorial assistance.
The Twisted Network Framework, Proceedings of the Tenth International Python Conference (2002): 83.