A Guide To Rule Writing

by Jochen Bedersdorfer

Version 1.4 from 02.11.97

[Changes for 1.3 is a new section about cancelled actions and a change in the Rule interface]

This document describes the steps needed to create rules in the White Orb System. Rules are currently used in two
areas. First, as building blocks for the specification of NPC behaviour and second, as means to specify the
characteristics of Activities. Rules create actions to be carried out by a Physical and compete with each other
in the context of a RuleSet.

Generally a rule consists of the following parts:

Normally, rules are managed by a RuleSet. Rules - to be exact - instances of classes that implement the rule interface - can be added/deleted in the ruleset. It is perfectly safe to add two instances of the same class (although the priority should differ). Among other things, the ruleset provides the method getFiringRule() that returns a rule with the following properties: Later we will see, that the proper choice of priorities are crucial to gain a special behaviour. The current rule of a ruleset is the rule, that is delivering actions at the moment. This makes sense, because after each completed action and each received event, the ruleset is asked for a new rule that has the above properties, thereby interrupting the current rule.

Now, let us look at the dirty details of writing a rule.


The Rule Interface

These are the methods a rule has to implement. I will explain them in detail below.
All of these methods are implemented in the class DefaultRule, which also is a simple example of a rule.
public interface Rule {
 public void init(Soul me, Active listener, RuleSet rs);
 public boolean canFire();
 public Rule fire();
 public Action deliver();
 public void done();
 public boolean interrupt();
 public boolean canContinue();
 public boolean doContinue();
 public boolean removeMe();
 public void setRemovable(boolean value);
 public void setPriority(int priority);
 public int getPriority();
 public String toString();
 public void failed(Action a);
}

public void init(Soul me, Active listener, RuleSet rs);

You got it! This initializes the rule with the needed information. Rules get access to the world via a Soul. You can
always use the corresponding member variable me in your code to access it.
The listener is an instance of a class that implements Active and is informed when an action is completed.
In case of NPCs this is NPCSoul and if you use the rule in context with an activity it is a subclass of Activity.
The last parameter allow rules to access methods in the RuleSet, thereby allowing for insertion and deletions of rules.
The ruleset also gives access to a current event or a last completed action. As you will see, these are usually the
variables the rule's precondition will work with.

public boolean canFire();

This method provides the precondition part of the rule. Here the rule decides if it wants to deliver actions to its caller via the deliver() method. If  it wants to, canFire() should deliver true.
It is important, that this method may not create actions on its own ! It is just asked, if it wants to !
Keep that in mind, when implementing this method.
Most rules will use rs.getEvent() and s.getAction() to get access to the currently propagated event and the last completed action resp.

public Rule fire();

After a rule is chosen to deliver events, this method is called. It gives the rule a chance to prepare itself, do some calculations and most important
deliver an instance of this rule back. Why that ?
In some cases, you would want to configure this rule in a special way and not mess around with the rule whose canFire() method is used. You do this by creating another instance of this rule and return that back. Important here is that the original rule can still fire and be chosen from getFiringRule(), since the current rule is possibly an instance of the same class, but not the same instance. So, be careful, if you 'spawn' new rules with fire().
Most rules will just return this, thereby preventing the rule from being chosen again by the ruleset as long as it fires (i.e. delivers events). The returned rule is now the current rule for the ruleset. Returning null is not allowed and undefined.
If the action part requires lengthy preparations, they should be done in this method and NOT in the canFire() method, since the rule doesn't know if it will be chosen. You can use rs.getEvent() and getAction() as usual, because they didn't change since the call to canFire().

public Action deliver();

This is actually what I called the body of the rule. This method gets called from outside as long as it returns an action that is then executed by the caller. So, if you return null the rule stops firing and loses its status as current rule. If you returned this in fire(), the rule can be chosen again by the ruleset next time it is asked.
Important! It must be guaranteed that at least one action is delivered !

public void done();

When the rule stopped delivering events (and only then), this method will be called by the system. A rule should now reorder itself, if needed. It is guaranteed that done() is called before the next call to canFire().

public boolean interrupt();

Now to something more interesting. As you already know, the system asks the ruleset for a firing rule everytime something interesting happens, e.g. an event occured or an action is completed. If the ruleset returns a new rule, the current rule needs to be interrupted, since the new rule is more important (or even VERY urgent, in case of running-away-as-fast-as-you-can-situations).
If this happens, the current rule's interrupt() method is called. The rule can now for example save its configuration for a possible continuation. You should return true, if the interruption was succesful, which is the precondition for resuming the rule's delivery.

public boolean canContinue();

The rule should return true, if it may be able to carry on rule delivery if it is interrupted. So the systems behaviour is like this: call interrupt(); if it returns true, call canContinue(); if it returns true, put the rule on the rule stack.

public boolean doContinue();

Upon completion of a rule, the system looks up, if there is a rule on the rule stack. If so, it calls this rule's doContinue() method. The rule should check now, if the environment is suitable for a continuation of processing and return true, if it wants to carry on. It is then chosen as the current rule and gets deliver() called as usual. If doContinue() returns false, the rule is thrown away with no further notification. Suppose, your WalkingToTheDentist rule was interrupted a while ago, but the rule wants to carry on, doContinue() will check for example if in the meantime the patient (i.e. the NPC) walked away or lost all its teeth etc.

public boolean removeMe();

This and the next method deal with automatic deletion of rules. Suppose a TimeTable rule inserted the current rule (A GoToBed rule for example) a while ago. Upon completion of the GoToBed-operation, the rule isn't needed anymore and can be removed from the ruleset. If this method returns true, the rule is deleted after calling done() automatically.

public void setRemovable(boolean value);

Used to set the return value of removeMe().

public void set/getPriority();

These methods are somewhat self-explanatory. Time to lose some words about priorities. Priorities are ints defined in the range from 0 to 2^32. The higher a priority, the more "important" a rule becomes. For a rule to interrupt the current rule, it must have a greater or equal priority !
It is not recommended to have two different rules with the same priority, since it is undefined which one is chosen, if both canFire().

public String toString();

Provide a description of the rule. Mainly important for debugging purposes and builder tools.

public void failed(Action a);

This method was added to let rules now that an action they produced were cancelled and failed execution.
The DefaultRule class stores this failed actions in the failedAction field, so there normally is no need to overwrite this method.

Phew ! That was quite a bit. I hope you got the most important points. Of course, normally, you wouldn't implement rules like WalkingToTheDentist or GoToBed, but use some more general rules.


Complex And Simple Rules

First of all you have to decide whether you want to write a complex rule or a simple rule.
The difference is, that orb.ai.rules.ComplexRule, the mother of all complex rules, manages a queue
for actions. That means, if someone asks the body of the rule to deliver an action, your processing
can possibly generate several actions, that need to be delivered one at a time.
For example, the PathFinder class returns a sequence of directions which should be carried out by MoveDirection
actions. Using a complex rule,  you put all actions in a queue and the rest is taken care of by ComplexRule.
We will look at this in detail in an example below. If you want to write a simple rule, just subclass from orb.ai.rules.DefaultRule and go ahead. DefaultRule provides some useful member variables and returns mostly used values for canContinue() etc. You should look at the code for DefaultRule now.
Here is an example of a subclass of DefaultRule:
public class SaySomething extends DefaultRule {

  public boolean canFire() {
    if (rs.getEvent() instanceof TalkEvent) {
      TalkEvent te = (TalkEvent)rs.getEvent();
      if ("Get me a drink!".equals(te.getMessage()))
	return true;
    }
    return false;
  }
As you can see, this method makes use of rs.getEvent() and tests if an TalkEvent occured. It checks if someone says "Get me a drink!" and if so returns true, thereby saying "Please, let me deliver actions".
  public Rule fire() {
    a = null;
    return this;
  }
This just sets a provided Action reference to null and returns itself, thereby preventing that canFire() of this rule is called, while firing.
  public Action deliver() {
    // deliver just one event and then quit
    if (a == null)
      a = TalkEvent.shout(listener, me.getBody(), "Shut up and keep shut!");
    else 
      a = null;
    return a;
  }
The method delivers just one action (is has to deliver at least one) and that is a TalkEvent. Remember me and listener ? They must be used upon creating actions as listener and source of this action. To be correct, the body of the soul should be the source of an action. (It is of course imaginable, that something else is the source, but this is not recommended) It is crucial, that listener is used correctly, since the system depends on that is informed if an action is executed. Otherwise the processing of this rule will stop!
  public String toString() {
    return "SaySomething - A test rule for reacting on TalkEvents";
  }
}
As you can imagine, deliver() can become quite bloated and complicated if you want to deliver more than one action. That is, where ComplexRule drops in.

Here is an excerpt from the definition:

public class ComplexRule extends DefaultRule {
As you see, this class inherits from DefaultRule, getting me and listener.
  public void queue(Action act) {
With this method, you can put actions in a queue. Upon the call to deliver() the first element of the queue is returned. So now you can easily put a lot of actions in the queue and let deliver() do the rest.
  void nextAction() {
    switch (phase) {
    case FIRST:  // sequence of action has begun
      // queue a waitaction
      queue(new WaitAction(listener, 100));
      break;
    }
    phase++;     // increment phase, thats important to prevent endless loops
  }
This is the heart of ComplexRule. Instead of overwriting deliver() as you did with simple rules and DefaultRule, you now have to overwrite nextAction(). This method is called whenever the queue is empty and new actions are needed.

I also implemented a simple phase model. You can separate your rule body in several phases. The first call to this method is made with the member variable phase set to FIRST, which is 0 in fact.
The switch statement can now be used to branch to different phases. For example, you would put a TalkEvent in the first phase, increment phase and put a lot of move events in phase 1. See GoAndFetch for an example of this. As soon as the queue runs empty and nextAction() fails to insert new actions, the rule's firing stops as usual. Instead of incrementing phase you can of course jump back to other phases etc.

You are now ready to implement your own rules. Take a look at rules like GoAndFetch to learn more about things like PathFinder etc.

Putting It All Together

I didn't say yet how rules get into the ruleset and where.
For NPCs, the Constructor is a possible place to insert rules. If you have to rely on an fully initialized world, use doBirth() instead. The class NPCSouls provides member variable rs as the default ruleset. Here's an example of a complete NPC:
public class BartenderNPC extends NPCSoul {
  public BartenderNPC(Clock clock, Physical c) {
    super(clock,c);
    r = (Bartending)rs.addRule(Bartending.class);
    r.setPriority(200);
    rs.addRule(StartStopActivity.class).setPriority(300);
    loadActivity(FishingActivity.class);
  }
  public void doBirth() {
    super.doBirth();
    r.setHome((Place)getBody().getLocation());
  }
}
This is quite self-explanatory, isn't it. The doBirth() method should always call its parent's and the bartending rule is configured here. This can't be done in the constructor, since getBody() possibly isn't initialized yet. It is also better to add rules or load activities then.

For Activities, rules should be added in init() the usual way via member variable rs.

Cancelled Action

As a new version approaches I have finally found a way to deal with cancelled actions. Normally you would expect that the actions of rules should be completed. But this is not always the case. Just imagine a DontMoveFilter in the filter section of a body a rule wants to move. The MoveEvent will be cancelled.
If that happens, the following steps are taken:
  1. currentRule.failed() will be called with the action that was cancelled.
  2. currentRule.interrupt() is called to inform the rule about an interruption
  3. currentRule.canContinue() is called. The rule should see if continuation is possible. This is only done if interrupt() returned true
  4. currentRule.doContinue() is called, if canContinue() returned true. The rule should now take care, that the next call to deliver() will return a new action.

The dangerous thing here is, that in the case of a DontMoveFilter a new movement will again be cancelled. Eventually the rule will again try to create a new MoveEvent and so on.
One way to cope with that is to deliver a WaitAction first with a reasonable waiting time. You could also implement a counter.
As an example, I updated GoAndFetch and GoAndDrop. They now try to plan a new way if their MoveEvents are cancelled. That makes the rules more robust against stumbling into a newly created wall or something like that.
How can you differ between an normally interrupted rule and an interruption caused by a cancel ?
If it was a cancelled action, failedAction will contain this action. Otherwise failedAction is null. Note that you sould reset failedAction to null again at the end of doContinue() !
Take a look at the code of GoAndFetch to see that in detail.

Now hopefully your rules can cope with cancelled action and provide a clever behaviour to resolve that.

The Dangerous Cliffs

It can be quite difficult to balance the priority of rules and to implement a natural behaviour.
You should provide reliable fall-back rules that bring a NPC back on track, if something unusual happens.

For NPCs, make use of Activities, since they have the additional benefit of providing filters. Use the rules to switch between different activities, for example, creating a time table for our NPC, that makes him wake up in the morning, cut wood til lunch etc.

Be careful when returning new instances of a rule with the fire() method. They will become interrupted, when the original rule fires again.

Don't generate actions in canFire(). This leads to chaos :)

Use set/get method pairs to configure more general rules. (as I did with the GoAndFetch/Drop rules)


FAQ

This is a collection of questions concerning this rule guide.

A ruleset can belog to a Soul and to an Activity, right? So why the init() method of a rule passes soul and ruleset?
Because the rules don't know how to reach the ruleset consistently. Since Soul and Activity aren't in an ancestor relation, I would have to give rules both Soul and Activity. Rules can be written to work with both that way.
Also, enhanced versions of NPCSoul which inherits from Soul could allow for a dynamical change of the ruleset and maintain more than one ruleset.

Where is the danger about 'spawning' rules with the fire() method?
For example, if the spawning rules reacts on movement events and for every movement it delivers a new firing rule, then the current rule (which was spawned) is interrupted. This is because they have the same priority ! You can play it safe, if you change the priority of spawned rules. I can't think of a situation at the moment, where you would need spawned rules, but who knows :)

Is there a way to find out the most prior rule in the ruleset? It could be useful when you want to add an emergency rule.
Unfortunately this is not possible at the moment. But I will insert it in the next version. Good idea !

I didn't got it pretty well how to define a NPCSoul(In Putting All Together). Could you give me another example?
NPCSoul is a subclass of Soul and works with rules, i.e. provides a ruleset, choses the firing rule and sends actions coming from the rules to the ActionQueue, thereby bringing the NPC to live. (For version 1.1 of the document, I updated the "putting ..." section.) I hope to have many other examples of NPCs soon.

How does this "StartStopActivity" works? BTW, how does and Activity works?
StarStopActivity is just a rule to test the activity stuff. If a NPC has this rule added and you say "start " to it, it will start the activity. In fact, all NPCs with that rule that can hear your voice will ;) With "stop activity" will stop their current activity. Activities are just rulesets with the possibility to add or remove effects to a physical. See my upcoming activity guide for details.

Have fun !