The TADS Alternate Library
Version 2.0
Sense Passing
Copyright 2000 by Kevin Forchione.
This is part of the TADS Alternate Library Authors Manual.
Introduction and Table of Contents
Sense Passing
Alt owes much of its sense-passing concepts, and is greatly indebted, to the pioneering efforts of the TADS 3 development team, notably the concepts of Materials, Connectors, and Senses developed by Michael J. Roberts, as well as work done in TADS 2 library extensions such as Sense.t and Scope.t.
Algorithms concerned with
working out a path along a containment tree will tend to have common
characteristics, however Alt takes a different tack from ADV3, Sense.t, or
WorldClass. The main driver behind the library’s sense passing mechanism is the
scope() method. This method takes 4 arguments: sense, path, target, and
callbackObj.
sense is one of the Sense class objects defined by Alt.
These objects define the sense attribute to be evaluated by the method. Alt
defines four senses: sight, sound, smell, and touch. Tasting is an extension of
touching. Passing nil creates a path
that traverses the tree without regard to sense.
path
is passed around the methods used to traverse the tree and contains a valid
sense path from the vantage to all objects sensible by the vantage or to the
target object, if target isn’t nil.
target
indicates an object that we are
evaluating for a particular sense accessibility by the vantage. If target is
nil then the path will contain all objects accessible via the indicated sense
for the vantage.
callbackObj is an object of the Callback class that can be
passed to control traversal across the tree, and is especially useful for
performing checks along the path. Alt defines one callbackObj:
lightingCallback, which indicates whether we have light along the path.
The scope() method returns
a list that is the path constructed between the vantage (initial object) and
all objects sensible to the vantage by a given sense, or a path between the
vantage and the target object.
These methods are called
by scope() and used by it to traverse the object tree. They should never be
called directly. scopeLocations() first determines if the object can sense its
locations (in the case of Connector class objects there may be more than one
location. Other classes return only one location). The scopeConents method
performs a similar function for the object’s contents.
These methods should also
never be called directly by author code. They are used by the scopeLocations()
and scopeContents() methods when an object cannot sense its locations or
contents and the scope() method has been passed a target. In this situation
these methods “feel” ahead for the target, traversing the tree, from the point
at which the sense was blocked, without regard to any sense in order to
determine if the target lies further along the path. If the target is found
using these methods then the scopeLocations() / scopeContents() methods set the
appropriate attributes on the target:
target.obstructedSense indicates the sense that failed to detect
the target
target.obstructor indicates the first object in the path
that blocked accessibility to the target.
These attributes are then
retrieved by the DeepVerb.cantReach() method and control is directed to the
appropriate object (the target’s obstructor) and the appropriate cantSense
method (i.e. if the obstructedSense is sight then the target’s cantSee() method
is called).
This method is used to
determine if a vantage can sense an object using a particular sense. The method
takes the following arguments:
sense The
Sense class object corresponding to the sense being used to detect the target
target The object that is being sensed by the
vantage.
The method returns true if
the target can be sensed by the vantage using the specified sense; otherwise it
returns nil.
This method is called
specifically by the DeepVerb.validXoSense() methods, which are called from the
checkValidity() function during preCommand(). At the preCommand() stage we are
only interested in whether the object can be sensed by the sense appropriate
for the verb.
If the object’s
senseByPreferred attribute has been set to true by the DeepVerb.validXo()
methods then checkValidity() bypasses executing DeepVerb.validXoSense(), saving
time in the validation step.
This method is used to
determine if the target can be sensed by the vantage using any sense. The
method takes two arguments:
prefSense Indicates the preferred sense used to
detect the target. The method will first attempt to detect the target using
this sense. If the target cannot be detected using the preferred sense then the
method continues sensing using the other senses available to the vantage.
target The
object that is being sensed by the vantage.
The method returns true if
the target can be sensed by the vantage using any sense at all; otherwise it
returns nil. Additionally the target’s senseByPreferred is set to true if the
object has been sensed by the preferred sense; otherwise this attribute is set
to nil.
This method is used
specifically by the DeepVerb.validXo() methods
These methods are used by
scope() to determine if the object allows sensing across the boundary between
its contents and its locations. The method takes two arguments:
sense The
Sense class object corresponding to the sense being used to detect the target
isVantage A
Boolean indicating whether this object is the vantage (i.e. the first object in
the sensing chain.)
These methods can be
modified by the author to control how an object behaves with regard to sensing.
The methods should return true to indicate that sensing is allowed; otherwise
they should return nil.
Alt defines a cantSense
method for each of the senses. Thus cantSee(), cantHear(), cantSmell(), and
cantTouch() each correspond to the four senses defined by Alt. When
accessibility fails, either during the parsing or preCommand’s checkAccessibility()
stages then control is passed to the obstructor’s corresponding cantSense
method. The methods first determine whether the actor can see the object. If
the object is visible, but not accessible to the sense required by the verb a
message appropriate to the action is displayed: “You can’t reach that from
here.” If the object is not visible then the “You don’t see any foo.” message
is displayed.
This method is used to
detemine whether an object is lit or not. Unlike ADV.T, isLit() is always used
as a method for calculating the presence of light for the object. If an object
provides light, such as a Room or a Lightsource, then the object’s lightsOn
attribute is true; if the object does not provide light on its own then lightsOn
should be set to nil. The determination of whether or not the object is lit is
handled by the isLit Method. This method takes one argument:
vantage The
“observer” of the object for which light is being determined.
Unlike ADV.T, whether or
not an object is lit is dependent upon the relationship of the object and an
observer, the vantage. It might be the case that the observer is inside the
object, such as a steel safe. In this case the safe might be lit from the
inside by a candle, while on the outside lighting is dependent upon other
sources in the room. Is the safe lit? This is not such a simple question.
It depends not only upon
whether the candle inside is burning, or whether the room has a Lightsource,
but requires a perspective when asking the question, “is the safe lit?” We must
necessarily ask, “for whom?”
Alt defines 4 senses: sight,
smell, sound, and touch. Taste is considered an extension of touch. Senses
are objects of the Sense class. Each sense has a thruProp attribute that is set
to an attribute pointer corresponding to that particular sense. The attribute
is used by the Materials class to determine whether the particular material
allows passage of the sense.
The Material class is one
of the most elegant aspects of Alt’s sense passing mechanism and owes its
conception to Michael J. Roberts. Alt shamelessly borrows from this conception
as a simplification of the scheme developed by Sense.t that should make life
much easier for authors.
Alt defines the following
Materials (based on the schema developed by Michael J. Roberts)
Material |
seeThru |
hearThru |
smellThru |
touchThru |
altima |
nil |
nil |
nil |
nil |
glass |
true |
nil |
nil |
nil |
paper |
nil |
true |
true |
nil |
fineMesh |
true |
true |
true |
nil |
coarseMesh |
true |
true |
true |
true |
altima is the default Material of every game object
inheriting from Thing class and does not allow passage of sight, sound, smell,
or touch.
glass allows passage of sight, but not of sound, smell,
or touch.
paper allows passage of sound and smell, but not sight
or touch.
fineMesh allows passage of sight, sound, and smell, but not
touch.
coarseMesh
allows passage of sight, sound,
smell, and touch.
Connectors act as a
conduit for sense-passing between locations. This concept isn’t new, WorldClass
employed a similar strategy, however Alt’s implementation is based upon the
work of Michael Roberts. Connectors derive from Alt’s MultiLoc class. This
means that they are Floating class objects that are moved to their locations
prior to the parsing and execution of each command.
Unlike other classes,
however, the Connector class returns the entire list of its locations when the
scope methods request getLocations(). This entire list is used in creating the
object tree, and in evaluating object accessibility.
Connector class objects
also inherit and evaluate their own connectorMaterial attribute, which is used
to determine if the sense can pass through the Connector’s material.
The final ingredient of
Alt’s sense passing mechanism involves the parser hooks for DeepVerb and
preCommand(). When a command is issued by the player the parser calls the
DeepVerb parser hooks validIoList(), validIo(), validDoList() and validDo() to
help it decide in the disambiguation of the indirect and direct objects. Alt’s
DeepVerb validXoList() methods return nil by default, meaning that every object
found by the parser using its dictionary match is to be passed to the DeepVerb
validXo() methods for further validation.
The validXo() methods send
a message to the actor.canSensePreferred() method, passing the corresponding
DeepVerb.xoSense and the target object to be sensed. The canSensePreferred()
method will return either true or nil depending upon whether the object can be
sensed by the actor by any sense. It will also set the target object’s
sensedByPreferred attribute to either true or nil if the object is sensible
using the DeepVerb.xoSense.
If the object cannot be
sensed at all by the actor then the parser will display “I don’t see any foo
here”.
If the object is at least
sensible to one sense by the actor then disambiguation and object resolution
continues until control passes to preCommand(). At preCommand() the command is
re-validated, this time checking the indirect and direct objects’
sensedByPreferred attribute. If the attribute is true then the command is
considered valid and processing continues through command execution.
If sensedByPreferred is
nil then a message is sent to the corresponding verb.validXoSense(). This
method uses actor.cansSenseObj() to determine if the object can be sensed by
the verb.xoSense. If the method returns true then processing continues through
command execution.
If DeepVerb.validXoSense()
fails then control is passed to DeepVerb.cantReach(). This method retrieves the
target.obstructor and target.obstructedSense values and passes control to the
corresponding obstructor’s cantSense method. Finally we reset the target
objects sensedByPreferred attribute and abort the command.
The advantage of
validating accessibility in this way is that by the time we reach the cantSense
methods for the obstructor all of the elements of command have been determined and
we are able to redirect control to other portions of the program without having
to parse player command phrases.
Suppose we want to connect
two rooms so that an actor can smell bread cooking in the kitchen while
standing in the pantry. The bread is in the stove, which is a closed
OpenableContainer.
#include <stdif.h>
modify story
startingLocation = kitchen
title =
"Alt Advanced Starter Game"
headline
= "An Interactive Fiction"
introduction = "Welcome to TADS Alternate Library..."
;
/* starting location */
kitchen: Room
sDesc =
"Kitchen"
lDesc =
"This is the Kitchen."
;
stove: OpenableContainer
location
= kitchen
noun =
'stove' 'oven'
isOpen =
nil
sDesc =
"stove"
;
loaf: FoodItem
location =
stove
noun =
'loaf'
adjective
= 'bread'
sDesc =
"loaf of bread"
smellDesc
= "Smells delicious!"
;
After creating the story
file we need to set the #include paths for compiling. These can be set from the
Workbench’s Build àSettings menu by selecting the Build Settings
Include tab and adding the following directories:
alt
alt\classes
alt\functions
alt\grammar
alt\objects
We can now compile the
source. The game should display:
Welcome to TADS Alternate Library...
Alt Advanced Starter Game
An Interactive Fiction
Release 1 / Serial number 000718
Alt Library Version 2.0.0
Kitchen
This is
the Kitchen.
You see
a stove here.
If we attempt to smell the
bread we get the following message:
>smell loaf
You can't see any loaf here.
This is because the stove
has been defined as closed. Closed OpenableContainers force the sense to pass
through their materials. Open OpenableContainers do not impede sense passage
regardless of their material. Since the stove doesn’t define a material
attribute it inherits it from Thing, which means that its default material is
altima. Altima doesn’t allow any senses to pass through an object, so the bread
cannot be detected.
Because smell has failed
the library then tries to detect the loaf using another sense. In this case the
actor fails to sense any loaf and so the library displays a default message
that is sight-oriented: “You don’t see any loaf here.” rather than
smell-oriented.
If we open the stove all
senses are able to pass through it, and thus we can now sense the bread:
>open stove
Opening the stove reveals
a loaf of bread.
>smell loaf
Smells delicious!
In order to allow the
actor to smell the bread while the stove is closed we must change the material
of the stove. Paper is the ideal material for allowing smell and sound to pass
through the object, while preventing sight and touch.
stove: OpenableContainer
location
= kitchen
noun =
'stove' 'oven'
isOpen =
nil
sDesc =
"stove"
/*
* Paper allows sound and smell to pass
*/
material
= paper
;
Now when we smell the loaf
we get a smell description whether the stove is open or closed. But suppose we want
the smell to display when the stove is open, whether the actor is smelling or
not? Normally a daemon would be activated, but with Alt object reactions we can
handle this from scopeEndCommand().
loaf: FoodItem
location
= stove
noun =
'loaf'
adjective = 'bread'
sDesc =
"loaf of bread"
smellDesc
= "The smell of baked bread permeates the room."
scopeEndCommand = {
if
(!(gVerb() == smell && gDobj() == self))
if (gActor().canSenseObj(sight, self))
"\b<<self.smellDesc>>";
};
First scopeEndCommand()
checks to see if we’re attempting to smell the bread. Since we will have
already displayed the smelldesc if we <<smell loaf>> there is no
need to display the message again. Also, since we only want the message to
display when the oven door is open we check to see if the actor can see the
loaf. canSenseObj(sight, self) will return true if the actor can see the loaf;
otherwise it returns nil.
Technically
our message will display even when the actor is holding the loaf, which could
be a little annoying. How can scopeEndCommand() be modified to display only
when the actor can see the loaf, but it isn’t being carried by an actor?
The next step is to add
our pantry definition.
kitchen: Room
sDesc = "Kitchen"
lDesc = "This is the Kitchen."
north = pantry
;
pantry: Room
sDesc = "Pantry"
lDesc = "This is the pantry."
south = kitchen
;
Both the kitchen and
pantry are top-level locations, which means that they aren’t connected to one
another along the object tree. The object tree consists of a network of objects
linked by locations and contents. The kitchen is linked to the pantry by travel
properties north and south. Given that objects only pass senses along the
object tree, how are we to pass senses across these two rooms?
Alt’s Connector class is
used as a conduit for senses between locations. Because the Connector is a
MultiLoc object we can do this in a variety of ways. Suppose we connect the two
rooms by a Passage that is also a Connector.
kitchen: Room
sDesc =
"Kitchen"
lDesc =
"This is the Kitchen. An archway to the north leads to the
pantry."
north =
archway
;
pantry: Room
sDesc =
"Pantry"
lDesc =
"This is the pantry. An archway to the south leads to the
kitchen."
south =
archway
;
archway: Connector, Passage
foundIn =
[kitchen, pantry]
sDesc =
"archway"
noun =
'archway' 'arch'
connectorMaterial = paper
;
Now our kitchen and pantry
are connected by the archway. If you open the oven door and walk into the
pantry you’ll notice that the loaf.smelldesc doesn’t display. This is because
the loaf is no longer visible to the actor.
Notice that the Connector class is
listed first in the archway definition and that the connectorMaterial attribute
is defined. Unlike normal game objects Connectors must define a
connectorMaterial, the default being altima, which doesn’t allow any senses to
pass through the object. If you find that your senses are not being passed
across locations joined by a Connector check that it assigns the appropriate
Material to its connectorMaterial attribute.
How can we modify
scopEndCommand() so that the message is displayed the first time the oven door
is opened and each time the actor enters the kitchen, but is not displayed on
consecutive turns within the room?