Skip to content

Instantly share code, notes, and snippets.

@kenwebb
Last active July 11, 2018 01:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kenwebb/39f5f62962bb9b5641b35f249f096e5d to your computer and use it in GitHub Desktop.
Save kenwebb/39f5f62962bb9b5641b35f249f096e5d to your computer and use it in GitHub Desktop.
JavaScript Rule Engines
<?xml version="1.0" encoding="UTF-8"?>
<!--Xholon Workbook http://www.primordion.com/Xholon/gwt/ MIT License, Copyright (C) Ken Webb, Tue Jul 10 2018 21:36:51 GMT-0400 (Eastern Daylight Time)-->
<XholonWorkbook>
<Notes><![CDATA[
Xholon
------
Title: JavaScript Rule Engines
Description:
Url: http://www.primordion.com/Xholon/gwt/
InternalName: 39f5f62962bb9b5641b35f249f096e5d
Keywords:
My Notes
--------
July 5, 2018
see my notebook for July 4
I would like to use json-rules-engine with Xholon, as an alternate way of doing recipes.
To make json-rules-engine available as a browser script I can use from Xholon:
-------------------------------------------------------
cd ~nodespace
mkdir jsonrulesengine
cd jsonrulesengine
npm install -g browserify
npm install json-rules-engine
- I created a simple main.js, and package.json
browserify main.js -o json-rules-engine.js
HTML file can include:
---------------------
<script src="json-rules-engine.js"></script>
To use the recipe with json-rules-engine (from Dev Tools):
----------------------------------------
// this works
console.log(window.JsonRulesEngine);
var rserv = xh.service("RecipeService");
var rbname = "RecipeService-JsonRulesEngineRecipeBook-Island";
var recipebook = null;
if (rserv) {
console.log(rserv.name());
var rmsg = rserv.call(-3897, rbname, xh.root());
console.log(rmsg)
recipebook = rmsg.data;
console.log(recipebook);
}
let engine = new window.JsonRulesEngine.Engine();
let rule = recipebook; // only one rule structure for now
engine.addRule(rule);
let avatarFacts = {
inventory: ['Stick','Hook','String','Fish','Fruit','Lumber'],
wants: 'FishingRod'
}
engine
.run(avatarFacts)
.then(events => { // run() returns events with truthy conditions
events.map(event => console.log(event.params.message))
})
References
----------
(1) https://www.npmjs.com/search?q=keywords%3Arules%20engine&ranking=quality
36 packages found
(2) https://www.npmjs.com/package/json-rules-engine
) https://github.com/CacheControl/json-rules-engine
A rules engine expressed in JSON
json-rules-engine is a powerful, lightweight rules engine. Rules are composed of simple json structures, making them human readable and easy to persist.
(3) https://en.wikipedia.org/wiki/Rete_algorithm
(4) https://en.wikipedia.org/wiki/Rule-based_system
(5) https://en.wikipedia.org/wiki/Drools
(6) https://github.com/noolsjs/nools
(7) https://en.wikipedia.org/wiki/RuleML
(8) https://github.com/CacheControl/json-rules-engine/blob/master/examples/09-rule-results.js
"This is a basic example demonstrating how to leverage the metadata supplied by rule results"
(9) https://github.com/chrisamaphone/interactive-lp/blob/master/examples/3lp.cep
Ceptre - Three little pigs
(10) https://graphicallinearalgebra.net/
Graphical Linear Algebra
Pawel Sobocinski
]]></Notes>
<_-.XholonClass>
<PhysicalSystem/>
<!--<JsonRulesEngineRecipeBook/> this is a Xholon built-in class -->
<JsonRulesEngineTester/>
<Items/>
<ItemsRoot/>
<Stick/>
<String/>
<Hook/>
<FishingRod/>
<Twig/>
<Vine/>
<Thorn/>
<Cat/>
<Fish/>
<Lumber/>
<LumberJackCat/>
<Number/>
<Egg/>
<Sugar/>
<Mascarpone/>
<White/>
<Yolk/>
<WhiskedWhites/>
<YolkyPaste/>
<ThickPaste/>
<CremaDiMascarpone/>
<!-- Three Little Pigs [ref 9]
pig : pred.
wolf : pred.
sticks : pred.
bricks : pred.
straw : pred.
stickhouse : pred.
brickhouse : pred.
strawhouse : pred.
-->
<Pig/>
<Wolf/>
<Sticks/>
<Bricks/>
<Straw/>
<StickHouse/>
<BrickHouse/>
<StrawHouse/>
</_-.XholonClass>
<xholonClassDetails>
<Avatar><Color>red</Color></Avatar>
<Items><Color>green</Color></Items>
<JsonRulesEngineTester><Color>indigo</Color></JsonRulesEngineTester>
</xholonClassDetails>
<PhysicalSystem>
<ItemsRoot>
<!-- all the ingredients necessary to make a FishingRod -->
<Items>
<String/>
<Stick/>
<Hook/>
</Items>
<!-- all the ingredients necessary to make 2 FishingRod, each from different materials -->
<Items>
<String/>
<Twig/>
<Stick/>
<Vine/>
<Hook/>
<Thorn/>
</Items>
<!-- all the ingredients necessary to make a FishingRod, plus more -->
<Items>
<Cat/>
<String/>
<Stick multiplicity="2"/>
<Lumber multiplicity="13"/>
<Hook/>
<Fish/>
</Items>
<!-- insufficient ingredients to make a FishingRod -->
<Items>
<String/>
<Cat/>
<Hook/>
</Items>
<!-- integers -->
<Items>
<Number roleName="1"/>
<Number roleName="2"/>
<Number roleName="3"/>
</Items>
<!-- Pawel Sobocinski - Graphical Linear Algebra (GLA) [ref 10] -->
<Items roleName="GLA">
<Egg multiplicity="2">
<White/>
<Yolk/>
</Egg>
<Sugar/>
<Mascarpone/>
</Items>
<!-- Three Little Pigs [ref 9]
context init =
{pig, pig, pig, wolf, straw, sticks, bricks}.
-->
<Items roleName="Pigs">
<Pig multiplicity="3"/>
<Wolf/>
<Straw/>
<Sticks/>
<Bricks/>
</Items>
</ItemsRoot>
<!-- alternative to MinecraftStyleRecipeBook -->
<JsonRulesEngineRecipeBook roleName="Island"><Attribute_String><![CDATA[
{
"conditions": {
"any": [{
"all": [{
"fact": "inventory",
"operator": "contains",
"value": "Stick",
"fate": "embed"
}, {
"fact": "inventory",
"operator": "contains",
"value": "String",
"fate": "embed"
}, {
"fact": "inventory",
"operator": "contains",
"value": "Hook",
"fate": "embed"
}, {
"fact": "wants",
"operator": "equal",
"value": "FishingRod"
}]
},
{
"all": [{
"fact": "inventory",
"operator": "contains",
"value": "Twig",
"fate": "retain"
}, {
"fact": "inventory",
"operator": "contains",
"value": "Vine",
"fate": "drop"
}, {
"fact": "inventory",
"operator": "contains",
"value": "Thorn",
"fate": "remove"
}, {
"fact": "wants",
"operator": "equal",
"value": "FishingRod"
}]
}]
},
"event": {
"type": "fishingRod",
"params": {
"message": "A FishingRod can be created."
}
}
}
]]></Attribute_String></JsonRulesEngineRecipeBook>
<JsonRulesEngineRecipeBook roleName="Hawaii"><Attribute_String><![CDATA[
[
{
"conditions": {
"any": [{
"all": [{
"fact": "inventory",
"operator": "contains",
"value": "Cat"
}, {
"fact": "inventory",
"operator": "contains",
"value": "Lumber"
}, {
"fact": "wants",
"operator": "equal",
"value": "LumberJackCat"
}]
}]
},
"event": {
"type": "lumberishy",
"params": {
"message": "A LumberJackCat can be created."
}
}
},
{
"conditions": {
"any": [{
"all": [{
"fact": "inventory",
"operator": "containsMultiple",
"value": "Lumber,5"
}, {
"fact": "wants",
"operator": "equal",
"value": "Cat"
}]
}]
},
"event": {
"type": "catty",
"params": {
"message": "A Cat can be created."
}
}
},
{
"conditions": {
"any": [{
"all": [{
"fact": "inventory",
"operator": "contains",
"value": "1",
"fate": "embed"
}, {
"fact": "inventory",
"operator": "contains",
"value": "3",
"fate": "embed"
}, {
"fact": "wants",
"operator": "equal",
"value": "Number",
"role": "4"
}]
}]
},
"event": {
"type": "numeric",
"params": {
"message": "A Number 4 can be created."
}
}
},
{
"conditions": {
"any": [{
"all": [{
"fact": "inventory",
"operator": "contains",
"value": "2",
"fate": "embed"
}, {
"fact": "inventory",
"operator": "contains",
"value": "4",
"fate": "embed"
}, {
"fact": "wants",
"operator": "equal",
"value": "Number",
"role": "6"
}]
}]
},
"event": {
"type": "numeric",
"params": {
"message": "A Number 6 can be created."
}
}
}
]
]]></Attribute_String></JsonRulesEngineRecipeBook>
<JsonRulesEngineRecipeBook roleName="Isola"><Attribute_String><![CDATA[
[
{
"conditions": {
"any": [{
"all": [{
"fact": "inventory",
"operator": "contains",
"value": "Egg",
"fate": "smash"
}, {
"fact": "wants",
"operator": "equal",
"value": "null"
}]
}]
},
"event": {
"type": "CrackEgg",
"params": {
"message": "An Egg can be cracked."
}
}
},
{
"conditions": {
"any": [{
"all": [{
"fact": "inventory",
"operator": "containsMultiple",
"value": "White,2"
}, {
"fact": "wants",
"operator": "equal",
"value": "WhiskedWhites"
}]
}]
},
"event": {
"type": "Whisk",
"params": {
"message": "Two Whites can be whisked."
}
}
},
{
"conditions": {
"any": [{
"all": [{
"fact": "inventory",
"operator": "containsMultiple",
"value": "Yolk,2"
}, {
"fact": "inventory",
"operator": "contains",
"value": "Sugar"
}, {
"fact": "wants",
"operator": "equal",
"value": "YolkyPaste"
}]
}]
},
"event": {
"type": "Beat",
"params": {
"message": "Two Yolks can be beaten."
}
}
},
{
"conditions": {
"any": [{
"all": [{
"fact": "inventory",
"operator": "contains",
"value": "YolkyPaste"
}, {
"fact": "inventory",
"operator": "contains",
"value": "Mascarpone"
}, {
"fact": "wants",
"operator": "equal",
"value": "ThickPaste"
}]
}]
},
"event": {
"type": "Stir",
"params": {
"message": "Ingredients can be stirred."
}
}
},
{
"conditions": {
"any": [{
"all": [{
"fact": "inventory",
"operator": "contains",
"value": "WhiskedWhites"
}, {
"fact": "inventory",
"operator": "contains",
"value": "ThickPaste"
}, {
"fact": "wants",
"operator": "equal",
"value": "CremaDiMascarpone"
}]
}]
},
"event": {
"type": "Fold",
"params": {
"message": "Ingredients can be folded."
}
}
}
]
]]></Attribute_String></JsonRulesEngineRecipeBook>
<!-- Three Little Pigs [ref 9]
stage go = {
build_straw : pig * straw -o strawhouse.
build_stick : pig * sticks -o stickhouse.
build_brick : pig * bricks -o brickhouse.
blow_straw : wolf * strawhouse -o wolf.
blow_stick : wolf * stickhouse -o wolf.
blow_brick : wolf * brickhouse -o brickhouse.
}
I have added fate: embed in the first 3 rules
-->
<JsonRulesEngineRecipeBook roleName="Pigs"><Attribute_String><![CDATA[
[
{
"conditions": {
"any": [{
"all": [{
"fact": "inventory",
"operator": "contains",
"value": "Pig",
"fate": "embed"
}, {
"fact": "inventory",
"operator": "contains",
"value": "Straw"
}, {
"fact": "wants",
"operator": "equal",
"value": "StrawHouse"
}]
}]
},
"event": {
"type": "build_straw",
"params": {
"message": "A StrawHouse can be created."
}
}
},
{
"conditions": {
"any": [{
"all": [{
"fact": "inventory",
"operator": "contains",
"value": "Pig",
"fate": "embed"
}, {
"fact": "inventory",
"operator": "contains",
"value": "Sticks"
}, {
"fact": "wants",
"operator": "equal",
"value": "StickHouse"
}]
}]
},
"event": {
"type": "build_stick",
"params": {
"message": "A StickHouse can be created."
}
}
},
{
"conditions": {
"any": [{
"all": [{
"fact": "inventory",
"operator": "contains",
"value": "Pig",
"fate": "embed"
}, {
"fact": "inventory",
"operator": "contains",
"value": "Bricks"
}, {
"fact": "wants",
"operator": "equal",
"value": "BrickHouse"
}]
}]
},
"event": {
"type": "build_brick",
"params": {
"message": "A BrickHouse can be created."
}
}
},
{
"conditions": {
"any": [{
"all": [{
"fact": "inventory",
"operator": "contains",
"value": "Wolf",
"fate": "retain"
}, {
"fact": "inventory",
"operator": "contains",
"value": "StrawHouse",
"fate": "smash"
}, {
"fact": "wants",
"operator": "equal",
"value": "null"
}]
}]
},
"event": {
"type": "blow_straw",
"params": {
"message": "A StrawHouse can be blown down."
}
}
},
{
"conditions": {
"any": [{
"all": [{
"fact": "inventory",
"operator": "contains",
"value": "Wolf",
"fate": "retain"
}, {
"fact": "inventory",
"operator": "contains",
"value": "StickHouse",
"fate": "smash"
}, {
"fact": "wants",
"operator": "equal",
"value": "null"
}]
}]
},
"event": {
"type": "blow_stick",
"params": {
"message": "A StickHouse can be blown down."
}
}
},
{
"conditions": {
"any": [{
"all": [{
"fact": "inventory",
"operator": "contains",
"value": "Wolf"
}, {
"fact": "inventory",
"operator": "contains",
"value": "BrickHouse",
"fate": "retain"
}, {
"fact": "wants",
"operator": "equal",
"value": "null"
}]
}]
},
"event": {
"type": "blow_brick",
"params": {
"message": "A BrickHouse can defeat the wolf."
}
}
},
{
"conditions": {
"any": [{
"all": [{
"fact": "inventory",
"operator": "contains",
"value": "Wolf",
"fate": "retain"
}, {
"fact": "inventory",
"operator": "contains",
"value": "Pig",
"fate": "eat"
}, {
"fact": "wants",
"operator": "equal",
"value": "null"
}]
}]
},
"event": {
"type": "blow_brick",
"params": {
"message": "A Wolf can eat a Pig."
}
}
}
]
]]></Attribute_String></JsonRulesEngineRecipeBook>
<!-- specify a specific "wants" for testing -->
<JsonRulesEngineTester roleName="Island" rbname="RecipeService-JsonRulesEngineRecipeBook-Island" wants="FishingRod"/>
<JsonRulesEngineTester roleName="Hawaii" rbname="RecipeService-JsonRulesEngineRecipeBook-Hawaii" wants="LumberJackCat,Cat,Number"/>
<JsonRulesEngineTester roleName="Isola" rbname="RecipeService-JsonRulesEngineRecipeBook-Isola" wants="null,WhiskedWhites,YolkyPaste,ThickPaste,CremaDiMascarpone"/>
<JsonRulesEngineTester roleName="Pigs" rbname="RecipeService-JsonRulesEngineRecipeBook-Pigs" wants="StrawHouse,StickHouse,BrickHouse,null"/>
<Animate selection='#xhgraph' duration='1'/>
</PhysicalSystem>
<JsonRulesEngineTesterbehavior implName="org.primordion.xholon.base.Behavior_gwtjs"><![CDATA[
const DEFAULT_FATE = "remove"; // by default, recipes should remove inventory items when there's a match
var me, engine, ready, ava, defaultFate, wantsAll, beh = {
postConfigure: function() {
me = this.cnode.parent();
me.println(me["rbname"]);
engine = null;
ready = false;
wantsAll = me["wants"].split(",");
if ($wnd.xh.html["selectTab"]) {
$wnd.xh.html.selectTab(0); // display contents of the "out" tab
}
ava = $wnd.xh.avatar();
if (ava.parent() == null) {
ava.action("appear;where;enter;enter;enter;where;look;");
}
this.initAvatarKeyMap();
ava.val(-1);
},
initAvatarKeyMap: function() {
var akm = $wnd.JSON.parse($wnd.xh.avatarKeyMap());
akm["0"] = "become this val 0;step;";
akm["1"] = "become this val 1;step;";
akm["2"] = "become this val 2;step;";
akm["3"] = "become this val 3;step;";
akm["4"] = "become this val 4;step;";
akm["5"] = "become this val 5;step;";
akm["6"] = "become this val 6;step;";
akm["7"] = "become this val 7;step;";
akm["8"] = "become this val 8;step;";
akm["9"] = "become this val 9;step;";
$wnd.xh.avatarKeyMap($wnd.JSON.stringify(akm));
},
act: function() {
if (!ready && $wnd["JsonRulesEngine"]) {
ready = true;
var rserv = $wnd.xh.service("RecipeService");
var rbname = me["rbname"]; //"RecipeService-JsonRulesEngineRecipeBook-Island";
var recipebook = null;
if (rserv) {
var rmsg = rserv.call(-3897, rbname, me);
recipebook = rmsg.data;
}
if (Array.isArray(recipebook)) {
engine = new $wnd.JsonRulesEngine.Engine(recipebook);
}
else {
engine = new $wnd.JsonRulesEngine.Engine();
//let rule = recipebook; // all rules are in the one rule structure for now
engine.addRule(recipebook);
}
// Define a 'containsMultiple' custom operator, for use in later rules
engine.addOperator('containsMultiple', (factValue, ruleValue) => {
var ruleValueArr = ruleValue.split(","); // ex: "Lumber,2"
var count = 0;
for (var i = 0; i < factValue.length; i++) {
var fval = factValue[i];
if (fval == ruleValueArr[0]) {
count++;
}
}
return count >= ruleValueArr[1];
})
engine.on('success', (event, almanac, ruleResult) => {
me.println(event.params.message);
beh.render(ruleResult);
})
//engine.on('failure', function(event, almanac, ruleResult) {
// $wnd.console.log(event);
//})
ava.action("script;param repeat true;\ntake;\nwait 3;\ndrop;\nnext;");
}
if (ready) {
// update avatarFacts every timestep
var rnum = ava.val(); // user may have pressed [0-9]
if ((rnum == -1) || (rnum >= wantsAll.length)) {
rnum = Math.floor(Math.random() * wantsAll.length);
}
else {
me.println(wantsAll + " " + rnum);
}
var invArr = this.createAvaInvArr();
let avatarFacts = {
inventory: invArr,
wants: wantsAll[rnum] //me["wants"]
}
engine.run(avatarFacts);
ava.val(-1);
}
},
createAvaInvArr: function() {
// create an Avatar inventory array
var iarr = [];
var node = ava.first();
while (node) {
iarr.push(node.name("R^^^^^"));
node = node.next();
}
return iarr;
},
render: function(ruleResult) {
// render the results from engine.on('success'
var anyconds = ruleResult.conditions.any;
for (var i = 0; i < anyconds.length; i++) {
var anycond = anyconds[i];
if (anycond.result) {
var conds = anycond.all;
var embedArr = [];
conds.forEach(function(cond) {
if (cond.fact == "inventory") {
// handle cond.operator == "containsMultiple" ex: cond.value = "Lumber,2"
var itemNum = 1;
var itemName = cond.value;
var fate = cond["fate"] || DEFAULT_FATE;
//me.println("fate: " + fate);
if (cond.operator == "containsMultiple") {
valArr = itemName.split(",")
itemName = valArr[0];
itemNum = valArr[1];
}
itemName = itemName.charAt(0).toLowerCase() + itemName.substring(1);
for (var j = 0; j < itemNum; j++) {
ava.action("x " + itemName + ";");
switch (fate) {
case "retain": // retain this item in the inventory
break;
case "drop": // drop this inventory item
ava.action("drop " + itemName + ";");
break;
case "embed": // embed this inventory item into the "wants" node once it's built
embedArr.push(itemName);
break;
case "smash":
ava.action("drop " + itemName + ";" + "smash " + itemName + ";");
break;
case "eat":
ava.action("eat " + itemName + ";");
break;
case "remove": // remove this inventory item from the game
default:
ava.action("unbuild " + itemName + ";");
break;
}
}
}
else if (cond.fact == "wants") {
var itemName = cond.value;
if (itemName == "null") {
// do nothing
}
else {
var roleName = cond["role"];
var roleNameStr = "";
if (roleName) {
roleNameStr = " role " + roleName;
}
else {
roleName = "";
}
me.println("about to build a " + itemName + roleNameStr);
ava.action("build " + itemName + roleNameStr + ";");
if (embedArr) {
itemName = itemName.charAt(0).toLowerCase() + itemName.substring(1);
if (roleName) {
roleName = roleName + ":";
}
embedArr.forEach(function(embedName) {
ava.action("put " + embedName + " in " + roleName + itemName + ";");
});
embedArr = [];
}
}
}
});
break; // break out of for loop
} // end if
} // end for loop
}
}
//# sourceURL=JsonRulesEngineTesterbehavior.js
]]></JsonRulesEngineTesterbehavior>
<SvgClient><Attribute_String roleName="svgUri"><![CDATA[data:image/svg+xml,
<svg width="100" height="50" xmlns="http://www.w3.org/2000/svg">
<g>
<title>JsonRulesEngineTester</title>
<rect id="PhysicalSystem/JsonRulesEngineTester" fill="#98FB98" height="50" width="50" x="25" y="0"/>
</g>
</svg>
]]></Attribute_String><Attribute_String roleName="setup">${MODELNAME_DEFAULT},${SVGURI_DEFAULT}</Attribute_String></SvgClient>
</XholonWorkbook>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment