Rewarding the Effort
If the player has gone to all this trouble to reach the top of the tree, perhaps he or she deserves some sort of reward for it. One way we can do this is to add some points to the player’s score. This can be done with the statement:
|
addToScore(points, 'reason for awarding points'); |
|
In this case we might have:
|
addToScore(1, 'reaching the top of the tree.'); |
|
The two problems we need to solve now are (a) where best to put this statement and (b) how to prevent the player from accumulating a huge (if boring) score by repeatedly going up the tree - the point should be awarded first time round only.
It would possible to code this on the TravelMessage object Heidi has to
travel via when she climbs the tree, but since what we want to do is to
check for Heidi arriving at a the top of the tree regardless of how she
gets there, the best solution is to make use of the enteringRoom
method of the topOfTree
room. The library code already keeps track of
which rooms have been visited by setting their seen
property, so we
can use this to ensure that the point is awarded only the first time
Heidi reaches the top of the tree. The revised topOfTree
room then
looks like this:
|
topOfTree : OutdoorRoom ’At the top of the tree’
”You cling precariously to the trunk, next to a firm, narrow branch.”
down = clearing
enteringRoom(traveler)
{
if(!traveler.hasSeen(self) && traveler == gPlayerChar)
addToScore(1, ’reaching the top of the tree. ’);
}
;
|
Being awarded a point is all very well, but it may all seem pretty pointless if that’s all that happens when Heidi arrives at the top of the tree. At the very least she should find something interesting there. Since the room description for the top of the tree mentions a branch, that may be the first thing to add. Then perhaps we could place a bird’s nest on the branch (in the original Adventures of Heidi the object was to replace the bird’s nest, complete with fledgling, to the branch), and finally we could place a worthwhile find in the nest.
Before turning over the page to see how this guide does it, you could have a go at implementing these extra objects yourself. Remember that the branch will need to be a Supporter so you can put things on it, and the nest a Container so you can put things in it. Remember too that you’ll need to make sure that Heidi can’t pick up the branch - after all it’s part of the tree and fixed in place (if in doubt, look at the pedestal in the ‘goldskull’ game). Then put something interesting in the nest, and see if you can get your revised game to compile and run.
|
Here’s how this guide does it (this code should be placed immediately after the definition of topOfTree).
+ branch : Surface, Fixture ’branch’ ’branch’
”The branch looks too narrow to walk, crawl or climb along, but firm
enough to support a reasonable weight. ”
;
++ nest : Container ’bird\s nest’ ’bird\s nest’
”It’s carefully woven from twigs and moss. ”
;
+++ ring : Thing ’platinum diamond ring’ ’diamond ring’
”The ring comprises a sparkling solitaire diamond set in platinum. It
looks like it could be intended as an engagement ring. ”
;
Note the use of the + notation to nest (no pun intended) each item in the preceding one. We make the branch a Surface so that we can put things on it and a Fixture so that it’s fixed in place; this llustrates how the same object may inherit from more than one superclass (but note that the same object can’t be both a Surface and a Container). The nest is made a Container so we can put something in it. Internally there’s not a lot of difference; the location property of ring is set to nest, and the location property of nest to branch. The difference lies in the way the library code describes the situation (in or on) and the type of commands it will respond to (put in or put on), as you’ll find if you add the code and play with the new version of the game.
You’ll probably also find that the discovery of the ring seems rather
bland and bathetic, since as soon as Heidi arrives at the top of the
tree the game announces “On the branch is a bird’s nest (which contains
a diamond ring).” It would be more interesting if she had to work a
little to find that ring. Besides, one might suppose that the ring would
at first be hidden among the twigs and moss that make up the nest. The
first step towards making things more interesting, then, is to remove
the +++
from in front of the definition of the ring (so that it starts
life outside the game world altogether) and then code the nest to
respond to a search or look in command. This code must first
check that we haven’t already found the ring, and then, if we haven’t,
it should move the ring into the nest and report the find. While we’re
at it we might as well award the player a point for the discovery. The
appropriate code looks like this:
|
++ nest : Container ’bird\s nest’ ’bird\s nest’
”It’s carefully woven from twigs and moss. ”
dobjFor(LookIn)
{
action()
{
if(ring.moved)
{
”You find nothing else of interest in the nest. ”;
exit;
}
ring.moveInto(self);
”A closer examination of the nest reveals a diamond ring inside! ”;
addToScore(1, ’finding the ring’);
}
}
;
You’ll remember that dobj is short for direct object, so that when we
define dobjFor(LookIn)
on an object we’re defining what should happen
when that object is the direct object of a look in command. In this
case, though, what comes after dobjFor
looks rather more complicated
than our previous example.
Let’s take the simpler part first. Towards the end of the nest object we
have written dobjFor(Search) asDobjFor(LookIn)
. That asDobjFor()
is
a reasonably close cousin of the remapTo()
we encountered earlier; you
could read it as “as if it were the direct object for” so that
dobjFor(Search) asDobjFor(LookIn)
means “when the nest is the direct
object of a search command treat it as if it were the direct object
of a look in command”, or “treat search nest as equivalent to
look in nest”.
Up to this point we’ve only used dobjFor()
to make one command behave
like another, but obviously there has to be more to writing a game than
that: at some point we have to define what a command actually does,
and this is what the example above does for dobjFor(LookIn)
. Note
first of all that when we do that we follow dobjFor()
with a block of
code enclosed by braces {}. Within that block we have what looks like a
method called action() with its code enclosed in a further set of braces
{}. This may look rather strange; it is in fact exactly equivalent to
defining a method called actionDobjLookIn()
(without dobjFor()
and
the outer enclosing braces), and you could indeed do it that way. We
tend to use dobjFor()
instead because it makes the code easier to read
and write.
We’ll give a fuller account of all this at the end of the chapter, so
don’t worry if it still seems a little hazy at the moment. The main
thing to note right now is that the method that looks like it’s called
action()
, what we might call the action part of the dobjFor()
block,
is the place where we put the code that defines what actually happens
when the nest is looked in.
There are four further points to note about this code:
(1) moved is a property defined by the library; it starts at nil (i.e.
not true) and is set to true as soon as the object is moved from its
initial location, which is what ring.moveInto(self) does;
(2) to move the ring we use its moveInto method, we do not change its
location property directly by writing something like
ring.location = self (part of the reason for this is that the library
maintains lists of what’s contained by what; the moveInto method takes
care of updating the appropriate lists when an object is moved, while
setting the location property would not);
(3) self simply refers to the object in which it occurs (in this case
the nest);
(4) exit terminates the action straightaway, so that code following an
exit statement is not executed; the exit statement may also be used to
veto an action at the check stage, as we shall see shortly;
An alternative way of achieving the same effect would be to leave the +++ in front of the definition of ring, and add PresentLater to the front of its class list, at the same time changing ring.moveInto(self) to ring.makePresent() in the definition of nest. Where the PresentLater mix-in class is used, the game initialization makes a note of the object’s location, then moves it into nil (i.e. out of the game world); a call to makePresent() then restores the object to its initial location. Yet another way we could achieve much the same effect would be by making the ring of class Hidden, but we shall illustrate that on another object shortly.
The revision to the nest object makes things slightly more interesting, but searching the nest isn’t much of a challenge. It would be rather more interesting if in order to search the nest we first had to hold it, and, furthermore, if the nest was just out of reach so we first had to find some way of bringing it nearer. The obvious tool for the job would be some sort of stick, and the obvious place to find such a stick might be among twigs and branches lying at the foot of the tree.
But first we must prevent Heidi from looking in the nest until she’s
holding it. To do that we can use check()
. Like action()
, check()
is a method that goes inside the braces following dobjFor()
, but
whereas we use action()
to define what happens when the command is
carried out, we can use check()
to stop it being carried out and
explain why. In this case we don’t want to stop it unconditionally, but
only if the nest isn’t held:
check() |
{ |
if(!isHeldBy(gActor)) |
{ |
”You really need to hold the nest to take a good look at
what’s inside. ”;
exit; |
} |
} |
|
Our next job is to make it impossible to take the nest without the use
of the stick. TADS 3 already defines what happens when the player tries
to take something, so what we need to do here is to change that
behaviour; we do this by overriding the nest’s dobjFor(Take)
routines.
Nonetheless, when we do allow the action to go ahead, we’ll still want
the normal library handling to work; to ensure that happens we need to
include inherited
in the action part, which calls the action handling
for take that the nest inherits from its superclass. To ease the
player’s “guess the verb” hassle we’ll let the player character take the
nest if she’s simply carrying the stick. The appropriate code, which
illustrates both a check and an action section in the same dobjFor()
block, is as follows::
|
dobjFor(Take)
{
check()
{
if(!moved && !stick.isIn(gActor))
{
”The nest is too far away for you to reach. ”;
exit;
}
}
action()
{
if(!moved)
”Using the stick you manage to pull the nest near enough to take,
which you promptly do. ”;
inherited;
}
}
|
We include the if(!moved) condition in both check() and action() here on
the assumption that once the nest has been moved, it won’t be put back
out of reach. The inherited statement at the end of the action method
ensures that we actually do end up taking the nest (by continuing with
the standard behaviour for the take action). But this raises another
issue: inherited()
means roughly “at this point carry out what would
have happened if we hadn’t overridden this method”, but what would have
happened is that not only would Heidi have picked up the nest, but this
would have been reported with the laconic response “Taken.” That’s fine,
except the first time we take the nest (when moved
is still nil
),
when we don’t want to see “Taken” in addition to our custom message
about moving the stick. Actually, this won’t be a problem; if you try
running this code you’ll find that the “Taken” message doesn’t appear
alongside the other message. That’s because the library doesn’t use a
double-quoted string to produce it, but a macro called
defaultReport()
, in this case. defaultReport('Taken. ')
, and a
defaultReport is only shown if there’s no other report.
This is fine, but it might not occur to the player that the nest can be taken simply because Heidi is holding the stick; the player may suppose other command needs to be used to bring the stick nearer, such as move nest with stick, so we’ll code this command to act in exactly the same way. This introduces a new complication, defining special behaviour for a verb involving two objects. We’ll begin by defining some code on the intended direct object of this command, which is still the nest:
|
dobjFor(MoveWith)
{
verify()
{
if(isHeldBy(gActor))
illogicalAlready(‘{You/he} {is} already holding it. ’);
}
check()
{
if(gIobj != stick)
{
”{You/he} can’t move the nest with that. ”;
exit;
}
}
}
The effect of this code is to rule that it is illogical to attempt to move the nest with anything while the nest is being held, and not to allow the nest to be moved with anything other than the stick.
This code snippet introduces several new features we should pause to
consider. First, just as in TADS 3 the abbreviation dobj will normally
refer to a direct object, so iobj will normally refer to an indirect
object, the second object involved in a two-object command like
move nest with stick. But in the check
routine above what we
actually see is something call gIobj
. Here the g effectively stands
for ‘global’; although TADS 3 does not in fact support global variables,
it has several things that look and act like global variables, and
gIobj
is one of them (in fact these pseudo-global variables are
shorthand ways of referring to the properties of objects in which the
values are actually stored, but that needn’t concern us now). The common
not-exactly-global variables you’ll often encounter in TADS 3 include:
gDobj
(the direct object of the current action), gIobj
(the indirect
object of the current action), gAction
(the current action), gActor
(the actor performing the current action, which is usually but not
necessarily the player character), and gPlayerChar
(the object
representing the player character).
The next new feature we’ve introduced in this code is a verify()
routine. We’ll be giving the full story on verify()
shortly, but the
very brief version is that, like check()
, verify
can be used to veto
an action, but that unlike check()
, verify()
affects disambiguation
(which object the parser thinks a player’s command most probably refers
to). A verify()
routine can contribute to disambiguation without
vetoing an action, but when it does disallow an action it (nearly)
always does so with statement of the form
illogical('Reason why this action is plain daft. ')
.
illogicalAlready()
is just a specialized form of illogical()
, used
when the action is pointless because it’s trying to bring about
something that’s already the case (for example, trying to open a door
that’s already open).
The third new feature is the use of parameter substitution strings,
meaning those strange things in curly braces: {you/he}
and {is}
. The
point of these is that when they are actually displayed in a game, they
are replaced with the appropriate text. So, for example, if the player
issues the command move nest with stick ‘{you/he} {is}
’ becomes ‘you
are’, but if an NPC called Fred attempted the action it would become
‘Fred is’. For the full story on parameter substitution strings, read
the Message Parameter Substitutions article in
the Technical Manual.
The next job is to make the stick handle its part of the action. You’ll
recall that the stick is the indirect object of this command, so you
might guess that just as we need to define dobjFor()
on the direct
object, we need to define iobjFor()
on the indirect object. IUf you
did guess that, you’d be right:
|
iobjFor(MoveWith)
{
verify() {}
check() {}
action()
{
if(gDobj==nest && !nest.moved)
replaceAction(Take, nest);
}
}
The first thing to note is the verify()and check()
routines that do
nothing. We need them to do nothing to make sure they don’t veto the use
of the stick as the indirect object of a move with command. Without
that empty verify()
method move nest with stick would produce the
response ‘You cannot move anything with the stick.’ We don’t actually
need to provide an empty check()
method here, since the library
version is already empty, but including it here does no harm, and if we
didn’t know that the library didn’t rule out move with in check()
it
would be as well to include it just to make sure.
The action()
routine only does anything if the direct object (gDobj
)
is the nest and the nest hasn’t already been moved. If either of those
conditions fails, the command will result in the default MoveWith action
of the direct object, which simply reports that moving the direct object
didn’t achieve anything. If, however, the direct object is the nest and
it hasn’t been moved yet we want the result to be the same as if we had
issued the command take nest. We achieve this with the
replaceAction
macro, which does just what it says it does and stops
processing the current command, replacing it with the new command. Had
we wished to execute another command and then continue with the existing
command we would have used nestedAction
instead.
You may be wondering how replaceAction()
differs from remap()
, which
we used before. The main difference is that remapping happens before
anything else (in particular, before verify()
and check()
), while
replaceAction()
, which can really only go in the action()
part,
happens after verify()
and check()
; replaceAction()
and
nestedAction()
are also more flexible than remap in that they can be
combined with other code in the action()
routine; we can do other
things before replacing the action, but we can’t do anything before
remapping.
You may also be wondering why we put the code for taking the nest in the
action()
routine of the indirect object rather than the direct object
here. The answer is, there’s no reason at all for doing it that way,
apart from illustrating the use of an action()
routine on an indirect
object (and incidentally taking advantage of the fact that the indirect
object’s action()
routine generally runs before the direct object’s
action()
routine). Other than that, it would have been just as good,
if not better, to have put this handling on the direct object, thus:
dobjFor(MoveWith){ verify() { if(isHeldBy(gActor)) illogicalAlready('{You/he} {is} already holding it. '); } check() { if(gIobj != stick) { "{You/he} can't move the nest with that. "; exit; } } action() { if(!moved) replaceAction(Take, self); else inherited(); }}
Indeed, this would probably have been a clearer and neater way of doing
it. In this revised version, note the use of inherited()
to call the
base class handling of MoveWith once the nest has been moved. Note also
that else
on the line before is not strictly necessary since once
replaceAction()
is executed the original (MoveWith) action is stopped
in any case.
More broadly, this illustrates that the action()
part of a two-object
command can be written on either the direct object or the indirect
object or even split between both, since both action routines will be
executed (unless exit
or one of its close relatives is used to break
out of them), with the indirect object’s action handling usually (but
not necessarily) called before the direct object’s. So how do we decide
which to use? One rule of thumb might be to write the action()
handling on the object most affected by the action. Another might be to
write it on the object which most affects the outcome of the item: for
example, if your game contains a knife, a sword and a laser rifle, and
cutting things with each of these produces greatly different results,
you might want to write the CutWith action handling on the indirect
object, whereas if it doesn’t make much difference what you cut with but
a great deal of difference whether it’s a piece of solid rock, a window,
a lump of butter, or Aunt Gertrude that you’re cutting, you’ll probably
be better off putting the action handling on the direct object. A third
rule of thumb might be to put the action handling on the object that
exhibits the most exceptional behaviour; so, for example, most objects
won’t allow things to be put inside them, but Containers will, so the
library action handling for put x in y goes on the indirect object
(the Container), not the thing that’s being put in the Container. Other
cases may be less clear-cut (no pun intended), in which case it’s best
to choose one or the other and stick with it consistently (having an
action like CutWith handled on some direct objects and some indirect
objects is a likely recipe for confusion).
Enough of that digression; let’s return to the stick. Although the base of the tree is a good place to find the stick, it’s probably better not to make it too obvious; if the stick is just lying there in plain sight the player will take it automatically, which will make getting hold of the nest virtually a non-puzzle. We’ll make things harder by burying the stick in a pile of useless twigs, so that the player has to do some work to find them. While we’re at it we’ll change the description of the sycamore tree so that it refers to the pile of twigs. Again, this is something you might like to try yourself before reading on to see how this guide does it. After changing the description of the tree, you’ll need to add one object to represent the pile of twigs, and then another for the stick object, which should remain hidden until Heidi examines or searches the pile of twigs. To hide the stick you could use one of the techniques discussed in relation to hiding the ring in the nest, or you could make the stick a Hidden object and call its discovered() method at the appropriate moment.
|
Here’s one way of doing it:-
|
+ tree : Fixture ’tall sycamore tree’ ’tree’
”Standing proud in the middle of the clearing, the stout
tree looks like it should be easy to climb. A small pile of loose
twigs has gathered at its base. ”
dobjFor(Climb) remapTo(Up)
;
+ Decoration ’loose small pile/twigs’ ’pile of twigs’
”There are several small twigs here, most of them small, insubstantial,
and frankly of no use to anyone bigger than a blue-tit <<stick.moved ?
nil : ’; but there is also one fairly long, substantial stick among
them’>>. <<stick.discover>>”
dobjFor(Search) asDobjFor(Examine)
;
+ stick : Hidden ’long substantial stick’ ’long stick’
”It’s about two feet long, a quarter of an inch in diameter, and
reasonably straight. ”
iobjFor(MoveWith)
{
verify() {}
check() {}
action()
{
if(gDobj==nest && !nest.moved)
replaceAction(Take, nest);
}
}
;
We could have handled the stick in a similar manner to the ring, by
moving it into the clearing when we wanted it to appear, but this seems
a good opportunity to introduce the Hidden class, which does much what
it says. A Hidden item is one that is physically present but does not
reveal its presence until its discover method is called. By making stick
of class Hidden instead of class Thing, we can control when we want it
to appear. In this case we want it to appear when the player character
examines the pile of twigs, so we make an embedded call to
stick.discover in the description of the twigs, using the <<>>
syntax. The fact that this method will be called every time the twigs
are examined doesn’t matter, since once the method has been called once,
the subsequent calls will have no effect. There are a couple of other
refinements we need to think about, however. First, the description of
the pile of twigs should only refer to the stick amongst them until the
stick has been moved; we achieve this through another embedded
expression,
<<stick.moved ? nil : '; but there is also one fairly long, substantial stick among them'>>
that displays nothing if the stick has been moved but describes the
stick if it hasn’t. Secondly, the player might reasonably try to
search the pile of sticks as well as examine them, so we add a
line to the definition of the anonymous sticks object to remap
search to examine.
Note how we have defined the vocabWords property of the Decoration object representing the pile of twigs: we have defined it as ‘loose small pile/twigs’. Although you can’t normally refer to an object by two or more of its nouns, there is an exception in the case of a name like ‘x of y’, where both x and y should be specified as nouns. Our pile of twigs will now respond to examine twigs or x small pile or search pile of loose twigs and other such combinations.
Let’s just add one final refinement. Normally if you drop an object, it lands in the room where you are, as you would expect. But if you were to drop something from the top of a tree you’d expect it to fall to the ground below rather than hover around in the air still conveniently in reach. It would be good if we could model this in our game, and it turns out to be fairly straightforward. First, we need to change the class of topOfTree to FloorlessRoom, which means that any object dropped or thrown from this location won’t land here. Then we need to override topOfTree’s bottomRoom property to define where something dropped from there will land. In this case we want bottomRoom to be clearing. Now anything dropped (or thrown) while Heidi is at the top of the tree will fall to the clearing, and the game will display a suitable message to show that the object is falling out of sight. The definition of topOfTree thus becomes;
|
topOfTree : FloorlessRoom ’At the top of the tree’
”You cling precariously to the trunk, next to a firm, narrow
branch. ”
down = clearing
enteringRoom(traveler)
{
if(!traveler.hasSeen(self) && traveler == gPlayerChar)
addToScore(1, ’reaching the top of the tree. ’);
}
bottomRoom = clearing
;
|
In the next chapter we shall learn how the ring came to be in the nest, who it belongs to, and how to win the game. To do that we shall need to create a Non Player Character (NPC), and that will be our central task.
Getting Started in TADS 3
[Main]
[Previous] [Next]