Weekly Update #10

Lots of good stuff this week.

Changelog:

  • Added .attribute() handler for Rulebooks. Now I can check the actor (usually the player), location, region, etc attributes, including callback results, and use a variety of comparison operators.
  • Fleshed out the Litterbug quest. It now has multiple objectives (some optional), all of which can be completed. The quest has two stages of completion, and appropriate checking for start/end conditions.
  • Improved quest objective handling and output.
  • Refactored all action (verb) handling to use a standardized data argument rather than an arg list. Has some downsides, but allows me to more easily pass new info to verb callbacks without doing a ton of extra refactoring work every time.
  • Added ‘get in/on’ verb for containers/holders.
  • Set hot springs to holder so you can go for a soak.
  • Added handling for internal Rules (not triggered by user action) so I can apply the rulebook style to lots more things.
  • Prototyped context dropdowns for command tags.
  • Refactored Verb and Rule handling to chain better, use more consistent parameters and return values. Now uses a standard Action type with common parameters.
  • Fixed dialogue trees not working for topics after the first
  • Fixed onEnter/onLeave callbacks for Hill Slide and Musty Cave

Known Issues:

  • Nothing new

Next Up:

  • Test recent refactoring
  • Add The Twins.

Daily Update 262.2015

Changes:

  • Refactored Verb and Rule handling to chain better, use more consistent parameters and return values. Now uses a standard Action type with common parameters.

Bugs & Problems:

  • Need to test core verbs against refactored action handling.

Rule Chaining

I’m putting some thought into refactoring the Rules handling a little bit. I have two goals: 1) making the process of modifying/replacing action output more elegant, and 2) supporting rule chaining (allowing multiple Rules to modify the same output).

Right now a Rule returns text, along with a constant which specifies whether the Rule cancels the action, replaces the action, does nothing, or appends to the action’s output. However, I’ve already done the groundwork for Rules that happen before or after the action is handled. The idea is that I can have rules like:

  • Before Rule: Instead of Walking North from Dim Clearing while Actor is stuck in Bear Trap, say “You can’t move. You are stuck in a bear trap.”
  • After Rule: After Walking North from Dim Clearing, say “Something clangs loudly behind you.”

The first Rule cancels the regular action processing, whereas the second merely adds to the output. This approach renders the “append” rule handling moot. Instead, I’d like to create some kind of Action object, which is created before the first Rules are processed. It would look something like:

Action Object


1
2
3
4
5
var action = {
'actor':player,
'action':input,
'output':''
};

This object would be passed to each rule in sequence, where it might be modified (primarily the output field). It can be prepended, appended, replaced entirely, etc. The resulting object is passed to the next rule, and so on until all rules have been processed or a rule has cancelled the action.

The full flow would look like:

  1. Run ‘Before’ rules
  2. If action is not canceled, run Verb processing and append to Action output
  3. If action is not canceled, run ‘After’ rules
  4. Return Action object

Note that the After rules cannot cancel the action. It’s too late. They can still prepend, append, or change the output.

Lastly, this makes me think certain rules will need broken into two rules (Before and After), which I’d like to streamline. Could expand the understand() rule builder to have before() and after() methods (effectively generating two rules in one overall block), but I don’t know if that’s really worthwhile (vs just generating two rules one after the other). Probably not.

Open questions:

  • Is there a need for a Rule to not cancel the action entirely, but cancel any remaining rules in the current rule group?
  • Is there a need for a Rule to cancel Verb processing but not cancel After rules?

Daily Update: 260.2015

Changes:

  • Prototyped context dropdowns for command tags.

Bugs & Problems:

  • Not really a problem, but I need to add a descriptive layer for when the player is inside something.
  • Need to figure out a good way to populate the dropdowns (probably something rule based so modules can add entries to tags, but that also requires rules to chain nicely). Not a trivial problem, but one that if I solve it for command tags, it will also be useful elsewhere.

Internal Rules & Rulebooks

Yesterday I added an Internal Rule feature to the engine; it’s an extension of the Rulebook system with some interesting uses.

Rulebook Categories / Rule Groups

The Rulebook is now categorized, so every Rule belongs to a Rule Group. The current groups are:

  • Before (which is the default; rules I’ve previously created all go here)
  • After (new group for rules that run after regular action processing; they can’t interrupt actions)
  • Internal

I can add more Groups later that can be executed at different times, or based on specific types of events. This will be important both for functionality improvements and performance, since I can avoid running every rule on every tick.

Internal Rules

Internal rules are triggered directly by the ECS, and aren’t dependent (entirely) on action execution. They can happen during non-tick executions, and can have a variety of effects. The basic idea is that I can define an internal action called (for example) getLocationName, and then apply rules to that action as if it were a regular verb action. I can also supply a default function if no rules override it.

Internal Action: getLocationName


1
2
3
ECS.addInternalAction('getLocationName', function(data){
return data.location.name;
});

This defines the default action for the getLocationName, which simply returns the name of the location. This is used by the Place component to print the location name during place descriptions.

Ok, so far I’ve just complicated the place descriptions. Next up is the fun part: modifying location names based on rules.

Example

Internal Rule: inside object location name rule


1
2
3
4
5
6
7
8
understand('inside object location name rule')
.internal('getLocationName')
.attribute('actor.parent', 'is', 'holder')
.do(function(data){
data.self.responseText = " (inside "+ data.actor.parent.name +")";
return Rulebook.ACTION_APPEND;
})
.start();

This verbosely-named Rule checks to see if the actor (usually the player) is in/on an object in the location. They could be hiding inside a box, soaking in the hot springs, etc. If so, it appends a parenthetical description to the location name, e.g. “Hot Springs (inside pools)”.

Similar internal rules can be created for object names (appending object state or changing the name entirely), controlling which objects can be seen through a window, etc. Like any other rule, internal rules can be deregistered once they’ve served their purpose.

Daily Update 258.2015

Did a lot of refactoring today.

Changes:

  • Refactored all action (verb) handling to use a standardized data argument rather than an arg list. Has some downsides, but allows me to more easily pass new info to verb callbacks without doing a ton of extra refactoring work every time.
  • Added ‘get in/on’ verb for containers/holders.
  • Set hot springs to holder so you can go for a soak.
  • Added handling for internal Rules (not triggered by user action) so I can apply the rulebook style to lots more things.

Bugs & Problems:

  • Nothing new

Daily Update 257.2015

Added some important new functionality and fixed a longstanding bug today.

Changes:

  • Added .attribute() handler for Rulebooks. Now I can check the actor (usually the player), location, region, etc attributes, including callback results, and use a variety of comparison operators.
  • Fleshed out the Litterbug quest. It now has multiple objectives (some optional), all of which can be completed. The quest has two stages of completion, and appropriate checking for start/end conditions.
  • Improved quest objective handling and output.
  • Fixed dialogue trees not working for topics after the first
  • Fixed onEnter/onLeave callbacks for Hill Slide and Musty Cave

Bugs & Problems:

  • Nothing new

Weekly Update #9

Lots of progress this week, plus changes to the update schedule. Finally built a more streamlined setup for managing rules.

Changelog:

  • Added descriptions for lair and wyrmling
  • Added missing exit tags for all locations
  • Made locationIs() check more flexible
  • Switched to Open Sans for better font weight options
  • Updated tag styles to include semibold, cursor style and hover
  • Updated player output style to be more traditional
  • Added a nifty background (experimental) with sunbeams and falling leaves
  • Added a new Rulebook system for flexible rule/special-case management
  • Cleaned up nametag helper to more closely match tag helper
  • Added cursor styles for LOOK and TAKE tags
  • Added .unless(), .modifier(), and .on() chains for Rules
  • Fixed exit descriptions in dim clearing
  • Fixed exit descriptions in east trail
  • Fixed bird chirps not working
  • Fixed wyrmling anger

Known Issues:

  • Nothing new

Next Up:

  • .attribute() chain for Rules
  • Fix nested dialogue trees
  • Wrap up litterbug quest
  • Add The Twins.

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.