UNPKG

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
"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)