Weightier Matters

Weighing it up

In the code used to decide whether or not to spring the trap when the player character takes the skull from the pedestal, we simply counted the number of items on the pedestal. This is fine in this game when there's only two portable objects in any case, but if this puzzle were part of a rather larger game with rather more portable objects in it, it might not be the best way to do things.

To illustrate the problem, try adding another small object to the cave location:

+ pebble: Thing 'tiny pebble; smooth round'
    "It's just a tiny, round pebble, almost perfectly smooth. "
;

Now compile and run the game again. You'll find that putting the tiny pebble on the pedestal works just as well in avoiding the trap as using the more substantial rock, but that doesn't seem to be right. Presumably the springing of the trap is in some way related to the weight resting on the pedestal, and while the rock might plausibly weigh the same as the skull, the pebble must be a lot lighter. Of course we could change the notifyRemove() method on the pedestal to check precisely which objects are on the pedestal when the rock or the skull is about to be removed, but that's rather an ad hoc solution, and the more objects there are in the game, the more complex and messy and ad hoc it will become, and the more chances there are of it not working properly. In principle the player should be able to use any combination of objects that weigh as much as or more than the skull and it should work to avoid springing the trap.

The best way to produce a fully general solution, then, is to have the springing of the trap dependent on the total weight of all the items resting on the pedestal. The adv3Lite library doesn't keep track of weight by default, but it's easy enough to modify Thing so it does. Here's a first attempt (which you could add right at the end of your existing code):

modify Thing
    weight = 1
    
    getWeightWithin()
    {
        local totalWeight = 0;
        for (local item in contents)
            totalWeight += item.weight;
        
        return totalWeight;
    }
;

This adds a new weight property to the Thing class and defines the default weight of every object as 1 (the unit can be whatever you imagine it to be, absolute precision isn't required here in any case; what we're really interested in is the rough relative weights of different objects within the limitations of integer arithmetic). For the sake of this exercise we'll leave the weight of the pebble as 1 (which it now inherits from the modified Thing, so we don't need to state it explicitly) and make the weight of both the rock and the skull 10. (Note: adv3Lite in fact comes with a Weight extension which we could use to implement this, but it will be more instructive for the sake of this tutorial to do it from scratch; besides adv3Lite extensions are beyond the scope of this tutorial).

++ goldSkull: Thing 'gold skull;; head'
    "It's the shape and size of a human skull, but made of solid gold; it must
    be worth a fortune. "
    
    weight = 10
;

+ smallRock: Thing 'small rock; round solid'
     "It's roughly round and looks pretty solid. "
    
    weight = 10
;

The next step is to arrange for the trap to be sprung when the total weight of objects resting on the pedestal is less than the weight of the skull:

+ pedestal: Fixture, Surface 'stone pedestal; smooth'
    "The smooth stone pedestal is artfully positioned to catch the sunlight at
    just this time of day. "
        
    notifyRemove(obj)
    {
        gMessageParams(obj);
        
        if(getWeightWithin() - obj.weight < goldSkull.weight)
        {
            "As {the subj obj} {leaves} the pedestal, a volley of poisonous
            arrows is shot from the walls! You try to dodge the arrows, but
            they take you by surprise!";  
            
            finishGameMsg(ftDeath, [finishOptionUndo]);  
        }
    }
;

Note how we specify this. The total weight of objects resting on top of the pedestal just before obj is removed is given by the new getWeightWithin() method we just defined on the modified Thing class. So the weight remaining on the pedestal after obj (i.e. the object that's being removed) is removed, is getWeightWithin() - obj.weight. The trap is sprung if this is less than the weight of the gold skull. Note that we haven't specified any numbers here (such as 10); we've kept the code as general as we can so that if, for example, we later changed our mind about the weight of the skull and decided it should be 20, everything would still work in a logical manner.

Another point to note is the introduction of the gMessageParams(obj) construct, and the subtle change of the start of the double-quoted string to "As {the subj obj} {leaves} the pedestal..." These two changes very much go together: {the subj obj} is a message parameter substitution, something we'll be seeing quite a bit of later. What this means is that when the game sees {the subj obj} (the message parameter) it substitutes the name of the object obj that is being removed (so this does much the same as the previous <<obj.theName>> that it replaces). So what's the point of it? You'll note that it's now followed by {leaves} rather than leaves. The subj in {the subj obj} tells the game that obj is the subject of the verb that follows. By marking leaves as {leaves} we indicate that this is the verb which has to agree with obj. This means that if obj were something with a plural name, 'the rocks', say, the game would output "As the rocks leave the pedestal" rather than "As the rocks leaves the pedestal", but if it's singular, 'the rock', say, we get "As the rock leaves the pedestal." This kind of subject-verb agreement is hard to achieve using the <<obj.theName>> method but easy to achieve using message parameter substitution.

To make it work, we have to identify obj as a parameter name as well as a variable name. That is what gMessageParams(obj) does for us: it tells the game, "treat obj as a parameter that refers to the same object that the obj variable does". At first this may seem very arcane, since, after all, obj and obj appear identical! But in fact they are different things in different contexts. In notifyRemove(obj) or obj.weight, obj is a variable name referencing an object; in {the subj obj} obj is simply a piece of text within a string. The difference is really that between obj (a variable name) and 'obj' (a piece of text). We need the gMessageParams(obj) statement to make the program recognize that the string 'obj' refers to the variable obj in the special context of a message parameter substitution like {the subj obj} within a string.

Don't worry if this isn't all entirely clear yet, as we'll be coming back to message parameter substitutions, and they'll make more sense as you get used to them. One last point to note about them right now, though, is that putting braces round a verb like {leaves} doesn't work for all verbs, but only those irregular verbs the library knows about (which is more about two hundred of the common ones). It is possible for your game to define additional verbs that can be understood in message parameter substitutions, but that's not a subject for now.

We're still not done yet, however, since there is a logical error in the way we've defined the getWeightWithin() method, as you may have already spotted. To demonstrate what it is, we'll introduce another object into the game, a knapsack worn by the player character. We can define it like this:

++ knapsack: Wearable, Container 'knapsack; trusty old worn; bag sack'
    "Your trusty old knapsack may be getting a bit worn, but it's accompanied
    you on so many adventures you wouldn't be without it. "
    
    wornBy = me
;

We could have defined the knapsack as a Thing with isWearable = true and contType = In, but we're sticking to our decision to use the library classes for this kind of thing. Here Wearable contributes isWearable = true and Container contributes contType = In. We still have to supply wornBy = me, however, since a Wearable object isn't necessarily worn; it might also be carried. In his case we want it to start out worn by the player character, so we set wornBy = me. We also need to ensure that the definition of the knapsack object follows directly after the definition of the me object in our code, so that the knapsack forms part of the player character's contents.

Try recompiling the game and playing it with the knapsack added. Go into the cave, put the rock into the knapsack, and then put the knapsack onto the pedestal. Now take the gold skull. You'll find the trap is still sprung, even though the knapsack with the rock in it ought to weigh enough to have prevented it. You've probably already worked out what the problem is: getWeightWithin() calculates the total weight of an object's contents by adding up the weights of each item in its contents list — ignoring any further items they may happen to contain in turn. What it should do is to add up the weight of every items in its contents, plus the weight of the items they contain, and so on all the way down the chain, plus the weight of the original object. Just a couple of small changes should fix it:

modify Thing
    weight = 1
    
    getWeightWithin()
    {
        local totalWeight = 0;
        for (local item in contents)
            totalWeight += item.getWeight;
        
        return totalWeight;
    }
    
    getWeight = weight + getWeightWithin()
;

At first sight this may look a little odd, since getWeightWithin() now calls itself as part of its calculation. In fact this recursion, as its called, is a perfectly legal and often useful programming technique, provided it's handled with care. For example, if there was any danger that getWeightWithin() might call itself on the same object that was doing the calling (for example, that knapsack.getWeightWithin() might call knapsack.getWeightWithin()) then we would indeed be in trouble, for our code could then get itself into an infinite loop from which it would never emerge and our game would hang. The golden rule of recursion is to ensure that there's always some way to end it, but here we're safe since each recursive call goes a step down the containment tree, and however far the containment tree goes, it can never be infinite (unless we've done something really weird in our game code) so we can be sure it'll bottom out somewhere (specifically, when getWeightWithin() is called on an item like the rock that has no contents, the for loop has nothing to do, so there's no further recursion). Note that we've also added a getWeight property, which is the total weight of a Thing (the weight of the object itself plus the weight of everything that it contains) and that we've defined this property as an expression, which is perfectly legal in TADS 3.

By the same token we need to ensure that the weight of any item we remove from the pedestal includes the weight of its contents, so we want to use the getWeight property of the object being removed rather than just its weight property:

+ pedestal: Fixture, Surface 'stone pedestal; smooth'
    "The smooth stone pedestal is artfully positioned to catch the sunlight at
    just this time of day. "
        
    notifyRemove(obj)
    {
        gMessageParams(obj);
        
        if(getWeightWithin() - obj.getWeight < goldSkull.weight)
        {
            "As {the subj obj} {leaves} the pedestal, a volley of poisonous
            arrows is shot from the walls! You try to dodge the arrows, but
            they take you by surprise!";  
            
            finishGameMsg(ftDeath, [finishOptionUndo]);  
        }
    }
;

If you compile and run the game again, you should find it runs as expected.

Throwing Stones

Although we've designed the game so that the way to get the gold skull is to place the rock on the pedestal first, that's not the only solution that might occur to the player. Another way to use the rock might be to throw it at the skull to knock it off the pedestal while the player character stands far enough away to avoid the volley of deadly arrows. Whether or not we allow this solution, a well-designed game ought to anticipate the attempt and provide an appropriate response. The library's default response to throwing something at something else is to have the missile strike the target and fall to the ground without affecting anything else. That's probably an appropriate response to the player throwing the pebble at the skull, but if the rock is thrown at the skull, it really should dislodge it from the pedestal. Let's say that the skull will be knocked off the pedestal if the object thrown at it is at least half its weight, then if we want the rock-throwing solution to succeed, we might write:

++ goldSkull: Thing 'gold skull;; head'
    "It's the shape and size of a human skull, but made of solid gold; it must
    be worth a fortune. "
    
    weight = 10
    
    iobjFor(ThrowAt)
    {
        action()
        {
            if(location != pedestal || gDobj.getWeight() < weight / 2)
                inherited;
            else
            {
                moveInto(cave);
                gDobj.moveInto(cave);
                "{The subj dobj} {strikes} the gold skull, sending both objects
                tumbling to the floor. At the same time, a volley of poisonous
                arrows is shot from the walls! Fortunately you're standing just
                far enough away to avoid being hit. ";
            }
        }
    }
;

If, on the other hand, we don't want this solution to work (perhaps the cave isn't big enough to allow the player character to stand far enough away to avoid the trap), we can use the slightly simpler:

++ goldSkull: Thing 'gold skull;; head'
    "It's the shape and size of a human skull, but made of solid gold; it must
    be worth a fortune. "
    
    weight = 10
    
    iobjFor(ThrowAt)
    {
        action()
        {
            inherited;
            if(gDobj.getWeight() >= weight/2 && location == pedestal)
                actionMoveInto(cave);
        }
    }
;

Okay, so let's take a closer look at what's going on here. The command THROW ROCK AT SKULL consists of a verb (throw), two nouns (rock and skull) and a preposition (at). This kind of action is said to have two objects, the direct object, coming between the verb and the preposition (in this case the rock) and the indirect object immediately following the preposition (in this case the gold skull). When we write a section of code that starts iobjFor(ThrowAt) we're defining what happens to this object (in this case the gold skull) when it's the indirect object of a ThrowAt action (or, in plainer English, the target of a throw); remember, whenever you see 'iobj' think 'indirect object'.

There are several stages in the action processing cycle at which the gold skull could respond to being the target of a throw, but in the interests of keeping things as simple as possible we shan't go into them all now. The stage we're interested in is the action() stage, which means that the action has passed all the tests that might rule it out (for example trying to throw something that's too heavy to pick up, or throw something at a target that's too far away) and is now going ahead. In the action() part we simply define what the action does (in this case, what happens when something hits the gold skull).

One thing we need to know in writing this action part is what's just been thrown at the gold skull, in other words what the direct object of the ThrowAt command is. This is stored in the object property libGlobal.curAction.getDobj(), but since that's rather a lot of typing for something so commonly needed, the library lets us abbreviate it to gDobj. (Technically, gDobj is a macro masquerading as a pseudo-global variable, but you really don't need to worry about that right now unless you were desperately anxious about what sort of thing gDobj could possibly be in a language that doesn't have any global variables). You could similarly use gIobj to get at the indirect object of the command if you needed it (but here we know the indirect object must be the gold skull, since that's where we're defining this iobjFor() code).

Remember that the inherited keyword means "at this point do what the method we're inheriting from would have done"; in this context it thus means "carry out the default ThrowAt handling" (which is to report that the missile has struck the target and landed on the floor, and to move the missile to the enclosing room).

Armed with those pieces of information we're now in a position to examine how each version of the ThrowAt handling works. In the first version above, which makes throwing the rock at the skull a successful solution to the puzzle, we first check whether it's the case either that the skull isn't on the pedestal any more, or that the missile being thrown at it (gDobj, the direct object of the command) is less than half the weight of the skull (since this is being defined on the goldSkull object, weight unqualified by any other object name refers to the weight of the skull). If either of these two conditions is met (the skull is no longer on the pedestal or the missile being thrown at it is too light to dislodge it), we carry out the inherited handling (the missile strikes the skull and falls to the ground). Otherwise (if the missile is heavy enough and the skull is on the pedestal), we move both the skull and the missile to the cave room (representing the fact that they both end up on the floor) and report what happens. Note the use of the message parameter substitutions "{The subj dobj} {strikes}" at the begining of this report. If it's the rock that's thrown this becomes "The small rock strikes" before it's displayed to the player, but because it's been written with message parameter substitutions it will name the correct object whatever the player throws at the skull.

You may be wondering about the difference between gDobj and {The subj dobj}, both of which have been used to refer to the direct object of this command in this short piece of code. The difference is that {The subj dobj} simply produces some text, the name of the direct object, and marks it as the subject of the verb that's about to follow (that's what the subj part is for), while gDobj provides a reference to the object itself. We need to move the object to the floor, not it's name; conversely we need to display the object's name, not the object. More concretely, gDobj refers to the smallRock object while "{The subj dobj}" expands into the string 'The small rock'.

The second example, where we don't allow throwing the rock as a successful solution to the puzzle, is a bit simpler. We start by carrying out the inherited handling in any case, since whatever happens we want to report the missile striking the gold skull and then falling to the floor. Then, if it's the case that the skull is on the pedestal and the missile (i.e. the direct object) is at least half the weight of the skull, we move the goldSkull object to the cave (here representing the floor of the cave). Since we here do so using actionMoveInto(cave) rather than just moveInto(cave) as we did in the first version, the removal of the skull from the pedestal will trigger the pedestal's notifyRemove(obj) method which, you may recall, reports the launching of the poison arrows and the death of the player character. Conversely, we used moveInto() rather than actionMoveInto() in the first version to avoid precisely this happening.

Don't worry if you're thinking there's rather a lot to take in here, or you're wondering how the heck you'd ever be able to work all this out for yourself. If it's all new to you it's bound to seem a bit odd and confusing at first. As you work through this tutorial this kind of thing should gradually become more familiar to you and you'll gradually gain confidence with it.