Table of Contents |
Extensions >
Rules
Rules
Overview
The purpose of the rules.t extension is to allow game code to define Rules and RuleBooks. Rules are associated with RuleBooks, which may be invoked at any point in game code to carry out actions and/or return values to their caller.
In brief, calling the follow() method of a RuleBook causes it to
call the follow()
method of each of its
associated Rules in descending order of precedence (ignoring those Rules
that don’t match their conditions) until either one of these Rules stops
the sequence or there are no more Rules to call. The value returned to
the RuleBook by the last-executed Rule’s
follow()
method is then returned to the caller
of the RuleBook’s follow()
method.
Doers and TopicEntries (especially Doers) are standard classes in the main adv3Lite library that act a little like Rules, but the Rules extension allows you to generalize this kind of functionality into all sorts of other situations. It is most likely to be most useful in situations where a tangle of nested if statements and switch statements would otherwise be needed to code complex interactions, but you are, of course, free to use rules wherever you wish.
This extension does not define any actual Rules or Rulebooks, but simply the Rule and RuleBook classes that allow rules and rulebooks to be defined. For some predefined rules and rulebooks that work with this extension and provide rule-based access to certain aspects of the action-processing and turn cycles, see the related sysrules extension.
New Classes, Methods and Properties
In addition to a number of objects, properties and methods intended purely for internal use, this extension defines the following new classes, methods and properties:
- Classes: Rule and RuleBook.
- Properties/Methods on Rule:
follow()
,priority
,execAfter
,execBefore
,isActive
,activate()
,deactivate()
,addTo(rb)
,moveTo(rb)
,removeFrom(rb)
,stopValue
,where
,when
,who
,during
,action
,dobj
,iobj
,aobj
,matchObj
, andpresent
. - Properties/Methods on RuleBook:
follow()
,actor
,contVal
,defaultVal
, andinitBook()
.
Usage
Include the rules.t file after the library files but before your game source files.
Defining Rules and Rulebooks
At its simplest a RuleBook can be defined simply by giving it a name and making it of the RuleBook class:
jumpRules: RuleBook
;
Unless you need to refer to them in some other part of your code, rules can usually be defined as anonymous objects located in the RuleBook to which they belong by using the + property. The one method you must define on a Rule is its follow() method; for example:
+ Rule
where = room
follow()
{
"You jump pointlessly. ";
}
;
+ Rule
where = nextRoom
follow()
{
"You jump fruitfully. ";
}
;
+ Rule
follow()
{
"You jump energetically. ";
}
;
In this case we could have defined these rules even more succinctly as:
+ Rule
where = room
follow = "You jump pointlessly. "
;
+ Rule
where = nextRoom
follow = "You jump fruitfully. "
;
+ Rule
follow = "You jump energetically. "
;
We can do that here because in this RuleBook, the
follow()
methods take no arguments. You can,
however, define as many arguments to the
follow()
method of a RuleBook as you like;
precisely the same arguments will then be passed to the
follow()
methods of any Rule it invokes, so
you must make sure that the argument lists match, or you’ll get a
run-time error. If in doubt you can use the variable argument list
notation to ensure that your Rules match any argument lists that are
passed to them, for instance:
+ Rule
where = room
follow([args])
{
"You jump pointlessly. ";
}
;
+ Rule
where = nextRoom
follow([args])
{
"You jump fruitfully. ";
}
;
+ Rule
follow([args])
{
"You jump energetically. ";
}
;
The advlite.h header file defines one template for use with Rules, which can be used to define their location and/or action properties. If both are present in the template the location property must be defined first, preceded by @. The action property is defined either with & or as a list, e.g.:
Rule @jumprules
// A Rule located in the jumprules rulebook
...
;
Rule &Jump
// A Rule matching the Jump action.
...
;
dropRule: Rule @treetopRules [Drop, Throw, ThrowDir]
/*
* A Rule located in the treetopRules rulebook and matching
* any one of the Drop, Throw or ThrowDir actions.
*/
...
;
Specifying Match Conditions on Rules
Rules are only useful for distinguishing what should happen under different circumstances. Most of the rules you define (with the possible exception of the odd catch-all rule) will need to specify the conditions under which they apply. This can be specified using one or more of the following properties:
- where: This can be the location either of the player character
(the default) or of the current actor, depending on the value of the
parent rulebook’s
actor
property (indeed it could be the location of whatever object is specified in the rulebook’s actor property, but the intention is that this should normally be eithergPlayerChar
orgActor
). Thewhere
property can be defined as a Room, a Region or a list of Rooms and/or Regions. - when: This can be any condition that must be true for the Rule to be matched. This is for use when none of the other means of speficifying rule conditions will do the job.
- who: The current actor (
gActor
): this can be specified as a single actor or as a list of actors. - during: A Scene or a list of Scenes, one of which must be currently happening for this Rule to be matched.
- action: An action of list of actions, of one which must be the
current action (
gAction
) for the Rule to be matched. - dobj: The direct object of the current action. This may be specified as an object, a class, or a list of objects and/or classes.
- iobj: The indirect object of the current action. This may be specified as an object, a class, or a list of objects and/or classes.
- aobj: The accessory object of the current action. This may be specified as an object, a class, or a list of objects and/or classes. Note that this property is only applicable when the TIAAction extension is included in your game.
- matchObj: an object or class or list of objects and/or classes one
of which much match the first argument passed to the rulebook’s
follow()
method. For example if the rulebook is invoked withfollow(helmetOfDoom)
the Rule would be matched if itsmatchObj
property washelmetOfDoom
orThing
or\[helmetOfDoom, pinkRabbit, cuddlyBear\]
(or anything else that included thehelmetOfDoom
). Note that while this condition is principally intended to be used with objects, it will in fact work with any kind of value, such as numbers and strings. - present: An object or a list of objects, one of which must be present to the actor. See below on what ‘present’ means.
Note that where any of the foregoing properties is specified as a list, the Rule will match if any of the items in the list matches (provided, of course, that any other conditions are also met).
For example, to write a Rule that matches when either Fred or Mary drops the crown jewels in either the hall or the lounge during the party scene when the queen can see them you might write:
+ Rule
follow()
{
"Oh dear! That was jolly embarrassing! Her Majesty does not look at all amused.<.p>";
}
where = [hall, lounge]
during = party
who = [fred, mary]
action = Drop
dobj = crownJewels
when = (queen.canSee(gActor))
;
(Assuming, that is, that we know that the player character will always be present in the same location, otherwise further conditions would need to be added).
Being Present
One of the properties we can define on a Rule is present, but what does this mean?
First, this property must specify objects rather than classes, except
for one special case. The present
property may
be defined as a single class (e.g. present =
Actor
), in which case the rule will be matched provided at least
one object of that class is in the same room as the actor (either
gPlayerChar
or
gActor
, depending on how the parent rulebook
defines this property).
Otherwise, if the present
property defines an
object or a list of objects, at least one of these objects must be
present in the same room as the actor for the rule to match.
There may, however, be occasions when ‘being in the same room as’ isn’t
quite what you want for a test of presence. For example, should the rule
match if the gem of ultimate destiny is in the same room as the actor
but hidden away inside a secret drawer the player character has yet to
open? Conversely, if the Elephant of Desire is clearly visible in the
neighbouring field, should it count as being present or not? To deal
with instances such as these you can define the
present
property so that at least one of the
object or objects it lists needs to be perceivable by actor via a
given sense, which you define as one of the property pointers
&canSee
, &canHear
,
&canSmell
,
&canReach
, e.g.:
present = [&canSee, gem]
present = {&canSmell, burntToast]
present = [&canHear, elephant, trumpet, sergeantMajor]
Rules’ Order of Precedence
We stated above that when RuleBooks are called, they run through their matching Rules in descending order of precedence. But how is that order of precedence determined?
Left to its own devices, the Rules extensions orders rules according to
their specificity (how specific their conditions are). The more
conditions are specified (out of when
,
where
, who
,
during
, action
,
dobj
, iobj
,
aobj
, matchObj
or
present
) the more specific the Rule is
considered to be. Also, a where
condition
referencing at least one Room is considered more specific that one that
one references Regions, and a dobj
,
iobj
, aobj
,
matchObj
or present
property that includes at least one object is considered more specific
that one that only refers to classes. Finally, if two rules appear to be
equally specific, precedence is given to the one defined later in your
source code (it may at first sight seem counter-intuitive to do it this
way round, but this is consistent with the way the library handles Doers
and AltTopics). Normally this default ordering will give you what you
want; a more specific rule will take precedence over a more general rule
without your having to worry about it. Note, however, that the library
has no way of judging how specific a when
condition is, which is why it’s normally better to use the other
condition properties when you can.
If for any reason you need to change the order of precedence there are a couple of ways you can do so. The first is to specify that the current rule must run either after or before one or more other specified rules (which would then need to be named so they can be referred to), by listing those other rules in the execAfter or execBefore properties respectively. For example, to ensure that your dangerousJumpRule executes after your dodgyJumpRule but before your fatalJumpRule you might write:
+ dangerousJumpRule: Rule
action = 'Jump'
follow = "I wouldn't jump here if I were you. "
execAfter = [dodgyJumpRule]
execBefore = [fatalJumpRule]
;
This will then override any other ordering the library would have made.
Note, however, it is then your responsibility to ensure that any use you
make of execAfter
and
execBefore
is consistent. If the
dodgyJumpRule
were specified in both the
execAfter and the execBefore properties of the same rule, for example,
this would be self-contradictory; in this case the library would resolve
the contradiction by ignoring the execBefore specification. On the other
were rule A to specify that it is to be run after rule B, while rule B
specified that it was to run after rule A, the outcome would be
unpredictable.
The second method of reordering rules is to override their priority
property. The default priority
is 100. The
higher the priority, the higher the precedence, so that a Rule with a
priority of 2000 will be run before a Rule with a
priority
of 1. Changing the
priority
to a very high or a very low value
can thus be a useful way of ensuring that a Rule runs towards the
beginning or the end of its RuleBook (or, provided you assign the
priority
numbers with sufficient care) that it
is either the very first or the very last Rule to be considered. Note
that any rearrangement of rule ordering carried out via the
execBefore
or
execAfter
properties will take precedence over
the setting of the priority
property, however.
Apart from that, Rules with the same priority
will be ordered according to their specificity.
A RuleBook re-sorts the rules it is considering each time its
follow()
method is called, so that if you
change the value of the priority
,
execBefore
or
execAfter
properties during the course of your
game, these changes will take effect the next time the corresponding
RuleBook is invoked. Normally, however, a rule’s specificity is only
calculated once, at preinit, so if you made any changes to a rule that
might change its specificity you would need explicitly to issue the
statement x.specificity = x.calcSpecificity()
(where x was the rule in question) for any reordering to take effect.
Such dynamic changes to rules’ precedence and conditions should only be
needed very rarely and should be used very sparingly, otherwise your
code is likely to become quite confusing.
Starting and Stopping Rulebooks: Return Values and Parameters
As we have seen, the way to start a RuleBook is to call its follow()
method. This causes it to select all of the rules that belong to it that
also match their various criteria, and then to run through them in
descending order of precedence, calling each of their
follow()
methods in turn until either there
are no more rules to consider or one of the rules signals that no more
rules should be considered after itself. But how does a rule signal
that?
The default behaviour is for a RuleBook to stop iterating through its
rules when one of its rules returns any value except null, where
null
is an enum that has been defined for this
purpose. Since TADS 3 considers a method that does not have an explicit
return value to implicitly return nil
(and
since nil
and null
are different values) this means that the default behaviour of a
RuleBook is to execute the highest priority matching rule, and then stop
iterating through any other rules that happen to match. Since the
RuleBook returns whatever its last-executed rule returns, this means
that by default it will simply return nil
to
its caller (assuming it actually executes any rules). Often this
behaviour may be just what you want, since if you simply want your
RuleBook to execute the most appropriate Rule, you may not be interested
in the return value, and you will probably want the RuleBook to stop at
the first rule it finds (in order of precedence).
There may be occasions when you want to change this behaviour, however.
First, it may that you want to define a Rule that doesn’t stop its
RuleBook. You can do that by ending the
follow()
method of that rule with the
nostop macro, for example:
+ Rule
priority = 1000
follow()
{
"You tense yourself. ";
nostop;
}
;
This rule will always run first (assuming there’s no rule in the same
RuleBook to which we’ve given a higher priority), but it won’t stop the
RuleBook. We could have used return null
rather than nostop
here, but not only is
nostop
less typing (and perhaps clearer in
meaning), for reasons we’ll explain shortly it’s also safer.
If you’re defining a RuleBook in which you want the majority of Rules
not to stop the processing of further Rules in that RuleBook, you can
override the RuleBook’s contValue (short for ‘continuation value’)
to nil
instead of
null
. The contValue
property defines the value a Rule must return if it is not to stop
the processing of the RuleBook. Since (as we mentioned just above) a
method with no explicit return value implicitly returns nil, this means
that the RuleBook won’t then stop at a rule with no explicit return
value (and hence an implicit return value of nil). If you want some
rules in such a RuleBook to stop the RuleBook from processing any more
rules, you can make those rules return a non-nil value or use the
stop macro to do this for you (by default
stop
returns true, but you can change what it
returns by overriding the RuleBook’s stopVal property).
As has been mentioned before, if a Rule returns any value other than its
RuleBook’s contVal
value, the RuleBook will
pass that value back to its caller. This may be useful simply as a way
of telling the caller what the RuleBook did, but it could also be used
if you wanted a RuleBook to calculate a value. Suppose, for example, you
wanted to use a RuleBook to calculate the player character’s happiness
rating; you could do something like this:
happinessRules: RuleBook
contVal = nil
total = 0
initBook()
{
total = 0;
}
;
+ Rule
priority = 0
follow()
{
return rulebook.total;
}
;
+ Rule
when = (me.hasSeen(gertrude))
follow()
{
rulebook.total += 3;
}
;
+ Rule
action = Kiss
dobj = gertrude
follow() { rulebook.total += 2; }
;
+ Rule
action = Attack
who = gertrude
dobj = me
follow() { rulebook.total -= 10; }
;
+ Rule
when = (orbOfSatisfaction.isIn(me))
follow() { rulebook.total += 6; }
;
...
Note how we here define a custom total
property on the RuleBook to keep track of the value we want to
calculate. We can then use the initBook() method (which is called
whenever we call the RuleBook’s follow()
method) to reset the value of total to 0 before our rules go on to
calculate its new value. We also supply a Rule with a priority of 0
(which should ensure that it’s the last rule to run) to return the value
of rulebook.total once all the other Rules have had their chance to
adjust it. Finally, note the use of the rulebook property of a Rule
to reference the Rulebook that triggered it (in this case, so that we
can get at the total
property of that
Rulebook).
Passing Arguments
Although it’s often fine to call the follow()
method of a RuleBook with no arguments, you can pass arguments if you
need to. These arguments are then passed in turn to the
follow()
method of each Rule executed by the
RuleBook and by the RuleBook’s initBook()
method, so you must make sure that their argument lists match those you
plan to pass to their RuleBook’s follow()
method (or you’ll get a run-time error due to argument mismatch). Thus,
for example, if you are going to call a RuleBook’s follow() method with
two arguments (myRules.follow(john, 'silly')
,
say) then you must ensure that you define all the associated Rules’
follow() methods and the RuleBook’s initBook()
method with two arguments (assuming you override the latter at all; if
you don’t, you don’t need to worry about it). If in doubt you can use
the variable argument list syntax on these methods so they’ll match any
parameters that are passed to them, like so:
myRules: RuleBook
initBook([args])
{
foobar = 0;
...
}
...
;
+ Rule
follow([args])
{
...
}
...
;
That way, you can be sure of avoiding any problems. But, to repeat the
point, this is only necessary if you plan to pass any arguments to your
RuleBook’s follow()
method, which very often
you won’t need to do.
You can use the arguments you pass (if you pass any) for any purpose you
find useful, but bear in mind that the first argument you pass will be
used to set the value of the RuleBook’s
matchObj
property for its Rules’
matchObj
properties to match against. This
first argument will then be passed as the first argument to each of the
matching Rules’ follow() method, which can be helpful as a way of
telling a Rule which specific object it’s matching. For example:
myRules.follow(magicOrb); // called somewhere else in your code
...
myRules: RuleBook;
+ Rule
matchObj = Treasure
follow(obj)
{
"\<<obj.theNameIs>> worth <<obj.points>>. ";
}
;
Assuming you have defined the magicOrb to be of the Treasure class (and have given it a points property), this will allow your rule to refer to the specific Treasure matched (in this case the magicOrb), although it would, of course, have matched any other Treasure.
Manipulating Rules
There are a couple of ways you can manipulate Rules at run-time. First
of all you can temporarily disable a Rule by calling its
deactivate() method, which sets its isActive property to nil.
You can set isActive
back to true again by
calling the Rule’s activate() method. When a Rule is deactivated it
can never be matched, so this may occasionally be a useful way of
temporarily removing a Rule from consideration.
You can also move Rules between RuleBooks, or add Rules to more than one RuleBook at a time, using the following methods:
- moveInto(rb): move this Rule to the rb RuleBook, moving the Rule out of every RuleBook it was in previously. If rb is nil this has the effect of leaving the Rule detached from any RuleBook.
- addTo(rb): add this Rule to the rb RuleBook while also leaving it in its current RuleBook(s). This allows a Rule to be used in more than one RuleBook at a time.
- removeFrom(rb): remove this Rule from the rb RuleBook while leaving it attached to any other RuleBook(s) it may be associated with.
Using Rules
This extension defines the Rule and RuleBook classes, but it doesn’t define any actual Rules or RuleBooks. It’s up to you as a game author to decide where you want to use them in your game.
Rules and RuleBooks are typically likely to be useful where a relatively complex set of responses would otherwise require a mass of if and switch statements to code, although there may often ways of approaching the same problem in adv3Lite. For example, if you want the Jump action to work differently in different situations, you could write a whole lot of roomBeforeAction() methods to intercept the Jump action, but it might be neater to redefine the action itself so that it uses a RuleBook:
modify Jump
execAction(cmd)
{
jumpRules.follow();
}
;
jumpRules: RuleBook
;
+ Rule
when = (goldBullion.isIn(me))
follow()
{
"The bullion is so heavy your feet hardly leave the ground. ";
}
;
+ Rule
where = lowCave
follow()
{
"Your head bumps the ceiling. Ouch! ";
}
;
+ Rule
present = [king, queen, auntAgatha]
follow()
{
"You hesitate to leap around in the presence of so
august a personage. ";
}
;
If there were many such factors involving the outcome of a JUMP command, it could become quite convoluted to code them in any other way.
Rules are less likely to be useful for coding the responses to TActions,
since normally these can be quite easily dealt with in the various
dobjFor()
sections of the direct object,
although again there may be occasional cases where the outcome of an
action depends on so many factors that defining a RuleBook and calling
it from the action() method may be the neatest way to deal with it. This
approach is more likely to be useful for TIActions, especially when
different outcomes depend on different combinations of direct object and
indirect object and perhaps other circumstances besides, which could
then be quite messy to code in the dobjFor() or iobjFor() sections of
the direct and/or indirect objects. For example, suppose that a variety
of objects can be used to dig a variety of other objects in a variety of
places under a variety of circumstances. The neatest way to handle it
might be something like this:
modify Thing
dobjFor(DigWith) { action() { diggingRules.follow(); } }
;
diggingRules: RuleBook;
+ Rule
where = sandyBeach
follow()
{
"You dig in the sand with {the iobj}, but turn up nothing
of interest. ";
}
;
+ Rule
dobj = rockyPath
follow()
{
"You find you can make little impression on the hard,
rocky path with {the iobj}. ";
}
;
+ Rule
dobj = rockyPath
iobj = pickaxe
follow()
{
"You manage to break through the surface of the rocky
path with the pickaxe. As a result you uncover a
small gold box. ";
goldBox.moveInto(me.location);
}
;
+ Rule
dobj = rockyPath
iobj = pickaxe
when = (goldBox.moved)
follow()
{
"Digging around in the path with the pickaxe reveals
nothing further of interest. ";
}
;
+ Rule
dobj = rockyPath
priority = 150
present = [&canSee, gardener]
{
"You start to wield {the iobj}, but when you see
the gardener looking your way you decide you'd
better stop. ";
}
;
...
Note that in this second case, the check() and verify() methods of the direct and indirect objects will already have ensured that the player character is attempting to dig something that can sensibly be dug with something that can sensibly be employed as a digging implement, so your digging rules would not need to test for any of that. If the check conditions were also likely to be complicated you could always define separate checkDigging and actionDigging RuleBooks and call them from the check() and action() methods respectively.
Rules and Doers
Rules are a lot like Doers, and at first sight it might appear that both examples above could equally well have been handled with Doers, for example:
+ Doer 'jump'
when = (goldBullion.isIn(me))
execAction(c)
{
"The bullion is so heavy your feet hardly leave the ground. ";
}
;
+ Doer 'jump'
where = lowCave
execAction(c)
{
"Your head bumps the ceiling. Ouch! ";
}
;
This would almost have the same effect, but not quite. By calling the
jumpRules from within the Jump Action you ensure that the Jump action
actually takes place. That means that any preconditions of jumping (such
as not being in any nested room) are honoured, and that the relevant
beforeAction() notifications are sent. This does not happen if you use
Doers to try to achieve the same thing. You may not notice the
difference, but again you may, since you might later go on to add a
roomBeforeAction() or a beforeAction() method that’s intended to trap a
Jump action, and then discover that it doesn’t work (because the
beforeAction() notification are never being sent). You could manually
call the beforeAction() notifications from your Doers (usually by
setting their handleAction
property to true),
but that’s extra work, potentially quite a lot of extra work if you’ve
defined quite a few of them. Using Doers works well if you want:
- To stop an action happening at all (and presumably display a message explaining why), e.g. “There’s not enough room here to jump. “; or
- To turn one action into another, e.g. to make PUT CARD IN SLOT perform the action UNLOCK SECURITY DOOR WITH CARD.
Otherwise, if you want the original action to go ahead, you may well be better off using Rules. That way you can take full advantage of the beforeAction() notifications, as well as of any verify() and check() methods the library already defines for you. (Also, Rules take a more fine-grained approach that Doers to sorting themselves in order of specificity, which may also be helpful in complex cases).
This covers most of what you need to know to use this extension. For additional information see the source code and comments in the rules.t file.
Adv3Lite Manual
Table of Contents |
Extensions >
Room Parts