[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 to the game, and one that will turn out to be sufficiently complex to introduce quite a few new elements.

Let us suppose that in order to climb the tree, Heidi first needs to fetch a chair and stand on it. The first thing to do is to prevent Heidi from being able to climb the tree from the ground. To achieve this we need to change the definition of clearing.up. For this purpose 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 should produce the same message. 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 trickier part is creating a chair object that will enable Heidi to climb the tree. The first thing is to create a suitable initial location for it; the most likely place you'd find a chair is probably inside the cottage. For the moment we'll keep things as simple as possible; define the inside of the cottage as follows:
 
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 effect of this is that if the player types east Heidi will end up where she would have done if the player had typed out, but that east won't be listed as a separate exit (either in the status line or in response to an exits command). The reason for doing it this way is that if the exit lister showed both east and out, it would look to the player as if there were two separate exits, instead of two synonyms for the same exit. The reason for providing the synonym is that since the forest lies to the east of the player's starting position, the cottage most naturally lies to the west, so that to return from the inside of the cottage to the starting position outside will most likely be understood as a move back eastwards.

Note 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:
 
in = insideCottage  
west asExit(in)  
 
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(TravelVia, outsideCottage.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 ->(outsideCottage.in) '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. "      
;
 
Again, this would work with ->insideCottage as well as ->(outsideCottage.in); the advantage of doing it this way is that if we change what outsideCottage.in points to (as we shall when we come to add a cottage door) we only have to change it in one place.

An alternative to using the
->connector syntax would have been to define the connector property explicitly with
 
connector = (outsideCottage.in)  
 
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. Moreover, when Heidi is standing on the chair, she'll still be in the location the chair is in. The class of object we need is thus basically what TADS 3 calls a NestedRoom. The TADS 3 library includes a subclass of
NestedRoom called Chair that does just the job (by default a Chair can be sat on and stood on but not lain upon):
 
+ chair : Chair 'wooden chair' 'wooden chair'
  "It's a plain wooden chair. "
;
 
There's one fairly easy way we can improve the behaviour of this chair before we even think about using it to climb the tree. When Heidi, the player character, 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:
 
up : OneWayRoomConnector  
  {  
    destination = topOfTree  
    canTravelerPass(traveler) { return chair.isIn(clearing); }  
           explainTravelBarrier(traveler) 
            { "The lowest bough is just too high for 
   you to reach. "; }  
  }  
 
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 print a suitable general-purpose 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:
 
up : OneWayRoomConnector  
  {  
    destination = topOfTree  
    canTravelerPass(traveler) { return chair.isIn(self); }  
           explainTravelBarrier(traveler) 
            { "The lowest bough is just too high for 
   you to reach. "; }  
  }  
 
Here we have simply changed the explicit reference to clearing to 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 (it would perhaps be even easier to have code on the OneWayRoomConnector refer to other properties of clearing without qualifying them with an object name, forgetting that clearing and the OneWayRoomConnector are two different objects), and raises the question whether there is a right way for a nested object like the anonymous OneWayRoomConnector in this example to refer to the object to one of whose properties it is attached. There is: what we actually need is lexicalParent. Thus we could correctly write the previous example as:
 
up : OneWayRoomConnector  
  {  
    destination = topOfTree  
    canTravelerPass(traveler) { return chair.isIn(lexicalParent); }  
           explainTravelBarrier(traveler) 
            { "The lowest bough is just too high for 
   you to reach. "; }  
  }  
 
 
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 connector should now look like this:  
 
up : TravelMessage  
  {  
    destination = topOfTree  
    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 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 first, since this step can be taken for granted. Maybe such an argument holds some water, but it is potentially rather leaky, since the chair is still in the clearing even if Heidi is still carrying it, and this code would allow Heidi to use the chair to climb the tree while she's still holding the chair, which surely can't be right. 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 = forestPath
;
 
 
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 really 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]