mistreevous
Version:
A library to declaratively define, build and execute behaviour trees, written in TypeScript for Node and browsers
1,338 lines (1,323 loc) • 108 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// node_modules/lotto-draw/dist/Participant.js
var require_Participant = __commonJS({
"node_modules/lotto-draw/dist/Participant.js"(exports2) {
"use strict";
Object.defineProperty(exports2, "__esModule", { value: true });
exports2.Participant = void 0;
var Participant = (
/** @class */
function() {
function Participant2(participant, tickets) {
if (tickets === void 0) {
tickets = 1;
}
this._participant = participant;
this._tickets = tickets;
}
Object.defineProperty(Participant2.prototype, "participant", {
/** Gets the actual participant. */
get: function() {
return this._participant;
},
enumerable: false,
configurable: true
});
Object.defineProperty(Participant2.prototype, "tickets", {
/** Gets or sets the number of tickets held by the participant. */
get: function() {
return this._tickets;
},
set: function(value) {
this._tickets = value;
},
enumerable: false,
configurable: true
});
return Participant2;
}()
);
exports2.Participant = Participant;
}
});
// node_modules/lotto-draw/dist/Utilities.js
var require_Utilities = __commonJS({
"node_modules/lotto-draw/dist/Utilities.js"(exports2) {
"use strict";
Object.defineProperty(exports2, "__esModule", { value: true });
exports2.isNaturalNumber = exports2.isNullOrUndefined = void 0;
function isNullOrUndefined2(value) {
return value === null || value === void 0;
}
exports2.isNullOrUndefined = isNullOrUndefined2;
function isNaturalNumber(value) {
return typeof value === "number" && value >= 1 && Math.floor(value) === value;
}
exports2.isNaturalNumber = isNaturalNumber;
}
});
// node_modules/lotto-draw/dist/Lotto.js
var require_Lotto = __commonJS({
"node_modules/lotto-draw/dist/Lotto.js"(exports2) {
"use strict";
Object.defineProperty(exports2, "__esModule", { value: true });
exports2.Lotto = void 0;
var Participant_1 = require_Participant();
var Utilities_1 = require_Utilities();
var Lotto2 = (
/** @class */
function() {
function Lotto3(customRandom) {
this._participants = [];
this._customRandom = customRandom;
}
Lotto3.prototype.add = function(participant, tickets) {
if (tickets === void 0) {
tickets = 1;
}
if (!(0, Utilities_1.isNaturalNumber)(tickets)) {
throw new Error("tickets value must be a natural number");
}
var existingParticipant = this._participants.find(function(part) {
return part.participant === participant;
});
if (existingParticipant) {
existingParticipant.tickets += tickets;
} else {
this._participants.push(new Participant_1.Participant(participant, tickets));
}
return this;
};
Lotto3.prototype.remove = function(participant, tickets) {
var existingParticipant = this._participants.find(function(part) {
return part.participant === participant;
});
if (!existingParticipant) {
return this;
}
if (tickets !== void 0) {
if (!(0, Utilities_1.isNaturalNumber)(tickets)) {
throw new Error("tickets value must be a natural number");
}
existingParticipant.tickets -= tickets;
if (existingParticipant.tickets < 1) {
this._participants = this._participants.filter(function(part) {
return part !== existingParticipant;
});
}
} else {
this._participants = this._participants.filter(function(part) {
return part !== existingParticipant;
});
}
return this;
};
Lotto3.prototype.draw = function(options) {
if (options === void 0) {
options = {};
}
if (this._participants.length === 0) {
return null;
}
var redrawable = (0, Utilities_1.isNullOrUndefined)(options.redrawable) ? true : options.redrawable;
var pickable = [];
this._participants.forEach(function(_a) {
var participant = _a.participant, tickets = _a.tickets;
for (var ticketCount = 0; ticketCount < tickets; ticketCount++) {
pickable.push(participant);
}
});
var random;
if (this._customRandom) {
random = this._customRandom();
if (typeof random !== "number" || random < 0 || random >= 1) {
throw new Error("the 'random' function provided did not return a number between 0 (inclusive) and 1");
}
} else {
random = Math.random();
}
var winner = pickable[Math.floor(random * pickable.length)];
if (!redrawable) {
this.remove(winner, 1);
}
return winner;
};
Lotto3.prototype.drawMultiple = function(tickets, options) {
if (options === void 0) {
options = {};
}
var uniqueResults = (0, Utilities_1.isNullOrUndefined)(options.unique) ? false : options.unique;
if (tickets === 0) {
return [];
}
if (!(0, Utilities_1.isNaturalNumber)(tickets)) {
throw new Error("tickets value must be a natural number");
}
var result = [];
while (result.length < tickets && this._participants.length > 0) {
result.push(this.draw(options));
}
if (uniqueResults) {
var unique = [];
for (var _i = 0, result_1 = result; _i < result_1.length; _i++) {
var participant = result_1[_i];
if (unique.indexOf(participant) === -1) {
unique.push(participant);
}
}
result = unique;
}
return result;
};
return Lotto3;
}()
);
exports2.Lotto = Lotto2;
}
});
// node_modules/lotto-draw/dist/createLotto.js
var require_createLotto = __commonJS({
"node_modules/lotto-draw/dist/createLotto.js"(exports2) {
"use strict";
Object.defineProperty(exports2, "__esModule", { value: true });
exports2.createLotto = void 0;
var Lotto_1 = require_Lotto();
function createLotto2(participantsOrOptions) {
if (!participantsOrOptions) {
return new Lotto_1.Lotto();
}
if (Array.isArray(participantsOrOptions)) {
var participants = participantsOrOptions;
var lotto_1 = new Lotto_1.Lotto();
participants.forEach(function(_a) {
var participant = _a[0], tokens = _a[1];
return lotto_1.add(participant, tokens);
});
return lotto_1;
} else {
var random = participantsOrOptions.random, participants = participantsOrOptions.participants;
var lotto_2 = new Lotto_1.Lotto(random);
if (participants) {
participants.forEach(function(_a) {
var participant = _a[0], tokens = _a[1];
return lotto_2.add(participant, tokens);
});
}
return lotto_2;
}
}
exports2.createLotto = createLotto2;
}
});
// node_modules/lotto-draw/dist/index.js
var require_dist = __commonJS({
"node_modules/lotto-draw/dist/index.js"(exports2) {
"use strict";
Object.defineProperty(exports2, "__esModule", { value: true });
var createLotto_1 = require_createLotto();
exports2.default = createLotto_1.createLotto;
}
});
// src/index.ts
var index_exports = {};
__export(index_exports, {
BehaviourTree: () => BehaviourTree,
State: () => State,
convertMDSLToJSON: () => convertMDSLToJSON,
validateDefinition: () => validateDefinition
});
module.exports = __toCommonJS(index_exports);
// src/State.ts
var State = /* @__PURE__ */ ((State2) => {
State2["READY"] = "mistreevous.ready";
State2["RUNNING"] = "mistreevous.running";
State2["SUCCEEDED"] = "mistreevous.succeeded";
State2["FAILED"] = "mistreevous.failed";
return State2;
})(State || {});
// src/Lookup.ts
var Lookup = class {
/**
* The object holding any registered functions keyed on function name.
*/
static registeredFunctions = {};
/**
* The object holding any registered subtree root node definitions keyed on tree name.
*/
static registeredSubtrees = {};
/**
* Gets the function with the specified name.
* @param name The name of the function.
* @returns The function with the specified name.
*/
static getFunc(name) {
return this.registeredFunctions[name];
}
/**
* Sets the function with the specified name for later lookup.
* @param name The name of the function.
* @param func The function.
*/
static setFunc(name, func) {
this.registeredFunctions[name] = func;
}
/**
* Gets the function invoker for the specified agent and function name.
* If a function with the specified name exists on the agent object then it will
* be returned, otherwise we will then check the registered functions for a match.
* @param agent The agent instance that this behaviour tree is modelling behaviour for.
* @param name The function name.
* @returns The function invoker for the specified agent and function name.
*/
static getFuncInvoker(agent, name) {
const processFunctionArguments = (args) => args.map((arg) => {
if (typeof arg === "object" && arg !== null && Object.keys(arg).length === 1 && Object.prototype.hasOwnProperty.call(arg, "$")) {
const agentPropertyName = arg["$"];
if (typeof agentPropertyName !== "string" || agentPropertyName.length === 0) {
throw new Error("Agent property reference must be a string?");
}
return agent[agentPropertyName];
}
return arg;
});
const agentFunction = agent[name];
if (agentFunction && typeof agentFunction === "function") {
return (args) => agentFunction.apply(agent, processFunctionArguments(args));
}
if (this.registeredFunctions[name] && typeof this.registeredFunctions[name] === "function") {
const registeredFunction = this.registeredFunctions[name];
return (args) => registeredFunction(agent, ...processFunctionArguments(args));
}
return null;
}
/**
* Gets all registered subtree root node definitions.
*/
static getSubtrees() {
return this.registeredSubtrees;
}
/**
* Sets the subtree with the specified name for later lookup.
* @param name The name of the subtree.
* @param subtree The subtree.
*/
static setSubtree(name, subtree) {
this.registeredSubtrees[name] = subtree;
}
/**
* Removes the registered function or subtree with the specified name.
* @param name The name of the registered function or subtree.
*/
static remove(name) {
delete this.registeredFunctions[name];
delete this.registeredSubtrees[name];
}
/**
* Remove all registered functions and subtrees.
*/
static empty() {
this.registeredFunctions = {};
this.registeredSubtrees = {};
}
};
// src/BehaviourTreeDefinitionUtilities.ts
function isRootNodeDefinition(node) {
return node.type === "root";
}
function isBranchNodeDefinition(node) {
return node.type === "branch";
}
function isLeafNodeDefinition(node) {
return ["branch", "action", "condition", "wait"].includes(node.type);
}
function isDecoratorNodeDefinition(node) {
return ["root", "repeat", "retry", "flip", "succeed", "fail"].includes(node.type);
}
function isCompositeNodeDefinition(node) {
return ["sequence", "selector", "lotto", "parallel", "race", "all"].includes(node.type);
}
function flattenDefinition(nodeDefinition) {
const nodes = [];
const processNode = (currentNodeDefinition) => {
nodes.push(currentNodeDefinition);
if (isCompositeNodeDefinition(currentNodeDefinition)) {
currentNodeDefinition.children.forEach(processNode);
} else if (isDecoratorNodeDefinition(currentNodeDefinition)) {
processNode(currentNodeDefinition.child);
}
};
processNode(nodeDefinition);
return nodes;
}
function isInteger(value) {
return typeof value === "number" && Math.floor(value) === value;
}
function isNullOrUndefined(value) {
return typeof value === "undefined" || value === null;
}
// src/mdsl/MDSLArguments.ts
function getArgumentJsonValue(arg) {
if (arg.type === "property_reference") {
return { $: arg.value };
}
return arg.value;
}
// src/mdsl/MDSLUtilities.ts
function popAndCheck(tokens, expected) {
const popped = tokens.shift();
if (popped === void 0) {
throw new Error("unexpected end of definition");
}
if (expected != void 0) {
const expectedValues = typeof expected === "string" ? [expected] : expected;
const tokenMatchesExpectation = expectedValues.some((item) => popped.toUpperCase() === item.toUpperCase());
if (!tokenMatchesExpectation) {
const expectationString = expectedValues.map((item) => "'" + item + "'").join(" or ");
throw new Error("unexpected token found. Expected " + expectationString + " but got '" + popped + "'");
}
}
return popped;
}
function tokenise(definition) {
definition = definition.replace(/\/\*(.|\n)*?\*\//g, "");
const { placeholders, processedDefinition } = substituteStringLiterals(definition);
definition = processedDefinition.replace(/\(/g, " ( ");
definition = definition.replace(/\)/g, " ) ");
definition = definition.replace(/\{/g, " { ");
definition = definition.replace(/\}/g, " } ");
definition = definition.replace(/\]/g, " ] ");
definition = definition.replace(/\[/g, " [ ");
definition = definition.replace(/,/g, " , ");
return {
// Split the definition into raw token form.
tokens: definition.replace(/\s+/g, " ").trim().split(" "),
// The placeholders for string literals that were found in the definition.
placeholders
};
}
function substituteStringLiterals(definition) {
const placeholders = {};
const processedDefinition = definition.replace(/"(\\.|[^"\\])*"/g, (match) => {
const strippedMatch = match.substring(1, match.length - 1);
let placeholder = Object.keys(placeholders).find((key) => placeholders[key] === strippedMatch);
if (!placeholder) {
placeholder = `@@${Object.keys(placeholders).length}@@`;
placeholders[placeholder] = strippedMatch;
}
return placeholder;
});
return { placeholders, processedDefinition };
}
// src/mdsl/MDSLNodeArgumentParser.ts
function parseArgumentTokens(tokens, stringArgumentPlaceholders) {
const argumentList = [];
if (!["[", "("].includes(tokens[0])) {
return argumentList;
}
const closingToken = popAndCheck(tokens, ["[", "("]) === "[" ? "]" : ")";
const argumentListTokens = [];
while (tokens.length && tokens[0] !== closingToken) {
argumentListTokens.push(tokens.shift());
}
argumentListTokens.forEach((token, index) => {
const shouldBeArgumentToken = !(index & 1);
if (shouldBeArgumentToken) {
const argumentDefinition = getArgumentDefinition(token, stringArgumentPlaceholders);
argumentList.push(argumentDefinition);
} else {
if (token !== ",") {
throw new Error(`invalid argument list, expected ',' or ']' but got '${token}'`);
}
}
});
popAndCheck(tokens, closingToken);
return argumentList;
}
function getArgumentDefinition(token, stringArgumentPlaceholders) {
if (token === "null") {
return {
value: null,
type: "null"
};
}
if (token === "true" || token === "false") {
return {
value: token === "true",
type: "boolean"
};
}
if (!isNaN(token)) {
return {
value: parseFloat(token),
isInteger: parseFloat(token) === parseInt(token, 10),
type: "number"
};
}
if (token.match(/^@@\d+@@$/g)) {
return {
value: stringArgumentPlaceholders[token].replace('\\"', '"'),
type: "string"
};
}
if (token.match(/^\$[_a-zA-Z][_a-zA-Z0-9]*/g)) {
return {
// The value is the identifier name with the '$' prefix removed.
value: token.slice(1),
type: "property_reference"
};
}
return {
value: token,
type: "identifier"
};
}
// src/mdsl/MDSLNodeAttributeParser.ts
function parseAttributeTokens(tokens, stringArgumentPlaceholders) {
const nodeAttributeNames = ["while", "until", "entry", "exit", "step"];
const attributes = {};
let nextAttributeName = tokens[0]?.toLowerCase();
while (nodeAttributeNames.includes(nextAttributeName)) {
if (attributes[nextAttributeName]) {
throw new Error(`duplicate attribute '${tokens[0].toUpperCase()}' found for node`);
}
tokens.shift();
const [attributeCallIdentifier, ...attributeArguments] = parseArgumentTokens(
tokens,
stringArgumentPlaceholders
);
if (attributeCallIdentifier?.type !== "identifier") {
throw new Error("expected agent function or registered function name identifier argument for attribute");
}
attributeArguments.filter((arg) => arg.type === "identifier").forEach((arg) => {
throw new Error(
`invalid attribute argument value '${arg.value}', must be string, number, boolean, agent property reference or null`
);
});
if (nextAttributeName === "while" || nextAttributeName === "until") {
let succeedOnAbort = false;
if (tokens[0]?.toLowerCase() === "then") {
tokens.shift();
const resolvedStatusToken = popAndCheck(tokens, ["succeed", "fail"]);
succeedOnAbort = resolvedStatusToken.toLowerCase() === "succeed";
}
attributes[nextAttributeName] = {
call: attributeCallIdentifier.value,
args: attributeArguments.map(getArgumentJsonValue),
succeedOnAbort
};
} else {
attributes[nextAttributeName] = {
call: attributeCallIdentifier.value,
args: attributeArguments.map(getArgumentJsonValue)
};
}
nextAttributeName = tokens[0]?.toLowerCase();
}
return attributes;
}
// src/mdsl/MDSLDefinitionParser.ts
function convertMDSLToJSON(definition) {
const { tokens, placeholders } = tokenise(definition);
return convertTokensToJSONDefinition(tokens, placeholders);
}
function convertTokensToJSONDefinition(tokens, stringLiteralPlaceholders) {
if (tokens.length < 3) {
throw new Error("invalid token count");
}
if (tokens.filter((token) => token === "{").length !== tokens.filter((token) => token === "}").length) {
throw new Error("scope character mismatch");
}
const treeStacks = [];
const rootNodes = [];
const pushNode = (node) => {
if (isRootNodeDefinition(node)) {
if (treeStacks[treeStacks.length - 1]?.length) {
throw new Error("a root node cannot be the child of another node");
}
rootNodes.push(node);
treeStacks.push([node]);
return;
}
if (!treeStacks.length || !treeStacks[treeStacks.length - 1].length) {
throw new Error("expected root node at base of definition");
}
const topTreeStack = treeStacks[treeStacks.length - 1];
const topTreeStackTopNode = topTreeStack[topTreeStack.length - 1];
if (isCompositeNodeDefinition(topTreeStackTopNode)) {
topTreeStackTopNode.children = topTreeStackTopNode.children || [];
topTreeStackTopNode.children.push(node);
} else if (isDecoratorNodeDefinition(topTreeStackTopNode)) {
if (topTreeStackTopNode.child) {
throw new Error("a decorator node must only have a single child node");
}
topTreeStackTopNode.child = node;
}
if (!isLeafNodeDefinition(node)) {
topTreeStack.push(node);
}
};
const popNode = () => {
let poppedNode = null;
const topTreeStack = treeStacks[treeStacks.length - 1];
if (topTreeStack.length) {
poppedNode = topTreeStack.pop();
}
if (!topTreeStack.length) {
treeStacks.pop();
}
return poppedNode;
};
while (tokens.length) {
const token = tokens.shift();
switch (token.toUpperCase()) {
case "ROOT": {
pushNode(createRootNode(tokens, stringLiteralPlaceholders));
break;
}
case "SUCCEED": {
pushNode(createSucceedNode(tokens, stringLiteralPlaceholders));
break;
}
case "FAIL": {
pushNode(createFailNode(tokens, stringLiteralPlaceholders));
break;
}
case "FLIP": {
pushNode(createFlipNode(tokens, stringLiteralPlaceholders));
break;
}
case "REPEAT": {
pushNode(createRepeatNode(tokens, stringLiteralPlaceholders));
break;
}
case "RETRY": {
pushNode(createRetryNode(tokens, stringLiteralPlaceholders));
break;
}
case "SEQUENCE": {
pushNode(createSequenceNode(tokens, stringLiteralPlaceholders));
break;
}
case "SELECTOR": {
pushNode(createSelectorNode(tokens, stringLiteralPlaceholders));
break;
}
case "PARALLEL": {
pushNode(createParallelNode(tokens, stringLiteralPlaceholders));
break;
}
case "RACE": {
pushNode(createRaceNode(tokens, stringLiteralPlaceholders));
break;
}
case "ALL": {
pushNode(createAllNode(tokens, stringLiteralPlaceholders));
break;
}
case "LOTTO": {
pushNode(createLottoNode(tokens, stringLiteralPlaceholders));
break;
}
case "ACTION": {
pushNode(createActionNode(tokens, stringLiteralPlaceholders));
break;
}
case "CONDITION": {
pushNode(createConditionNode(tokens, stringLiteralPlaceholders));
break;
}
case "WAIT": {
pushNode(createWaitNode(tokens, stringLiteralPlaceholders));
break;
}
case "BRANCH": {
pushNode(createBranchNode(tokens, stringLiteralPlaceholders));
break;
}
case "}": {
const poppedNode = popNode();
if (poppedNode) {
validatePoppedNode(poppedNode);
}
break;
}
default: {
throw new Error(`unexpected token: ${token}`);
}
}
}
return rootNodes;
}
function createRootNode(tokens, stringLiteralPlaceholders) {
let node = {
type: "root"
};
const nodeArguments = parseArgumentTokens(tokens, stringLiteralPlaceholders);
if (nodeArguments.length) {
if (nodeArguments.length === 1 && nodeArguments[0].type === "identifier") {
node.id = nodeArguments[0].value;
} else {
throw new Error("expected single root name argument");
}
}
node = { ...node, ...parseAttributeTokens(tokens, stringLiteralPlaceholders) };
popAndCheck(tokens, "{");
return node;
}
function createSucceedNode(tokens, stringLiteralPlaceholders) {
const node = {
type: "succeed",
...parseAttributeTokens(tokens, stringLiteralPlaceholders)
};
popAndCheck(tokens, "{");
return node;
}
function createFailNode(tokens, stringLiteralPlaceholders) {
const node = {
type: "fail",
...parseAttributeTokens(tokens, stringLiteralPlaceholders)
};
popAndCheck(tokens, "{");
return node;
}
function createFlipNode(tokens, stringLiteralPlaceholders) {
const node = {
type: "flip",
...parseAttributeTokens(tokens, stringLiteralPlaceholders)
};
popAndCheck(tokens, "{");
return node;
}
function createRepeatNode(tokens, stringLiteralPlaceholders) {
let node = { type: "repeat" };
const nodeArguments = parseArgumentTokens(tokens, stringLiteralPlaceholders);
if (nodeArguments.length) {
nodeArguments.filter((arg) => arg.type !== "number" || !arg.isInteger).forEach(() => {
throw new Error(`repeat node iteration counts must be integer values`);
});
if (nodeArguments.length === 1) {
node.iterations = nodeArguments[0].value;
if (node.iterations < 0) {
throw new Error("a repeat node must have a positive number of iterations if defined");
}
} else if (nodeArguments.length === 2) {
node.iterations = [nodeArguments[0].value, nodeArguments[1].value];
if (node.iterations[0] < 0 || node.iterations[1] < 0) {
throw new Error("a repeat node must have a positive minimum and maximum iteration count if defined");
}
if (node.iterations[0] > node.iterations[1]) {
throw new Error(
"a repeat node must not have a minimum iteration count that exceeds the maximum iteration count"
);
}
} else {
throw new Error("invalid number of repeat node iteration count arguments defined");
}
}
node = { ...node, ...parseAttributeTokens(tokens, stringLiteralPlaceholders) };
popAndCheck(tokens, "{");
return node;
}
function createRetryNode(tokens, stringLiteralPlaceholders) {
let node = { type: "retry" };
const nodeArguments = parseArgumentTokens(tokens, stringLiteralPlaceholders);
if (nodeArguments.length) {
nodeArguments.filter((arg) => arg.type !== "number" || !arg.isInteger).forEach(() => {
throw new Error(`retry node attempt counts must be integer values`);
});
if (nodeArguments.length === 1) {
node.attempts = nodeArguments[0].value;
if (node.attempts < 0) {
throw new Error("a retry node must have a positive number of attempts if defined");
}
} else if (nodeArguments.length === 2) {
node.attempts = [nodeArguments[0].value, nodeArguments[1].value];
if (node.attempts[0] < 0 || node.attempts[1] < 0) {
throw new Error("a retry node must have a positive minimum and maximum attempt count if defined");
}
if (node.attempts[0] > node.attempts[1]) {
throw new Error(
"a retry node must not have a minimum attempt count that exceeds the maximum attempt count"
);
}
} else {
throw new Error("invalid number of retry node attempt count arguments defined");
}
}
node = { ...node, ...parseAttributeTokens(tokens, stringLiteralPlaceholders) };
popAndCheck(tokens, "{");
return node;
}
function createSequenceNode(tokens, stringLiteralPlaceholders) {
const node = {
type: "sequence",
...parseAttributeTokens(tokens, stringLiteralPlaceholders)
};
popAndCheck(tokens, "{");
return node;
}
function createSelectorNode(tokens, stringLiteralPlaceholders) {
const node = {
type: "selector",
...parseAttributeTokens(tokens, stringLiteralPlaceholders)
};
popAndCheck(tokens, "{");
return node;
}
function createParallelNode(tokens, stringLiteralPlaceholders) {
const node = {
type: "parallel",
...parseAttributeTokens(tokens, stringLiteralPlaceholders)
};
popAndCheck(tokens, "{");
return node;
}
function createRaceNode(tokens, stringLiteralPlaceholders) {
const node = {
type: "race",
...parseAttributeTokens(tokens, stringLiteralPlaceholders)
};
popAndCheck(tokens, "{");
return node;
}
function createAllNode(tokens, stringLiteralPlaceholders) {
const node = {
type: "all",
...parseAttributeTokens(tokens, stringLiteralPlaceholders)
};
popAndCheck(tokens, "{");
return node;
}
function createLottoNode(tokens, stringLiteralPlaceholders) {
const nodeArguments = parseArgumentTokens(tokens, stringLiteralPlaceholders);
nodeArguments.filter((arg) => arg.type !== "number" || !arg.isInteger || arg.value < 0).forEach(() => {
throw new Error(`lotto node weight arguments must be positive integer values`);
});
const node = {
type: "lotto",
...parseAttributeTokens(tokens, stringLiteralPlaceholders)
};
if (nodeArguments.length) {
node.weights = nodeArguments.map(({ value }) => value);
}
popAndCheck(tokens, "{");
return node;
}
function createActionNode(tokens, stringLiteralPlaceholders) {
const [actionNameIdentifier, ...agentFunctionArgs] = parseArgumentTokens(tokens, stringLiteralPlaceholders);
if (actionNameIdentifier?.type !== "identifier") {
throw new Error("expected action name identifier argument");
}
agentFunctionArgs.filter((arg) => arg.type === "identifier").forEach((arg) => {
throw new Error(
`invalid action node argument value '${arg.value}', must be string, number, boolean, agent property reference or null`
);
});
return {
type: "action",
call: actionNameIdentifier.value,
args: agentFunctionArgs.map(getArgumentJsonValue),
...parseAttributeTokens(tokens, stringLiteralPlaceholders)
};
}
function createConditionNode(tokens, stringLiteralPlaceholders) {
const [conditionNameIdentifier, ...agentFunctionArgs] = parseArgumentTokens(tokens, stringLiteralPlaceholders);
if (conditionNameIdentifier?.type !== "identifier") {
throw new Error("expected condition name identifier argument");
}
agentFunctionArgs.filter((arg) => arg.type === "identifier").forEach((arg) => {
throw new Error(
`invalid condition node argument value '${arg.value}', must be string, number, boolean, agent property reference or null`
);
});
return {
type: "condition",
call: conditionNameIdentifier.value,
args: agentFunctionArgs.map(getArgumentJsonValue),
...parseAttributeTokens(tokens, stringLiteralPlaceholders)
};
}
function createWaitNode(tokens, stringLiteralPlaceholders) {
const node = { type: "wait" };
const nodeArguments = parseArgumentTokens(tokens, stringLiteralPlaceholders);
if (nodeArguments.length) {
nodeArguments.filter((arg) => arg.type !== "number" || !arg.isInteger).forEach(() => {
throw new Error(`wait node durations must be integer values`);
});
if (nodeArguments.length === 1) {
node.duration = nodeArguments[0].value;
if (node.duration < 0) {
throw new Error("a wait node must have a positive duration");
}
} else if (nodeArguments.length === 2) {
node.duration = [nodeArguments[0].value, nodeArguments[1].value];
if (node.duration[0] < 0 || node.duration[1] < 0) {
throw new Error("a wait node must have a positive minimum and maximum duration");
}
if (node.duration[0] > node.duration[1]) {
throw new Error("a wait node must not have a minimum duration that exceeds the maximum duration");
}
} else if (nodeArguments.length > 2) {
throw new Error("invalid number of wait node duration arguments defined");
}
}
return { ...node, ...parseAttributeTokens(tokens, stringLiteralPlaceholders) };
}
function createBranchNode(tokens, stringLiteralPlaceholders) {
const nodeArguments = parseArgumentTokens(tokens, stringLiteralPlaceholders);
if (nodeArguments.length !== 1 || nodeArguments[0].type !== "identifier") {
throw new Error("expected single branch name argument");
}
return { type: "branch", ref: nodeArguments[0].value };
}
function validatePoppedNode(definition) {
if (isDecoratorNodeDefinition(definition) && isNullOrUndefined(definition.child)) {
throw new Error(`a ${definition.type} node must have a single child node defined`);
}
if (isCompositeNodeDefinition(definition) && !definition.children?.length) {
throw new Error(`a ${definition.type} node must have at least a single child node defined`);
}
if (definition.type === "lotto") {
if (typeof definition.weights !== "undefined") {
if (definition.weights.length !== definition.children.length) {
throw new Error(
"expected a number of weight arguments matching the number of child nodes for lotto node"
);
}
}
}
}
// src/BehaviourTreeDefinitionValidator.ts
function validateDefinition(definition) {
if (definition === null || typeof definition === "undefined") {
return createValidationFailureResult("definition is null or undefined");
}
if (typeof definition === "string") {
return validateMDSLDefinition(definition);
} else if (typeof definition === "object") {
return validateJSONDefinition(definition);
} else {
return createValidationFailureResult(`unexpected definition type of '${typeof definition}'`);
}
}
function validateMDSLDefinition(definition) {
let rootNodeDefinitions;
try {
rootNodeDefinitions = convertMDSLToJSON(definition);
} catch (exception) {
return createValidationFailureResult(exception.message);
}
const mainRootNodeDefinitions = rootNodeDefinitions.filter(({ id }) => typeof id === "undefined");
const subRootNodeDefinitions = rootNodeDefinitions.filter(({ id }) => typeof id === "string" && id.length > 0);
if (mainRootNodeDefinitions.length !== 1) {
return createValidationFailureResult(
"expected single unnamed root node at base of definition to act as main root"
);
}
const subRootNodeIdenitifers = [];
for (const { id } of subRootNodeDefinitions) {
if (subRootNodeIdenitifers.includes(id)) {
return createValidationFailureResult(`multiple root nodes found with duplicate name '${id}'`);
}
subRootNodeIdenitifers.push(id);
}
try {
validateBranchSubtreeLinks(rootNodeDefinitions, false);
} catch (exception) {
return createValidationFailureResult(exception.message);
}
return {
succeeded: true,
json: rootNodeDefinitions
};
}
function validateJSONDefinition(definition) {
const rootNodeDefinitions = Array.isArray(definition) ? definition : [definition];
try {
rootNodeDefinitions.forEach((rootNodeDefinition) => validateNode(rootNodeDefinition, 0));
} catch (error) {
if (error instanceof Error) {
return createValidationFailureResult(error.message);
}
return createValidationFailureResult(`unexpected error: ${error}`);
}
const mainRootNodeDefinitions = rootNodeDefinitions.filter(({ id }) => typeof id === "undefined");
const subRootNodeDefinitions = rootNodeDefinitions.filter(({ id }) => typeof id === "string" && id.length > 0);
if (mainRootNodeDefinitions.length !== 1) {
return createValidationFailureResult(
"expected single root node without 'id' property defined to act as main root"
);
}
const subRootNodeIdenitifers = [];
for (const { id } of subRootNodeDefinitions) {
if (subRootNodeIdenitifers.includes(id)) {
return createValidationFailureResult(
`multiple root nodes found with duplicate 'id' property value of '${id}'`
);
}
subRootNodeIdenitifers.push(id);
}
try {
validateBranchSubtreeLinks(rootNodeDefinitions, false);
} catch (exception) {
return createValidationFailureResult(exception.message);
}
return {
succeeded: true,
json: rootNodeDefinitions
};
}
function validateBranchSubtreeLinks(rootNodeDefinitions, includesGlobalSubtrees) {
const rootNodeMappings = rootNodeDefinitions.map(
(rootNodeDefinition) => ({
id: rootNodeDefinition.id,
refs: flattenDefinition(rootNodeDefinition).filter(isBranchNodeDefinition).map(({ ref }) => ref)
})
);
const followRefs = (mapping, path = []) => {
if (path.includes(mapping.id)) {
const badPath = [...path, mapping.id];
const badPathFormatted = badPath.filter((element) => !!element).join(" => ");
throw new Error(`circular dependency found in branch node references: ${badPathFormatted}`);
}
for (const ref of mapping.refs) {
const subMapping = rootNodeMappings.find(({ id }) => id === ref);
if (subMapping) {
followRefs(subMapping, [...path, mapping.id]);
} else if (includesGlobalSubtrees) {
throw new Error(
mapping.id ? `subtree '${mapping.id}' has branch node that references root node '${ref}' which has not been defined` : `primary tree has branch node that references root node '${ref}' which has not been defined`
);
}
}
};
followRefs(rootNodeMappings.find((mapping) => typeof mapping.id === "undefined"));
}
function validateNode(definition, depth) {
if (typeof definition !== "object" || typeof definition.type !== "string" || definition.type.length === 0) {
throw new Error(
`node definition is not an object or 'type' property is not a non-empty string at depth '${depth}'`
);
}
if (depth === 0 && definition.type !== "root") {
throw new Error(`expected root node at base of definition but got node of type '${definition.type}'`);
}
switch (definition.type) {
case "action":
validateActionNode(definition, depth);
break;
case "condition":
validateConditionNode(definition, depth);
break;
case "wait":
validateWaitNode(definition, depth);
break;
case "branch":
validateBranchNode(definition, depth);
break;
case "root":
validateRootNode(definition, depth);
break;
case "succeed":
validateSucceedNode(definition, depth);
break;
case "fail":
validateFailNode(definition, depth);
break;
case "flip":
validateFlipNode(definition, depth);
break;
case "repeat":
validateRepeatNode(definition, depth);
break;
case "retry":
validateRetryNode(definition, depth);
break;
case "sequence":
validateSequenceNode(definition, depth);
break;
case "selector":
validateSelectorNode(definition, depth);
break;
case "parallel":
validateParallelNode(definition, depth);
break;
case "race":
validateRaceNode(definition, depth);
break;
case "all":
validateAllNode(definition, depth);
break;
case "lotto":
validateLottoNode(definition, depth);
break;
default:
throw new Error(`unexpected node type of '${definition.type}' at depth '${depth}'`);
}
}
function validateNodeAttributes(definition, depth) {
["while", "until", "entry", "exit", "step"].forEach((attributeName) => {
const attributeDefinition = definition[attributeName];
if (typeof attributeDefinition === "undefined") {
return;
}
if (typeof attributeDefinition !== "object") {
throw new Error(
`expected attribute '${attributeName}' to be an object for '${definition.type}' node at depth '${depth}'`
);
}
if (typeof attributeDefinition.call !== "string" || attributeDefinition.call.length === 0) {
throw new Error(
`expected 'call' property for attribute '${attributeName}' to be a non-empty string for '${definition.type}' node at depth '${depth}'`
);
}
if (typeof attributeDefinition.args !== "undefined" && !Array.isArray(attributeDefinition.args)) {
throw new Error(
`expected 'args' property for attribute '${attributeName}' to be an array for '${definition.type}' node at depth '${depth}'`
);
}
});
}
function validateRootNode(definition, depth) {
if (definition.type !== "root") {
throw new Error("expected node type of 'root' for root node");
}
if (depth > 0) {
throw new Error("a root node cannot be the child of another node");
}
if (typeof definition.id !== "undefined" && (typeof definition.id !== "string" || definition.id.length === 0)) {
throw new Error("expected non-empty string for 'id' property if defined for root node");
}
if (typeof definition.child === "undefined") {
throw new Error("expected property 'child' to be defined for root node");
}
validateNodeAttributes(definition, depth);
validateNode(definition.child, depth + 1);
}
function validateSucceedNode(definition, depth) {
if (definition.type !== "succeed") {
throw new Error(`expected node type of 'succeed' for succeed node at depth '${depth}'`);
}
if (typeof definition.child === "undefined") {
throw new Error(`expected property 'child' to be defined for succeed node at depth '${depth}'`);
}
validateNodeAttributes(definition, depth);
validateNode(definition.child, depth + 1);
}
function validateFailNode(definition, depth) {
if (definition.type !== "fail") {
throw new Error(`expected node type of 'fail' for fail node at depth '${depth}'`);
}
if (typeof definition.child === "undefined") {
throw new Error(`expected property 'child' to be defined for fail node at depth '${depth}'`);
}
validateNodeAttributes(definition, depth);
validateNode(definition.child, depth + 1);
}
function validateFlipNode(definition, depth) {
if (definition.type !== "flip") {
throw new Error(`expected node type of 'flip' for flip node at depth '${depth}'`);
}
if (typeof definition.child === "undefined") {
throw new Error(`expected property 'child' to be defined for flip node at depth '${depth}'`);
}
validateNodeAttributes(definition, depth);
validateNode(definition.child, depth + 1);
}
function validateRepeatNode(definition, depth) {
if (definition.type !== "repeat") {
throw new Error(`expected node type of 'repeat' for repeat node at depth '${depth}'`);
}
if (typeof definition.child === "undefined") {
throw new Error(`expected property 'child' to be defined for repeat node at depth '${depth}'`);
}
if (typeof definition.iterations !== "undefined") {
if (Array.isArray(definition.iterations)) {
const containsNonInteger = !!definition.iterations.filter((value) => !isInteger(value)).length;
if (definition.iterations.length !== 2 || containsNonInteger) {
throw new Error(
`expected array containing two integer values for 'iterations' property if defined for repeat node at depth '${depth}'`
);
}
if (definition.iterations[0] < 0 || definition.iterations[1] < 0) {
throw new Error(
`expected positive minimum and maximum iterations count for 'iterations' property if defined for repeat node at depth '${depth}'`
);
}
if (definition.iterations[0] > definition.iterations[1]) {
throw new Error(
`expected minimum iterations count that does not exceed the maximum iterations count for 'iterations' property if defined for repeat node at depth '${depth}'`
);
}
} else if (isInteger(definition.iterations)) {
if (definition.iterations < 0) {
throw new Error(
`expected positive iterations count for 'iterations' property if defined for repeat node at depth '${depth}'`
);
}
} else {
throw new Error(
`expected integer value or array containing two integer values for 'iterations' property if defined for repeat node at depth '${depth}'`
);
}
}
validateNodeAttributes(definition, depth);
validateNode(definition.child, depth + 1);
}
function validateRetryNode(definition, depth) {
if (definition.type !== "retry") {
throw new Error(`expected node type of 'retry' for retry node at depth '${depth}'`);
}
if (typeof definition.child === "undefined") {
throw new Error(`expected property 'child' to be defined for retry node at depth '${depth}'`);
}
if (typeof definition.attempts !== "undefined") {
if (Array.isArray(definition.attempts)) {
const containsNonInteger = !!definition.attempts.filter((value) => !isInteger(value)).length;
if (definition.attempts.length !== 2 || containsNonInteger) {
throw new Error(
`expected array containing two integer values for 'attempts' property if defined for retry node at depth '${depth}'`
);
}
if (definition.attempts[0] < 0 || definition.attempts[1] < 0) {
throw new Error(
`expected positive minimum and maximum attempts count for 'attempts' property if defined for retry node at depth '${depth}'`
);
}
if (definition.attempts[0] > definition.attempts[1]) {
throw new Error(
`expected minimum attempts count that does not exceed the maximum attempts count for 'attempts' property if defined for retry node at depth '${depth}'`
);
}
} else if (isInteger(definition.attempts)) {
if (definition.attempts < 0) {
throw new Error(
`expected positive attempts count for 'attempts' property if defined for retry node at depth '${depth}'`
);
}
} else {
throw new Error(
`expected integer value or array containing two integer values for 'attempts' property if defined for retry node at depth '${depth}'`
);
}
}
validateNodeAttributes(definition, depth);
validateNode(definition.child, depth + 1);
}
function validateBranchNode(definition, depth) {
if (definition.type !== "branch") {
throw new Error(`expected node type of 'branch' for branch node at depth '${depth}'`);
}
if (typeof definition.ref !== "string" || definition.ref.length === 0) {
throw new Error(`expected non-empty string for 'ref' property for branch node at depth '${depth}'`);
}
["while", "until"].forEach((attributeName) => {
if (typeof definition[attributeName] !== "undefined") {
throw new Error(
`guards should not be defined for branch nodes but guard '${attributeName}' was defined for branch node at depth '${depth}'`
);
}
});
["entry", "exit", "step"].forEach((attributeName) => {
if (typeof definition[attributeName] !== "undefined") {
throw new Error(
`callbacks should not be defined for branch nodes but callback '${attributeName}' was defined for branch node at depth '${depth}'`
);
}
});
}
function validateActionNode(definition, depth) {
if (definition.type !== "action") {
throw new Error(`expected node type of 'action' for action node at depth '${depth}'`);
}
if (typeof definition.call !== "string" || definition.call.length === 0) {
throw new Error(`expected non-empty string for 'call' property of action node at depth '${depth}'`);
}
if (typeof definition.args !== "undefined" && !Array.isArray(definition.args)) {
throw new Error(`expected array for 'args' property if defined for action node at depth '${depth}'`);
}
validateNodeAttributes(definition, depth);
}
function validateConditionNode(definition, depth) {
if (definition.type !== "condition") {
throw new Error(`expected node type of 'condition' for condition node at depth '${depth}'`);
}
if (typeof definition.call !== "string" || definition.call.length === 0) {
throw new Error(`expected non-empty string for 'call' property of condition node at depth '${depth}'`);
}
if (typeof definition.args !== "undefined" && !Array.isArray(definition.args)) {
throw new Error(`expected array for 'args' property if defined for condition node at depth '${depth}'`);
}
validateNodeAttributes(definition, depth);
}
function validateWaitNode(definition, depth) {
if (definition.type !== "wait") {
throw new Error(`expected node type of 'wait' for wait node at depth '${depth}'`);
}
if (typeof definition.duration !== "undefined") {
if (Array.isArray(definition.duration)) {
const containsNonInteger = !!definition.duration.filter((value) => !isInteger(value)).length;
if (definition.duration.length !== 2 || containsNonInteger) {
throw new Error(
`expected array containing two integer values for 'duration' property if defined for wait node at depth '${depth}'`
);
}
if (definition.duration[0] < 0 || definition.duration[1] < 0) {
throw new Error(
`expected positive minimum and maximum duration for 'duration' property if defined for wait node at depth '${depth}'`
);
}
if (definition.duration[0] > definition.duration[1]) {
throw new Error(
`expected minimum duration value that does not exceed the maximum duration value for 'duration' property if defined for wait node at depth '${depth}'`
);
}
} else if (isInteger(definition.duration)) {
if (definition.duration < 0) {
throw new Error(
`expected positive duration value for 'duration' property if defined for wait node at depth '${depth}'`
);
}
} else {
throw new Error(
`expected integer value or array containing two integer values for 'duration' property if defined for wait node at depth '${depth}'`
);
}
}
validateNodeAttributes(definition, depth);
}
function validateSequenceNode(definition, depth) {
if (definition.type !== "sequence") {
throw new Error(`expected node type of 'sequence' for sequence node at depth '${depth}'`);
}
if (!Array.isArray(definition.children) || definition.children.length === 0)