behavior-promise
Version:
Organize complex code execution inJavascript using behavior trees
478 lines (424 loc) • 25.5 kB
JavaScript
// BEHAVIOR-PROMISE
var yaml = require('js-yaml');
var fs = require('fs');
// // // Create a new behavior tree and return its instance
// Input:
// config: (Required) The configuration object to create for the tree. May contain the following options:
// root: (Required) The root node of the tree, containing all nodes. May be an object or a string
// Root parsing is performed according to the option rootFormat
// rootFormat: (Optional) Determine the way to parse the given root, in order to build the tree.
// Default value: If root is a string, default rootFormat is 'json'. If root is an object, default rootFormat is 'object'
// rootFormat may be one of the following:
// 'object': Root is a tree object already. Do not parse it
// 'json': Root is a JSON string. Parse it to get the tree object
// 'jsonfile': Root is the filename of a JSON text file. Load and parse it to get the tree object
// 'yaml': Root is a YAML text string. Parse it to get the tree object
// 'yamlfile': Root is the filename of a YAML text file. Load and parse it to get the tree object
// actions: (Required) An object containing all the available actions as properties
// Property key is the action path key (see run)
// Property value is either a function (the action) or an object containing more properties
//
exports.create = function(config) {
var tree = new Tree(config);
return tree;
}
// Available node types
var nodeTypes = {
action: {leaf: true}, // An action
seq: {leaf: false}, // Sequence: Execute actions until fail
sel: {leaf: false}, // Selector: Execute actions until succeed
par: {leaf: false}, // Parallel: Execute actions until max fails of max succeeds
invert: {leaf: false, decorator: true}, // Invertor: Execute action, then invert result
success: {leaf: false, decorator: true}, // Success: Execute action, then always report success
failure: {leaf: false, decorator: true}, // Fail: Execute action, then always report failure
};
// Available action types
var actionTypes = {
promise: {},
callback: {},
boolean: {},
plain: {},
};
// Tree class
function Tree(treeConfig) {
var _this = this;
this.root = null;
this.config = null;
this.actions = null;
this.error = null;
// Debugging
this.debug = {
enable: false, // Debug status. true = enable writing in console.log
stack: [], // When a .debug property is found in a node, push here previous debug status
prefix: ' ', // Write this string before each debug message
tabWidth: 4, // Increase prefix by this size on each node entry
open: function(node) {
// Push new debug state into debugStack
if (node.hasOwnProperty('debug')) {
_this.debug.stack.push(_this.debug.enable);
_this.debug.enable = node.debug;
}
if (_this.debug.enable) {
ds = _this.debug.prefix;
ds += 'NODE(type='+node.type;
if (node.title)
ds+=', title='+node.title;
if (node.type=='action') {
ds +=', actionType='+node.actionType;
}
ds += ')';
console.log(ds);
_this.debug.prefix += ' '.repeat(_this.debug.tabWidth);
}
},
close: function(node) {
// Pop debug state from stack
if (_this.debug.enable) {
_this.debug.prefix = _this.debug.prefix.slice(0,-_this.debug.tabWidth);
}
if (node.hasOwnProperty('debug')) {
_this.debug.enable = _this.debug.stack.pop();
}
},
};
// Prepare for execution recursively a node during tree initialization
this.prepare = function(node, parent){
// Ensure a valid .type exists
// If type is not given, search node properties and return the first valid type property. If no such property was found, set type to 'seq'.
var nodeType = node.type || null;
if (!nodeType) { // If .type is not given
// Search for a type specific property
Object.keys(nodeTypes).forEach(function(aType){ // For each type specific property
if (node.hasOwnProperty(aType)) { // If property exists
// There can be only one! (type property)
if (nodeType===null)
nodeType = aType;
// TODO: else error
}
});
if (nodeType) // If type property found
node.type = nodeType;
} else { // If .type is given
// Type must be a valid one
if (!nodeTypes[nodeType])
nodeType = null;
}
if (!nodeType) { // If could not determine the node type
// TODO: Report error
return;
}
// Ensure a valid .actionType exists (specific type, parent type, default type)
if (!node.hasOwnProperty('actionType')) { // If actionType is not given
node.actionType = parent.actionType || 'plain';
}
if (!actionTypes[node.actionType]) {
_this.error = 'Invalid action type '+node.actionType;
return;
}
// If node is a leaf
if (nodeTypes[nodeType].leaf) {
// Get the function
var typeofAction = typeof node.action
switch (typeofAction) {
case 'string':
// From action path (AAA, AAA.BB, AAA.BB.C etc) -> Locate function (actions[AAA], actions[AAA][BB], actions[AAA][BB][C] etc)
node.fn = node.action.split('.').reduce(function (prev,curr){return prev[curr]}, _this.actions);
break;
case 'function':
node.fn = node.action;
break;
default:
_this.error = 'Invalid typeof action: '+typeofAction;
break;
}
} else { // If node is not a leaf
// Ensure .nodes exists
// Create a .nodes property if it does not exist
if (!node.hasOwnProperty('nodes')) { // If there is no nodes property, create it
if (node.hasOwnProperty(nodeType)) // If there is a type property
node.nodes = node[nodeType];
// TODO: Else error
}
// Prepare recursively all children of this node
for (var i in node.nodes) {
if (typeof node.nodes[i] === 'string')
node.nodes[i] = {action: node.nodes[i]+''};
_this.prepare(node.nodes[i], node);
}
}
};
// Run a node of the tree
// runNode(node, parentScope) returns a promise to run this node
// Node is an object or a string:
// If node is a string SSSSS, then it is the equivalent to object {action:SSSSS}
// If node is an object, then it may have the following properties:
// type: A string indicating the node type.
// May be one of: 'action','seq','sel','invert','success','failure'
// Node must also have a property with a name similar to this value. For example, if node.type='seq', node should also have a node.seq property.
// If type is not given, search node properties and return the first valid type property. If no such property was found, set type to 'seq'.
// actionType: If node is an action, define here the action type. If node is not an action, define here the default action type of the children nodes.
// May be one of : 'promise','callback','boolean','plain' (see action for definitions)
// Default is going up parent actionType. If not found and no children specific property is given, actionType='plain'.
// scope: A scope object, containing a set of variables as properties. Each property X is available as this.X inside the actions of this node and its child nodes
// An action, through this, can access its scope and all its parent nodes scopes as one scope.
// In order to avoid bugs, it is considered an error to have a property with the same name in a node scope and in any of its children nodes scope
// title: A string describing this node
// Useful for editing the tree
//
// action: Action to execute.
// May be a word string, an object path string or a function:
// - If word string (AAAA), execute the function from tree.config.actions[AAAA]
// - If object path string (AAA.BBB.CCC), execute the function from tree.config.actions[AAA][BBB][CCC]
// - If function, execute directly this function
// The function will be invoked according to the rules of the specified actionType:
// - promise: Invoke a promise function
// If rejected or exception occurs, return failure. If fulfilled, return success
// - callback: Invoke a function of type f(input,cb)
// input is an optional argument
// cb is a callback function of type cb(err,result)
// When done, cb is called. If err!==null or exception occurs, return failure. If err === null, return success
// - boolean: Invoke a function of type f(input)
// input is an optional argument
// When done, function must return a true/false result. If result = false or exception occurs, return failure. If result = true, return success
// - plain: Invoke a plain function(input)
// input is an optional argument
// When done, function must just return. If exception occurs, return failure. If no exception occurs, return success
// seq: Sequence. An array with child nodes to execute in sequence.
// When a failure occurs in a child node, stop the sequence and return a failure.
// If all nodes succeeded, return a success
// sel: Priority selector. An array with nodes to select one
// Try to execute all child nodes in sequence, from first to last
// When a child node succeeds, stop the sequence and return immediately success
// When a child node fails, try the next node in sequence
// If all nodes failed, return a failure
// par: Parallel execution. An array with nodes to run in parallel
// The node will end when maxSuccess is triggered, when maxFailure is triggered or when all child nodes end
// maxSuccess: When set inside a parallel container, defines how many of its child nodes must end with success in order trigger a container success.
// May be:
// A number (1,2,3,...), indicating how many child nodes must end with a success
// true, indicating that all child nodes must end with a success
// false, indicating that container should never check for a success
// maxFail: When set inside a parallel container, defines how many of its child nodes must end with failure in order to trigger a container failure.
// May be:
// A number (1,2,3,...), indicating how many child nodes must end with a failure
// true, indicating that all child nodes must end with a failure
// false, indicating that container should never check for a failure
// waitForMe If set to true in a parallel child, then parallel will wait for this node to end before returning
// waitForAll If set to true in a parallel container, then parallel will wait for all of its child nodes to end before returning
// invert: Invert decorator. Execute the child node, reverse its result (success<->failure) and return it
// success: Success Decorator. Execute the child node and return success, no matter what the child node returned
// failure: Failure Decorator. Execute the child node and return failure, no matter what the child node returned
// debug: TODO: if set and true, print debug information in console while executing
// nodes: Alternative children object. Use this to name the children property with a fixed name, instead of having a different property for each style
// Use either this or a style specific property (seq,sel,inv)
//
var runNode = function(node, parentScope) {
var result; // Result to return
var ds; // Debug string
_this.debug.open(node);
// Get this scope
var scope = node.scope || {};
// Mix with parent scope
for (var i in parentScope) {
if (!scope.hasOwnProperty(i))
scope[i] = parentScope[i];
}
switch (node.type) {
case 'action':
switch (node.actionType) {
case 'promise':
return function(input) {
if (_this.debug.enable)
console.log(_this.debug.prefix+'Input='+input);
return node.fn.call(scope,input);
}
break;
case 'callback':
return function(input) {
if (_this.debug.enable)
console.log(_this.debug.prefix+'Input='+input);
return new Promise(function(resolve, reject){
try {
//..
node.fn.call(scope, input, function(err,res){
if (err)
reject(err);
else
resolve(res);
});
} catch (ex) {
console.log(ex);
reject(ex);
}
});
}
break;
case 'boolean':
return function(input) {
if (_this.debug.enable)
console.log(_this.debug.prefix+'Input='+input);
return new Promise(function(resolve, reject){
try {
result = node.fn.call(scope, input);
if (result)
resolve(result);
else
reject(result);
} catch (ex) {
reject(ex);
}
});
}
case 'plain':
return function(input) {
if (_this.debug.enable)
console.log(_this.debug.prefix+'Input='+input);
return new Promise(function(resolve, reject){
try {
result = node.fn.call(scope, input);
resolve(result);
} catch (ex) {
reject(ex);
}
});
}
break;
}
break;
case 'seq':
// Execute all childs until failure.
// Return first failure or last success
// Each child gets its input from the previous child success
return function(input) {
var chain = Promise.resolve(input);
node.nodes.forEach(function(child){
chain = chain.then(function(inp){
return runNode(child,scope)(inp);
});
});
return chain;
}
break;
case 'sel':
// Execute all childs until success.
// Return first success or last failure
// Each child gets its input from the previous child failure
return function(input) {
var chain = Promise.reject(input);
node.nodes.forEach(function(child){
chain = chain.then(
function onSuccess(inp){
return Promise.resolve(inp);
},
function onFailure(inp){
return runNode(child,scope)(inp);
}
);
});
return chain;
}
break;
case 'invert':
// Invert decorator. Execute the first child node
// If child returned a result as success, return this result as failure
// If child returned a result as failure, return this result as success
return function(input) {
var child = node.nodes[0];
return runNode(child,scope)(input).then(
function onSuccess(inp){
return Promise.reject(inp);
},
function onFailure(inp){
return Promise.resolve(inp);
}
);
}
break;
case 'success':
// Success Decorator. Execute the first child node
// Return its success or failure result as a success
return function(input) {
var child = node.nodes[0];
return runNode(child,scope)(input).then(
function onSuccess(res){
return Promise.resolve(res);
},
function onFailure(res){
return Promise.resolve(res);
}
);
}
break;
case 'failure':
// Success Decorator. Execute the first child node
// Return its success or failure result as a success
return function(input) {
var child = node.nodes[0];
return runNode(child,scope)(input).then(
function onSuccess(res){
return Promise.reject(res);
},
function onFailure(res){
return Promise.reject(res);
}
);
}
break;
case 'par':
// Execute all childs in parallel.
// Consider maxSuccess, maxFail, waitForMe, waitForAll
// Result is Success when maxSuccess is satisfied (output value is the output value of the last child node that succeeded)
// Result is Failure when maxFailure is satisfied (output value is the output value of the last child node that failed)
// If neither maxSuccess nor maxFailure satisfied but all child nodes returned, return with the result and the output value of the last child node
// Each child gets the same input from the parent parallel container input
/*
* TODO
var parallel = Promise.reject(input);
node.nodes.forEach(function(child){
chain = chain.then(
function onSuccess(inp){
return Promise.resolve(inp);
},
function onFailure(inp){
return runNode(child,scope)(inp);
}
);
});
*/
break;
}
};
// Init object
this.config = treeConfig;
// Convert root according to rootFormat
this.root = treeConfig.root || (this.error = 'No root found');
this.rootFormat = treeConfig.rootFormat || (typeof this.root === 'object' ? 'object': 'json');
switch (this.rootFormat) {
case 'object':
// Do nothing, tree is ready. Of course, it needs to be prepared before execution
break;
case 'json':
this.root = JSON.parse(this.root);
break;
case 'jsonfile':
this.root = JSON.parse(fs.readFileSync(this.root,'utf8'));
break;
case 'yaml':
this.root = yaml.safeLoad(this.root);
break;
case 'yamlfile':
this.root = yaml.safeLoad(fs.readFileSync(this.root,'utf8'));
break;
default:
this.error = 'Unknown rootFormat';
return;
break;
}
this.actions = treeConfig.actions || {};
this.prepare(this.root,{actionType:'plain'});
// console.log(this.root);
// Run the tree
this.run = function(input){
return runNode(_this.root, {})(input);
}
}