[Main]
[Previous]
[Next]
Making Life More Problematic
So far the game allows the player to walk from the cottage to the
clearing and then climb the tree, but this is not particularly
challenging. The time has come to add a puzzle: let’s suppose that in
order to climb the tree, Heidi first needs to fetch a chair and stand on
it. To make this a puzzle we must first prevent Heidi climbing the tree
when she’s standing on the ground. To do this we must change the
definition of clearing.up
. As a first attempt, we’ll use a close
relative (in fact the parent) of the FakeConnector
, namely the
NoTravelMessage
. Modify the clearing object so that its up
property
is now defined as follows:
up : NoTravelMessage {“The lowest bough is just too high for
you to reach. ”}
Now recompile the game and try both going up from the clearing and
climbing the tree. Both attempts should be foiled in exactlty the same
way. If we had remapped climb tree to TravelVia, topOfTree
instead
of Up
this would not have worked; the player could have bypassed our
puzzle by typing climb tree instead of up.
That was the easy part. The tricky part is creating a chair object that
will enable Heidi to climb the tree. The first thing we need is
somewhere to put it; the most likely place you’d find a chair is
probably inside the cottage. For the moment we’ll define the inside of
the cottage as:
insideCottage : Room ’Inside Cottage’
”You are in the front parlour of the little cottage. The door out
is to the east. ”
out = outsideCottage
east asExit(out)
;
The only new element here is the asExit
macro. The cottage lies to the
west of the starting postion, so to get from inside the cottage back
outside the player might type either out or east. The asExit
()
macro make going east the same as going out, but without having
east listed as a separate exit (either in the status line or in
response to an exits command). This allows us to make two directions
lead to the same destination without misleading the player into
supposing that they are two separate exits, instead of two synonyms for
the same exit.
Note too that since insideCottage is an indoor room, we have defined it
to be of class Room rather than class OutsideRoom. To make this room
accessible at all we should add the following to the definition of
outsideCottage:
Now recompile the game and you should be able to get inside the cottage
by typing either enter or in or west (or w). But one
thing the player might equally well try, namely enter cottage won’t
work.
The obvious way to fix this on the basis of what we’ve done before is to
add the following to the definition of cottage:
|
dobjFor(Enter) remapTo(In)
|
And this will certainly act just as expected. However, it’s more work
than we need, since the TADS 3 library provides an Enterable class to
handle just this kind of situation. All we need do, in fact, is to
change the definition of cottage to:
+ Enterable ->insideCottage ’pretty little
cottage/house/building’ ’pretty little cottage’
”It’s just the sort of pretty little cottage that townspeople dream of living in,
with roses round the door and a neat little window frame freshly painted in green. ”
;
This introduces a new template element: ->insideCottage
. In this
instance the ->
points to the TravelConnector
that is traversed when
the Enterable is entered. Remember that a room is a kind of
TravelConnector
, and that travelling via a room is the same as
travelling to a room, so for now the command enter cottage will take
Heidi to the inside of the cottage (we’ll be changing that later when we
give the cottage a locked front door). An alternative to using the
->connector
syntax would have been to define the connector property
explicitly with:
|
connector = insideCottage
|
Whether you prefer this as being more readable is up to you.
Now that we have somewhere to put the chair, we can start defining it.
What we need is something that we can carry around and stand on (but not
both at the same time!). So it must be something that can contain an
actor while appearing as an object inside a room. In TADS 3 this kind of
object is called a NestedRoom
. The TADS 3 library includes a subclass
of NestedRoom
called Chair
that does just the job (a Chair
is
something you can sit on or stand on but not lie on):
+ chair : Chair ’wooden chair’ ’wooden chair’
”It’s a plain wooden chair. ”
;
There’s one way we can improve the behaviour of this chair before we
even think about using it to climb the tree. When Heidi is sent into the
cottage, the game displays the plain vanilla default message “You see a
wooden chair here.” We can improve on this by adding the following
property definition to the chair object:
initSpecialDesc = ”A plain wooden chair sits in the corner. ”
The initSpecialDesc property defines how the object will be described in
a room description before the object has been moved (if we wanted to, we
could override the conditions under which initSpecialDesc was displayed,
but that’s a complication we won’t tangle with for now).
Now try compiling and rerunning the game. You should find that the chair
now behaves just as one would expect: you can sit or stand on it (but
not lie on it), you can also take it, but you can’t take it while you’re
sitting or standing on it, and you can’t sit or stand on it while you’re
carrying it.
But, as you will discover, the chair still doesn’t help Heidi climb the
tree. The problem is that we defined the connector on clearing.up as a
NoTravelMessage, which blocks travel under all circumstances. What we
need is a connector that allows Heidi to pass only when the chair is at
the foot of the tree, i.e. in the clearing. One type of connector
appropriate to this task is a OneWayRoomConnector, since this possesses
methods to control the conditions under which travel is permitted. We
could define it thus:
|
canTravelerPass(traveler) { return chair.isIn(clearing); }
|
explainTravelBarrier(traveler)
{ ”The lowest bough is just too high for
The canTravelerPass() method allows travel if and only if it returns
true, which in this case will happen if and only if the chair is in the
clearing. If travel is disallowed, the method explainTravelBarrier() is
called to explain why not. In this case we just display a suitable
message.
Before we carry on with refining this, let’s digress to another matter.
The connector we’ve just defined is defined on the up property of
clearing. This might lead us to suppose that we could have defined a
slightly more general version of it by defining:
|
canTravelerPass(traveler) { return chair.isIn(self); }
|
explainTravelBarrier(traveler)
{ ”The lowest bough is just too high for
Here we have simply changed chair.isIn(clearing)
to
chair.isIn(self)
, on the assumption that it will effectively mean the
same thing. But it won’t, since in the context in which we’ve defined
it, self
refers not to the clearing, but to the nested
OneWayRoomConnector
we’ve just defined on one of its properties. This
is a fatally easy easy mistake to make, and raises the question whether
there is a right way for a nested object like the anonymous
OneWayRoomConnector
in this example to refer to its host object. There
is: what we actually need is lexicalParent
. We could correctly write
the previous example as:
|
canTravelerPass(traveler) { return chair.isIn(lexicalParent); }
|
explainTravelBarrier(traveler)
{ ”The lowest bough is just too high for
This is now equivalent to writing chair.isIn(clearing), but using
lexicalParent makes it immediately obvious what the intention is (as
opposed to having to check that chair refers to the enclosing object).
If you now recompile the game and try it again, you’ll find that it now
works after a fashion, but that it’s less than ideal. There are still
several things we should tidy up.
One thing we might like to do is to display a suitable message when the
player character climbs off the chair and up the tree, rather than just
have Heidi suddenly transported from the chair to the top. There is a
TravelMessage
class that allows a message to be displayed while
traveling, but we have already defined the connector to be a
OneWayRoomConnector
. Since, however, the TravelMessage
class
inherits all the methods we have already used, we can simply change
OneWayRoomConnector
to TravelMessage
and add the following
property:
travelDesc = ”By standing on the chair you just manage to reach the lowest
bough and haul yourself up the tree.<.p>“
The <.p>
just adds a paragraph break (blank line) after the message.
We could do the same with \b
, except that using <.p>
ensures we
don’t get multiple blank lines should the next message start with
<.p>
. The connector should now look like this:
|
canTravelerPass(traveler) { return chair.isIn(lexicalParent); }
|
explainTravelBarrier(traveler)
{ ”The lowest bough is just too high for you to reach. ”; }
travelDesc = ”By standing on the chair you just manage to
reach the lowest bough and haul yourself up the tree.<.p>”
}
Recompile the game and try it again. You will soon encounter another
small problem: the game now describes Heidi as using the chair to reach
the bough whether she’s standing on the chair or still on the ground
when the climb tree or up command is issued. You might think
this is okay on the grounds that if the player has made Heidi carry the
chair to the clearing he’s probably figured why, so we don’t need to
make Heidi explicitly stand on the chair before climbing the tree. But
there’s another problem: the chair is counted as being in the clearing
even if Heidi is still carrying it, so this code would allow Heidi to
use the chair to climb the tree while she’s still holding the chair. It
would be better, then, to check that Heidi is actually on the chair
(which she can’t be if she’s carrying it) before allowing her to climb.
We can achieve this by changing the canTravelerPass
method to
|
canTravelerPass(traveler) { return traveler.isIn(chair); }
|
We don’t then need to test as well that the chair is in the clearing,
since it already must be if Heidi is in the chair when this connector
is available to her.
Now everything should work reasonably well, except that the game will
now allow Heidi to climb the tree from the chair even if she’s only
sitting on the chair, and not standing on it. Again, we may not think
this matters very much in practice, but if we do, there are various ways
we could go about fixing it. Perhaps the simplest for now is to add the
condition that Heidi must be standing to the canTravelerPass() method,
which finally gives us:
clearing : OutdoorRoom ’Clearing’
”A tall sycamore tree stands in the middle of this clearing.
One path winds to the southwest, and another to the north.”
southwest = forest
up : TravelMessage
{ ->topOfTree
”By clinging on to the bough you manage to haul yourself
up the tree. ”
canTravelerPass(traveler)
{ return traveler.isIn(chair) && traveler.posture==standing; }
explainTravelBarrier(traveler)
{ ”The lowest bough is just out of reach. ”; }
}
north : FakeConnector {“You decide against going that way right
now. “}
;
If there were several objects that could be used for Heidi to stand on,
the canTravelerPass(traveler) method would only become a little more
complicated, e.g.:
|
canTravelerPass(traveler) {
|
|
return traveler.location is in (chair, crate, stepladder) &&
|
|
&& traveler.posture == standing;
|
Since an just out-of-reach bough is mentioned when the player tries to
get Heidi up the tree without the aid of the chair, we might want to add
that bough somewhere. The slight complication is that the bough will be
out of reach if Heidi is standing on the ground, but not if she’s
standing on the chair. The OutOfReach class handles this type of
situation; you could place the following code immediately after the
definition of the tree object:
++ bough : OutOfReach, Fixture ’lowest bough’ ’lowest bough’
”The lowest bough of the tree is just a bit too high up for you
to reach from the ground. ”
canObjReachContents(obj)
{
if(obj.posture == standing && obj.location == chair)
return true;
return inherited(obj);
}
cannotReachFromOutsideMsg(dest)
{
return ’The bough is just too far from the ground for you to reach. ’;
}
;
Admittedly this doesn’t allow Heidi to interact very interestingly with
the bough even if she is standing on the chair; she can touch the bough
which she can’t do from the ground, but that’s about it. It might be
more interesting if on the bough was concealed an object that Heidi
needed to find, but this is a step further than we need to go for this
game (but you’re welcome to experiment with it if you wish!).
One final point: using one object (like the chair here) to gain access
to a connector (like the way up the chair) is a fairly common situation
in Interactive Fiction. Often, however, it turns out to be a bit more
complicated to implement than the example we have worked throught here.
You don’t need to worry about that just yet - there’s plenty more to do
in this guide first - but if when you try to implement something similar
in your own game you find TADS 3 doing its best to frustrate you at
every turn, you’ll also find that help is at hand in the article on
‘Using NestedRooms as
Staging Locations’ in the Technical Manual.
Getting Started in TADS 3
[Main]
[Previous]
[Next]