The Rulebook

Yesterday I mentioned a new system for building ‘rules’, whatever that means. Now I’ll go over whatever that means.

I’ve previously covered a couple pieces of functionality for handling special cases: Command Interrupts and Filters. Both are useful (and will stick around in some capacity), but had a few notable shortcomings.

The Problems

  1. Filters: Complex Setup, Unintuitive

Filters let me do things like blocking items inside Containers from vision (unless the container is open), as well as adding implicit actions (such as opening a door when trying to move in a direction). Very useful, but the code for it looks like this:

Add container restrictions on LOOK and TAKE actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var f = function(args){
if(args.nouns.length > 0)
{
var obj = args.nouns[0];
if(obj.parent != null
&& obj.parent.hasComponent('container')
&& !obj.parent.isOpen)
{
if(!obj.parent.isTransparent)
{
queueOutput("{{gm}}<p>You can't see any such thing.</p>");
return false;
}

queueOutput("<p>(first opening the "+getNameTag(obj.parent)+")</p>");
}
}

return true;
};

It’s a decent chunk of code, and while it’s not that hard to follow, I think I can do better.

  1. Command Interrupts: Limited Use

Command Interrupts are handy for things like menus, snarky one-off responses, easter eggs and the like. Generally they only happen once, and they happen instead of regular verb processing. Nothing particularly wrong with them, but they don’t have the versatility I was looking for.

The Use Case, Part 1

Examples:

  1. Warning the player that the area they’re in is too hot/cold (happens after verb processing, doesn’t prevent any actions).
  2. Adding a new command (not a verb), WANDER AIMLESSLY, which can be used anywhere within the Forest region.
  3. Preventing the player from accidentally (or intentionally) leaving behind a crucial item in a place they can’t get back to.

I’ll explain the solution to this problem, and then come back to these examples.

The Rulebook

The Rulebook is loosely modeled after Inform 7, like many things in the engine. In Inform 7, you can write a statement like:

1
Instead of punching the goblin:
  say "That seems unlikely to achieve anything.";
  stop the action.

This is part of their Rule system, which handles Instead, Before, After, and many other types of rules, which they group into Rulebooks.

Mine’s a bit simpler for now, but is structured in such a way that I can add new capabilities to it over time (and Modules can add capabilities/rules, of course).

The SRPG Rules can handle:

  • Action checks (raw text, Verb matching, regex, or callback)
  • Location and Region restrictions (only apply the rule if the actor is in the set of locations/regions)
  • Arbitrary filters (callbacks)
  • Canceling, Prepending, or Appending to regular Verbs

I’d also like to add some simple attribute checking, to avoid the need for callbacks in most filters. One of the end goals is to minimize the amount of custom code written for rules, in favor of a more expressive style for which the Rulebook generates the appropriate code automatically.

Rules use chaining (each function/method returns the object), similar to how many ORMs implement querying. Most of the chained functions resolve into filters, which are grouped together by type and processed in blocks (for example, Location filters are resolved together).

Use Cases, Part 2

Let’s get back to the 3 examples listed above.

Warning the Player they are going to Die

I’ll show the code for this Rule, then explain in detail what each step does. I’m pretending for the moment that the “simple attribute checking” element is already in place.

The Dangerous Temperature Rules
1
2
3
4
5
6
7
8
9
understand('dangerously hot rule')
.attribute('location.temperature', '>', 325)
.append("This area is dangerously hot. Best not stick around.")
.start();

understand('dangerously cold rule')
.attribute('location.temperature', '<', 275)
.append("This area is dangerously cold. Best not stick around.")
.start();

Now the explanation:

  • Line 1: Define a new named rule (the name isn’t important)
  • Line 2: Only apply this rule of the temperature of the actor’s location is greater than 325K
  • Line 3: If all the filters for this rule have been matched, output the given string after regular verb processing.
  • Line 4: Register the Rule with the Rulebook.
  • Lines 6-9: Same as 1-3, but for cold instead instead of hot.

Wandering Aimlessly

Next up is the first Rule actually in the game. Any time the player is in the Forest, they can choose to WANDER AIMLESSLY. This is not an actual Verb, but the command is intercepted by the Rule (similar to how a command interrupt would).

The Wandering Aimlessly Rule
1
2
3
4
5
6
7
8
understand('rule for wandering aimlessly')
.text('wander aimlessly')
.inRegion('forest')
.as(
"You wander for a bit and find yourself right back where you started.",
Rulebook.ACTION_CANCEL
)
.start();

Explanation:

  • Line 2: Match exact text from the actor (not case-sensitive though)
  • Line 3: Only apply this Rule in Locations that are part of the Forest Region.
  • Line 4: If all filters are met:
  • Line 5: Queue a response
  • Line 6: Cancel the default action (i.e. ignore any verb that might have matched this action)

Don’t Forget Your Keys

Lastly, here’s a rule to prevent the player from accidentally leaving the Rainbow Sword behind on a hill.

The Remember Your Sword Rule
1
2
3
4
5
6
7
8
9
10
11
12
13
understand('remember your sword rule')
.verb('move')
.modifier('down')
.in('hill-slide')
.attribute('actor.inventory', '!contains', 'rainbow-sword')
.as(
"You won't be able to come back up. Don't forget your sword!",
Rulebook.ACTION_CANCEL
)
.until(function(actor){
return actor.locationIs('north-trail');
})
.start();

Explanation:

  • Line 2: Match any instance of the movement verb (which could be WALK, RUN, E, etc)
  • Line 3: Only match if the verb has a specific modifier (in this case, the actor is trying to move down)
  • Line 4: Only apply if the actor is in the Hill Slide location.
  • Line 5: Only apply if the actor’s inventory does not contain the rainbow sword.
  • Line 6: If all filters are met, warn the actor and cancel the action.
  • Line 10: Only apply this Rule until this condition is met. In this case, the Rule is permanently removed once the actor moves from Hill Slide down to the North Trail.

Next Up

I have a bunch of Rule functions I need to add, including some I’ve cheated by using in these examples. I say ‘cheated’, but they are achievable in the current system by using while() callbacks (it’s just more verbose).

  1. attribute(a, operator, value)
  2. modifier(m), modifier(callback)
  3. unless(callback)
  4. on(target)

With all that in place, I think I could rewrite the container restriction from the top of this post to be less verbose/unwieldy. Something like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
understand('container interaction rule')
.verb('look')
.verb('take')
.attribute('target.parent.components', 'contains', 'container')
.attribute('target.parent.isOpen', '=', false)
.then(function(actor,action){
if(action.target.parent.isTransparent) {
queueOutput("(first opening the "+getNameTag(action.target.parent)+")");
return Rulebook.ACTION_NONE;
} else {
queueGMOutput("You can't see any such thing.");
return Rulebook.ACTION_CANCEL;
}
})
.start();

I also need to put some thought into cases like the dangerous temperature example above. I suspect that it will get triggered even if the action being taken is leaving a cold/hot area, due to the order in which verbs and appending occur.

Lastly, I’d like to build in the capacity for ‘meta filters’. In Inform, you can define (for example) a set of behaviors as ‘embarrassing’, and then reference the ‘embarrassing’ trait in other rules.

I’m pretty happy with how the Rulebook is working so far, but I know there’s a lot more I can do with it.