Custom Preconditions

by Eric Eve

Preconditions provide a powerful and reasonably easy-to-use way of enforcing conditions when certain actions are performed on or with certain objects: for example, the Player Character must be able to touch the door in order to open it, and must be holding the key in order to unlock anything with it. Preconditions can be particularly helpful since they can not only enforce conditions (such as that the key must be held before it can be used), but can also bring them about through implicit actions: if the key to the door is lying in plain sight it's simply annoying for the player to be told "You must be holding the key before you can unlock anything with it"; it's much neater if the parser recognizes what the player intends and makes the Player Character first pick up the key automatically so that the unlocking can proceed.

The standard library already defines many commonly useful preconditions, and applies them appropriately to a large range of actions (for further details, see the Preconditions section of the Technical Manual Article on Action Results). Sometimes, though, it can be useful to define preconditions of your own in your own game. This article will show you how.

New Object Preconditions

We'll go on to discuss how to define a brand new precondition below, but we'll start by pointing out that this isn't always necessary. Sometimes we can get the effect we want by constructing a custom precondition with new ObjectPreCondition together with an existing precondition and the particular object we want it to apply to.

For example, suppose we have defined a new WRITE ON action, of the form WRITE ZANZIBAR ON PAPER; we don't want the Player Character to be able to write on the paper unless he's holding a pen, but we don't want to force the player to use the awkward syntax WRITE ZANZIBAR ON PAPER WITH PEN. One way to deal with with would be to make holding the pen a precondition of writing anything on the piece of paper. This would enforce the availability of a pen as a condition of writing, but it would also result in the Player Character picking up the pen as an implicit action if the pen is to hand but not held when the player issues a command like WRITE ZANZIBAR ON PAPER:

>write Zanzibar on paper
(first picking up the pen)
You write "Zanzibar" on the piece of paper.

Since the implicit action (picking up the pen), and hence the precondition being enforced, involves an object (the pen) not explicitly mentioned in the player's command, you might think you'd have to write a special penHeld precondition to deal with it. But in fact there's no need for this, since we can simply use the existing objHeld precondition and apply it to a different object via the new ObjectPreCondition construct. This is typically used thus:

    new ObjectPreCondition(obj, cond)

Where obj is the object to which we want the cond precondition to apply.

So, for example, assuming we've already defined the a new WriteLiteralOnAction, if on the piece of paper we defined:

   paperPiece: Readable 'piece/paper' 'piece of paper'
      dobjFor(WriteLiteralOn)
      {
	      preCond = [touchObj, objHeld]
      }
   ;

Then we'd be making holding the piece of paper a precondition of writing on it (which we may or may not want), because a precondition normally applies to the object on which it's listed for a particular action. But since in this case what we really want is to enforce the precondition that the pen must be held in order to write on the paper, we can use the new ObjectPreCondition construct to 'redirect' the objHeld precondition from the paper to the pen:

+ paperPiece: Readable 'piece/paper' 'piece of paper'
    dobjFor(WriteLiteralOn)
    {
        preCond = [touchObj, new ObjectPreCondition(pen, objHeld)]
        verify() {}
        action()
        {
            "You write <<gLiteral>> on the piece of paper. ";
            writing += ('\n'+ gLiteral);
        }
    }
    writing = ''
    readDesc = "On the paper is written: <<writing>>"
;

The moral of this example is that before writing a completely new precondition, you should first ask yourself whether you can construct it using new ObjectPreCondition in conjunction with an existing precondition: this is generally possible if all you want to do is to apply an existing kind of precondition to an object that is not explicitly involved in the action (i.e. an object that is not the direct or indirect object of the current command). It may be, of course, that what you want to do can't be handled that way; for example, if several writing implements were available in your game and the Player Character just had to be holding one of them in order to be able to write on things, then you'd need a different approach. In the next section we'll see how to write a completely new precondition.

Completely New Preconditions

The standard libary defines a good selection of preconditions to cover the most usual cases, but it may be that you'll come up against a situation that isn't covered by any of the existing preconditions. For example, suppose that your game contains one or more balls that can only be kicked when the actor is not holding them, then ideally you'd like an objNotHeld you could use like this:

class Ball: Thing 
    dobjFor(Attack)
    {
	    preCond = [touchObj, objNotHeld]
	    verify() { }
	    action()
	    {
		    "{You/he} give{s} {the dobj/him} a good kick. ";
	    }
    }
;

Ideally, this would make the Player Character drop a ball he was holding in response to KICK BALL (or HIT BALL or ATTACK BALL) before carrying out the kicking action. Additionally, if the Player Character is carrying a red ball and a blue ball is lying on the ground, it should make KICK BALL prefer the ball that is not being carried (i.e. the blue ball). But since the library does not define an objNotHeld precondition, we'd need to define one for ourselves.

There are several things a PreCondition object needs to do if it is to function correctly, but rather than describe them in the abstract, it will far easier simply to start from an existing library precondition that's reasonably similar to what we want and then adapt it. This will ensure that we follow the coding pattern of the standard library preconditions, which should in turn ensure that our new precondition does what it should. Indeed, this is probably the best procedure to follow whenever you want to create a new precondition:

As just stated, the fourth stage above may not look particularly helpful, but this will all become much clearer if we work through an example. Suppose that we indeed wish to create an objNotHeld precondition. Since this seems to be the reverse of the existing objHeld precondition, that might be the best place to start. So the first step is to locate the objHeld precondition in preCond.t (perhaps with the help of the Library Reference Manual), and the second step is to copy and paste that object definition into our own code:

objHeld: PreCondition
    checkPreCondition(obj, allowImplicit)
    {
        /* if the object is already held, there's nothing we need to do */
        if (obj == nil || obj.meetsObjHeld(gActor))
            return nil;
        
        /* the object isn't being held - try an implicit 'take' command */
        if (allowImplicit && obj.tryHolding())
        {
            /* 
             *   we successfully executed the command; check to make sure
             *   it worked, and if not, abort the command without further
             *   comment (if the command failed, presumably the command
             *   showed an explanation as to why) 
             */
            if (!obj.meetsObjHeld(gActor))
                exit;

            /* tell the caller we executed an implicit command */
            return true;
        }

        /* it's not held and we can't take it - fail */
        reportFailure(&mustBeHoldingMsg, obj);

        /* make it the pronoun */
        gActor.setPronounObj(obj);

        /* abort the command */
        exit;
    }

    /* lower the likelihood rating for anything not being held */
    verifyPreCondition(obj)
    {
        /* if the object isn't being held, reduce its likelihood rating */
        if (obj != nil && !obj.meetsObjHeld(gActor))
            logicalRankOrd(80, 'implied take', 150);
    }
;

We next carry out Step 3 and change the name to the new name we want (if we don't we'll get a duplicate object definition compiler error when we try to compile, and the new name we want to use won't be recognized, so it's best to do this straight away before we forget):

objNotHeld: PreCondition

Well, that's the first three steps out of the way, now we just have the final, most complex step. To make this clearer, we'll break it down into small sub-steps. The existing definition starts with:

    checkPreCondition(obj, allowImplicit)
    {
        /* if the object is already held, there's nothing we need to do */
        if (obj == nil || obj.meetsObjHeld(gActor))
            return nil;

The checkPreCondition method is called at the point when implicit actions may be carried out to meet the condition we want to impose. The obj parameter is the object whose condition we're testing and which any implicit action would normally be carried out on. The allowImplicit parameter defines whether or not an implicit action may be attempted; during command execution the verify and checkPreCondition routines may be called more than once, but implicit actions are only allowed on the first pass (to prevent an infinite loop should the execution of an implicit command then bring about the need to carry out another implicit command that undoes the result of the first).

The first part of this method checks to see if an implicit action is actually necessary. In the original objHeld there's no need to do anything if the actor is already holding the object in question, so the method just returns nil to tell its caller that it didn't need to do anything. In our new objNotHeld precondition we want to apply precisely the opposite test: we return nil and do no more if the object is not already held:

            
     /* if the object is already not held, there's nothing we need to do */
        if (obj == nil || !obj.meetsObjHeld(gActor))
            return nil;

The next part of the code in the original objHeld precondition then tries to carry out an implicit TAKE or TAKE FROM command and checks whether this has succeeded, provided that implicit actions are allowed at this stage of the proceedings:

      /* the object isn't being held - try an implicit 'take' command */
        if (allowImplicit && obj.tryHolding())
        {
            /* 
             *   we successfully executed the command; check to make sure
             *   it worked, and if not, abort the command without further
             *   comment (if the command failed, presumably the command
             *   showed an explanation as to why) 
             */
            if (!obj.meetsObjHeld(gActor))
                exit;

            /* tell the caller we executed an implicit command */
            return true;
        }

The slight oddity here is the test if (allowImplicit && obj.tryHolding()). If allowImplicit is nil then the test fails straight away and the entire code block is bypassed. If allowImplicit is true, then we try to hold the object via a call to obj.tryHolding() which will attempt to take the object out of its container if the actor is carrying the container, or will otherwise simply attempt to take the object. Either way the attempt will be via a call to tryImplicitAction and tryHolding() will return the result of that call (true or nil); if the implicit action is attempted then tryImplicitAction will return true, but if it's not (e.g. because one of the objects involved is not in scope for the actor) it will return nil.

If no implicit action has been attempted then we can skip the rest of the code block. If, on the other hand, it has been attempted then we need to check whether it has been successful (maybe the actor tried to pick up the anvil but it proved to be too heavy). If the implicit action did not achieve the desired result (in this case, if the object does not end up being held by the actor) then we assume that the failed attempt has already reported the reason for failure and simply exit the action. If on the the other hand the implicit action succeeded we return true to tell the caller that we carried out an implicit action that succeeded in meeting the required condition.

Our new objNotHeld precondition needs to follow the same coding pattern and much of the same logic, reversing it in just a couple of places: we need to try dropping the object instead of taking it, and we need to check that the object ends up not held, not that it ends up held. The revised code becomes:

       /* the object is being held - try an implicit 'drop' command */
        if (allowImplicit && tryImplicitAction(Drop, obj))
        {
            /* 
             *   we successfully executed the command; check to make sure
             *   it worked, and if not, abort the command without further
             *   comment (if the command failed, presumably the command
             *   showed an explanation as to why) 
             */
            if (obj.meetsObjHeld(gActor))
                exit;

            /* tell the caller we executed an implicit command */
            return true;
        }

The final part of the method has to deal with the case in which no implicit action was attempted, either because we're in the second pass and allowImplicit is nil or because the implicit action could not be attempted. In this case we need to display a message explaining why the main action cannot go ahead and abort the command. Just before aborting the command we make the appropriate pronoun (most usually 'it') refer to the object in question (so that if the player sees a message like "You need to be holding the Golden Gas-Guzzler of Garak before you can do that" and responds with TAKE IT, the parser will understand that "it" refers to the Golden Gas-Guzzler of Garak):

        /* it's not held and we can't take it - fail */
        reportFailure(&mustBeHoldingMsg, obj);

        /* make it the pronoun */
        gActor.setPronounObj(obj);

        /* abort the command */
        exit;

We can use almost precisely the same code for our new objNotHeld precondition, except that we need to supply a custom failure message appropriate to the different situation:

       /* it's held and we can't drop it - fail */
        
        gMessageParams(obj);
        reportFailure('{You/he} can\'t do that while {you\'re/he\'s} holding 
            {the obj/him}. ');

        /* make it the pronoun */
        gActor.setPronounObj(obj);

        /* abort the command */
        exit;
    }  

That completes the checkPreCondition method, but there's also the verifyPreCondition method, which we also need to adapt. On the objHeld precondition this makes it less likely that the command in question applies to an object that is not currently held by the actor (e.g., if EAT has a objHeld precondition attached to its direct object, then EAT CAKE will choose the chocolate cake held by the actor in preference to the fruit cake sitting on the table):

    /* lower the likelihood rating for anything not being held */
    verifyPreCondition(obj)
    {
        /* if the object isn't being held, reduce its likelihood rating */
        if (obj != nil && !obj.meetsObjHeld(gActor))
            logicalRankOrd(80, 'implied take', 150);
    }
;

For our new objNotHeld precondition we simply need to reverse the logic so the likelihood rating is reduced for an object that is being held (so, e.g., KICK BALL prefers the blue ball on the ground to the red ball being carried):

    /* lower the likelihood rating for anything being held */
    verifyPreCondition(obj)
    {
        /* if the object is being held, reduce its likelihood rating */
        if (obj != nil && obj.meetsObjHeld(gActor))
            logicalRankOrd(80, 'implied drop', 150);
    }
;

With these changes, our new objNotHeld precondition becomes:

objNotHeld: PreCondition checkPreCondition(obj, allowImplicit) { /* if the object is already not held, there's nothing we need to do */ if (obj == nil || !obj.meetsObjHeld(gActor)) return nil; /* the object is being held - try an implicit 'drop' command */ if (allowImplicit && tryImplicitAction(Drop, obj)) { /* * we successfully executed the command; check to make sure * it worked, and if not, abort the command without further * comment (if the command failed, presumably the command * showed an explanation as to why) */ if (obj.meetsObjHeld(gActor)) exit; /* tell the caller we executed an implicit command */ return true; } /* it's held and we can't drop it - fail */ gMessageParams(obj); reportFailure('{You/he} can\'t do that while {you\'re/he\'s} holding {the obj/him}. '); /* make it the pronoun */ gActor.setPronounObj(obj); /* abort the command */ exit; } /* lower the likelihood rating for anything being held */ verifyPreCondition(obj) { /* if the object is being held, reduce its likelihood rating */ if (obj != nil && obj.meetsObjHeld(gActor)) logicalRankOrd(80, 'implied drop', 150); } ;