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
gDobj
if it’s anActor
, orgDobj.getCarryingActor()
if it’s a non-Actor
Thing
) - 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
Trigger
instances directly toScene
instances.Trigger
is a subclass ofRule
, andScene
is a subclass ofRuleUser
. Adding aRule
directly to aRuleUser
creates aRulebook
with the ID “default” - The default behavior described above notwithstanding, you can declare multiple
Rulebooks
on scenes if you need them - A
Rulebook
instance’s state isnil
by default, and it becomestrue
when all the rulebook’s rules are matched. But there are subclasses with different behaviors:RulebookMatchAny
, which becomestrue
if any of its rules match; andRulebookMatchNone
, which has a default state oftrue
and becomesnil
if any of its rules are matched. There’s also aRulebookMatchAll
which is just a synonym for the baseRulebook
class. - In addition to using
Trigger
instances, you can useRule
instances. ARule
is just a l’il object that implements an arbitrary state check and provides amatchRule(data?)
method that returns booleantrue
on a match and anil
otherwise.
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.