There are existing contrib modules for scenes and rulebooks, but they don’t do what I need so I wrote some code.
First, a simple rules engine: ruleEngine github repo.
Second, module for rule-based scenes: scene github repo.
I won’t go over all of the internals here unless someone wants more details, but the basics are:
// You have to declare one (and only one) scene controller.
mySceneController: SceneController;
// Delcare a simple scene.
myScene: Scene
sceneAction() {
"<.p>This is a scene, doing nothing. ";
}
;
This creates a scene that writes a single line of output every turn. Or rather it would if it was active, which it is not by default. You can manually toggle the scene with:
// Enable the scene.
myScene.setActive(true);
// Disable the scene.
myScene.setActive(nil);
That works, but isn’t very useful. To automatically start and stop scenes you can instead define triggers:
// Delcare a scene that's active when >TAKE is used.
myScene: Scene
sceneAction() {
"<.p>This is the scene, noticing that you
used TAKE. ";
}
;
+Trigger
action = TakeAction
;
This will make the scene active on any turn where >TAKE is the action.
The Trigger class understands multiple properties as test criteria:
- srcObject the object taking the action (usually
gIobj) - srcActor the actor taking the action (usually
gActor) - dstObject the object receiving the action (usually
gDobj) - dstActor the actor receiving the action (either
gDobjif it’s anActor, orgDobj.getCarryingActor()if it’s a non-ActorThing) - action the action itself (usually
gAction) - room the location of the action (usually
gActor.getOutermostRoom())
So, for example, if you wanted the scene to be active only when Bob tries to take the pebble:
+Trigger
srcActor = bob
dstObject = pebble
action = TakeAction
;
By default, an ordinary Scene runs only when active and is active only if a) something has explicitly called setActive(true) on the Scene instance, or b) all of the conditions defined on the scene are matched for the turn.
If you want a scene that runs continuously once triggered (or runs until stopped by a different trigger), there’s SceneDaemon:
daemonScene: SceneDaemon
unique = true
sceneStartAction() {
"<.p>This is the scene daemon starting. ";
}
sceneStopAction(v?) {
"<.p>This is the scene daemon stopping. ";
}
sceneAction() {
"<.p>This is the scene daemon, first started
<<toString(getDuration())>> turns ago.\n ";
}
;
+SceneStartMatchAny;
++Trigger
srcObject = pebble
action = TakeAction
;
+SceneEnd;
++Trigger
srcObject = pebble
action = DropAction
;
This creates a scene that is started by >TAKE PEBBLE and stopped by >DROP PEBBLE:
>take pebble
Taken.
This is the scene daemon starting.
>x pebble
A small, round pebble. Picking it up starts the scene.
This is the scene daemon, first started 1 turns ago.
>drop pebble
Dropped.
This is the scene daemon stopping.
There are also utility classes for scenes that either allow all actions except those defined as triggers, or deny all actions except for those defined as triggers:
SceneDefaultAllow;
+Trigger
srcObject = pebble
action = TakeAction
;
This allows all actions except >TAKE PEBBLE:
>x pebble
A small, round pebble.
>take pebble
You can't do that here.
The message is either the scene’s sceneBlockMsg or playerActionMessages.sceneCantDefaultAllow.
The deny variation is the inverse:
SceneDefaultDeny;
+Trigger
action = static [ TakeAction, QuitAction ]
;
This creates a scene that blocks all actions except >TAKE and >QUIT:
>x pebble
You can't do that here.
>take pebble
Taken.
In this case the block message is either the scene’s sceneBlockMsg or playerActionMessages.sceneCantDefaultDeny.
All of this logic is built on the Rule, Rulebook, and RuleUser logic provided by the ruleEngine module. I won’t go into all of that here, except to point out a couple features:
- All the examples above involve adding
Triggerinstances directly toSceneinstances.Triggeris a subclass ofRule, andSceneis a subclass ofRuleUser. Adding aRuledirectly to aRuleUsercreates aRulebookwith the ID “default” - The default behavior described above notwithstanding, you can declare multiple
Rulebookson scenes if you need them - A
Rulebookinstance’s state isnilby default, and it becomestruewhen all the rulebook’s rules are matched. But there are subclasses with different behaviors:RulebookMatchAny, which becomestrueif any of its rules match; andRulebookMatchNone, which has a default state oftrueand becomesnilif any of its rules are matched. There’s also aRulebookMatchAllwhich is just a synonym for the baseRulebookclass. - In addition to using
Triggerinstances, you can useRuleinstances. ARuleis just a l’il object that implements an arbitrary state check and provides amatchRule(data?)method that returns booleantrueon a match and anilotherwise.
The code is (hopefully) extensively commented and there are little demo “games” in the ./demo subdir of each repo.
The main “gotcha” here is that as currently written all Rule instances get evaluated every turn. In a “real” rules engine there would be a graph/tree/grammar that would short-circuit rule chains so as to not evaluate rules whose state doesn’t matter (if a you’re looking at a rulebook that’s true if all its rules match and the first rule doesn’t, then you don’t need to check the rest). This is kinda a side-effect of wanting to be able to implement arbitrary checks in Rule.matchRule() without implementing a whole new grammar for rules. I might do a parse tree sort of thing with Trigger instances later, though.