Custom Preconditions
by Eric Eve
Adapted for Adv3Lite 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: Thing 'piece of paper' dobjFor(WriteLiteralOn) { preCond = [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() { "{I} {give} {the dobj} 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:
- Look through the library file precond.t (perhaps with the aid of the Library Reference Manual) until you find a precondition that does something reasonably similar to what you want your new precondition to do.
- Copy the code for the library precondition (which will be an object of type PreCondition, or possibly one of its subclasses) and paste it into your own code at the point at which you want to define your own custom precondition.
- Change the name of the PreCondition object you have just copied to the name you want for your new custom precondition (e.g. at the point where you have just copied the
objHeld
precondition into your own code you might change its name toobjNotHeld
). - Then work through the copied PreCondition object in your own code, making all the other changes needed to ensure it does what you want.
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 verifyPreCondition(obj) { /* * If the object is fixed in place it can't be picked up, so there's * no point in trying. BUT we also have to check for the case that the * object is directly in the player (perhaps because a body part). */ if(obj.isFixed && !obj.isDirectlyIn(gActor)) illogical(obj.cannotTakeMsg); /* * If the actor isn't carrying the object it's slightly less likely to * be the one the player means. */ if(!obj.isIn(gActor)) logicalRank(90); } checkPreCondition(obj, allowImplicit) { /* * if the object is already held, we're already done. */ if (obj == nil || obj.isDirectlyIn(gActor)) return true; /* * If we're allowed to attempt an implicit action, try taking obj * implicitly and see if we succeed. */ if(allowImplicit) { /* * Try taking obj implicitly and note if we were allowed to make * the attempt. */ local tried = tryImplicitAction(Take, obj); /* * If obj is now held by the actor return true to signal that this * precondition has now been met. */ if(obj.isDirectlyIn(gActor)) return true; /* * Otherwise, if we tried but failed to take obj, return nil to * signal that this precondition can't be met (so the main action * cannot proceed). The attempt to take obj will have explained * why it failed, so there's no need for any further explanation * here. */ if(tried) return nil; } /* * If we reach here obj isn't being held by the actor and we weren't * allowed to try to take it; display a message explaining the * problem. */ gMessageParams(obj); DMsg(need to hold, '{I} need{s/ed} to be holding {the obj} to do that. '); /* Then return nil to indicate that the precondition hasn't been met. */ return nil; } ;
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.isDirectlyIn(gActor)) return true;
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 true 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
true and do no more if the object is not already in the player
(we almost certainly want our objNotHeld PreCondition to ensure not
only that the player isn't directly holding the object but that it's
not indirectly contained by the pc either):
/* * if the object is already not in the actor, we're already done. */ if (obj == nil || !obj.isIn(gActor)) return true;
The next part of the code in the original objHeld
precondition then tries to carry out an implicit TAKE
command and checks whether this has succeeded, provided that implicit
actions are allowed at this stage of the proceedings:
/* * If we're allowed to attempt an implicit action, try taking obj * implicitly and see if we succeed. */ if(allowImplicit) { /* * Try taking obj implicitly and note if we were allowed to make * the attempt. */ local tried = tryImplicitAction(Take, obj); /* * If obj is now held by the actor return true to signal that this * precondition has now been met. */ if(obj.isDirectlyIn(gActor)) return true; /* * Otherwise, if we tried but failed to take obj, return nil to * signal that this precondition can't be met (so the main action * cannot proceed). The attempt to take obj will have explained * why it failed, so there's no need for any further explanation * here. */ if(tried) return nil; }
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 take the object via
a call to tryImplicitAction(Take, obj)
which will attempt to take the
object attempt to take the object. 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.
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:
/* * If we're allowed to attempt an implicit action, try taking obj * implicitly and see if we succeed. */ if(allowImplicit) { /* * Try dropping obj implicitly and note if we were allowed to make * the attempt. */ local tried = tryImplicitAction(Drop, obj); /* * If obj is now not in the actor return true to signal that this * precondition has now been met. */ if(!obj.isIn(gActor)) return true; /* * Otherwise, if we tried but failed to drop obj, return nil to * signal that this precondition can't be met (so the main action * cannot proceed). The attempt to drop obj will have explained * why it failed, so there's no need for any further explanation * here. */ if(tried) return nil; }
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 'obj' a temporary message parameter
that refers to obj so that we can construct the failure message.
/* * If we reach here obj isn't being held by the actor and we weren't * allowed to try to take it; display a message explaining the * problem. */ gMessageParams(obj); DMsg(need to hold, '{I} need{s/ed} to be holding {the obj} to do that. '); /* Then return nil to indicate that the precondition hasn't been met. */ return nil;
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:
/* * If we reach here obj is still in the actor and we weren't * allowed to try to drop it; display a message explaining the * problem. */ gMessageParams(obj); DMsg(need to not hold, '{I} need{s/ed} to be not holding {the obj} to do that. '); /* Then return nil to indicate that the precondition hasn't been met. */ return nil; }
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):
verifyPreCondition(obj) { verifyPreCondition(obj) { /* * If the object is fixed in place it can't be picked up, so there's * no point in trying. BUT we also have to check for the case that the * object is directly in the player (perhaps because a body part). */ if(obj.isFixed && !obj.isDirectlyIn(gActor)) illogical(obj.cannotTakeMsg); /* * If the actor isn't carrying the object it's slightly less likely to * be the one the player means. */ if(!obj.isIn(gActor)) logicalRank(90); } } ;
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 fixed in place it can't be moved, so there's * no point in trying to drop it. */ if(obj.isFixed) illogical(obj.cannotTakeMsg); /* * If the actor is carrying the object it's slightly less likely to * be the one the player means. */ if(obj.isIn(gActor)) logicalRank(90); } ;
With these changes, our new objNotHeld
precondition
becomes:
objNotHeld: PreCondition verifyPreCondition(obj) { /* * If the object is fixed in place it can't be moved, so there's * no point in trying. */ if(obj.isFixed) illogical(obj.cannotTakeMsg); /* * If the actor is carrying the object it's slightly less likely to * be the one the player means. */ if(obj.isIn(gActor)) logicalRank(90); } checkPreCondition(obj, allowImplicit) { /* * if the object is already not in the actor, we're already done. */ if (obj == nil || !obj.isIn(gActor)) return true; /* * If we're allowed to attempt an implicit action, try taking obj * implicitly and see if we succeed. */ if(allowImplicit) { /* * Try taking obj implicitly and note if we were allowed to make * the attempt. */ local tried = tryImplicitAction(Drop, obj); /* * If obj is now not in the actor return true to signal that this * precondition has now been met. */ if(!obj.isIn(gActor)) return true; /* * Otherwise, if we tried but failed to drop obj, return nil to * signal that this precondition can't be met (so the main action * cannot proceed). The attempt to drop obj will have explained * why it failed, so there's no need for any further explanation * here. */ if(tried) return nil; } /* * If we reach here obj is still in the actor and we weren't * allowed to try to drop it; display a message explaining the * problem. */ gMessageParams(obj); DMsg(need to not hold, '{I} need{s/ed} to be not holding {the obj} to do that. '); /* Then return nil to indicate that the precondition hasn't been met. */ return nil; } ; ;