Skip to content

Commit

Permalink
Tidying up some of our utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
Nikolas Howard committed Oct 27, 2023
1 parent e3f0e40 commit b1da1eb
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 70 deletions.
69 changes: 69 additions & 0 deletions src/BehaviourTreeDefinitionUtilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { NodeDefinition, RootNodeDefinition, DecoratorNodeDefinition, CompositeNodeDefinition, AnyNode, BranchNodeDefinition } from "./BehaviourTreeDefinition";

/**
* A type guard function that returns true if the specified node satisfies the RootNodeDefinition type.
* @param node The node.
* @returns A value of true if the specified node satisfies the RootNodeDefinition type.
*/
export function isRootNode(node: NodeDefinition): node is RootNodeDefinition {
return node.type === "root";
}

/**
* A type guard function that returns true if the specified node satisfies the BranchNodeDefinition type.
* @param node The node.
* @returns A value of true if the specified node satisfies the BranchNodeDefinition type.
*/
export function isBranchNode(node: NodeDefinition): node is BranchNodeDefinition {
return node.type === "branch";
}

/**
* A type guard function that returns true if the specified node satisfies the NodeDefinition type.
* @param node The node.
* @returns A value of true if the specified node satisfies the NodeDefinition type.
*/
export function isLeafNode(node: NodeDefinition): node is NodeDefinition {
return ["branch", "action", "condition", "wait"].includes(node.type);
}

/**
* A type guard function that returns true if the specified node satisfies the DecoratorNodeDefinition type.
* @param node The node.
* @returns A value of true if the specified node satisfies the DecoratorNodeDefinition type.
*/
export function isDecoratorNode(node: NodeDefinition): node is DecoratorNodeDefinition {
return ["root", "repeat", "retry", "flip", "succeed", "fail"].includes(node.type);
}

/**
* A type guard function that returns true if the specified node satisfies the CompositeNodeDefinition type.
* @param node The node.
* @returns A value of true if the specified node satisfies the CompositeNodeDefinition type.
*/
export function isCompositeNode(node: NodeDefinition): node is CompositeNodeDefinition {
return ["sequence", "selector", "lotto", "parallel"].includes(node.type);
}

/**
* Flatten a node definition into an array of all of its nested node definitions.
* @param nodeDefinition The node definition to flatten.
* @returns An array of all of nested node definitions.
*/
export function flattenDefinition(nodeDefinition: AnyNode): AnyNode[] {
const nodes: AnyNode[] = [];

const processNode = (currentNodeDefinition: AnyNode) => {
nodes.push(currentNodeDefinition);

if (isCompositeNode(currentNodeDefinition)) {
currentNodeDefinition.children.forEach(processNode);
} else if (isDecoratorNode(currentNodeDefinition)) {
processNode(currentNodeDefinition.child);
}
};

processNode(nodeDefinition);

return nodes;
}
63 changes: 40 additions & 23 deletions src/BehaviourTreeDefinitionValidator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { RootNodeDefinition } from "./BehaviourTreeDefinition";
import { flattenDefinition, isBranchNode } from "./BehaviourTreeDefinitionUtilities";
import { parseMDSLToJSON } from "./mdsl/MDSLDefinitionParser";

/**
Expand All @@ -14,10 +16,18 @@ export type DefinitionValidationResult = {
errorMessage?: string;
};

/**
* Validates the specified behaviour tree definition in the form of JSON or MDSL.
* @param definition The behaviour tree definition in the form of JSON or MDSL.
* @returns An object representing the result of validating the given tree definition.
*/
export function validateDefinition(definition: any): DefinitionValidationResult {
// A helper function to create a failure validation result with the given error message.
const createFailureResult = (errorMessage: string) => ({ succeeded: false, errorMessage });

// The definition must be defined.
if (definition === null || typeof definition === "undefined") {
throw new Error(`definition is null or undefined`);
return createFailureResult("definition is null or undefined");
}

let rootNodeDefinitions: any[];
Expand All @@ -32,10 +42,7 @@ export function validateDefinition(definition: any): DefinitionValidationResult
rootNodeDefinitions = parseMDSLToJSON(definition);
} catch (error) {
// We failed to parse the JSON from the mdsl, this is likely to be the result of it not being a valid mdsl string.
return {
succeeded: false,
errorMessage: `invalid mdsl: ${definition}`
};
return createFailureResult(`invalid mdsl: ${definition}`);
}
} else if (typeof definition === "object") {
// The definition will either be an array (of root node definitions) or an object (the single primary root node definition).
Expand All @@ -49,10 +56,7 @@ export function validateDefinition(definition: any): DefinitionValidationResult

// If we have any invalid node definitions then validation has failed.
if (invalidDefinitionElements.length) {
return {
succeeded: false,
errorMessage: "invalid elements in definition array, each must be an root node definition object"
};
return createFailureResult("invalid elements in definition array, each must be an root node definition object");
}

// Our definition is already an array of root node definition objects.
Expand All @@ -62,48 +66,56 @@ export function validateDefinition(definition: any): DefinitionValidationResult
rootNodeDefinitions = [definition];
}
} else {
return {
succeeded: false,
errorMessage: `unexpected definition type of '${typeof definition}'`
};
return createFailureResult(`unexpected definition type of '${typeof definition}'`);
}

// TODO Iterate over our array of root nodes and call validateNode for each, passing an initial depth of 0, wrapped in a try catch to handle validation failures.

// Unpack all of the root node definitions into arrays of main (no 'id' defined) and sub ('id' defined) root node definitions.
// Unpack all of the root node definitions into arrays of main ('id' defined) and sub ('id' not defined) root node definitions.
const mainRootNodeDefinitions = rootNodeDefinitions.filter(({ id }) => typeof id === "undefined");
const subRootNodeDefinitions = rootNodeDefinitions.filter(({ id }) => typeof id === "string" && id.length > 0);

// We should ALWAYS have exactly one root node definition without an 'id' property defined, which is out main root node definition.
if (mainRootNodeDefinitions.length !== 1) {
return {
succeeded: false,
errorMessage: "expected single root node without 'id' property defined to act as main root"
};
return createFailureResult("expected single root node without 'id' property defined to act as main root");
}

// We should never have duplicate 'id' properties across our sub root node definitions.
const subRootNodeIdenitifers: string[] = [];
for (const { id } of subRootNodeDefinitions) {
// Have we already come across this 'id' property value?
if (subRootNodeIdenitifers.includes(id)) {
return {
succeeded: false,
errorMessage: `multiple root nodes found with duplicate 'id' property value of '${id}'`
};
return createFailureResult(`multiple root nodes found with duplicate 'id' property value of '${id}'`);
}

subRootNodeIdenitifers.push(id);
}

// TODO Check for any root node circular depedencies.
// TODO Check for any root node circular depedencies. This will not include any globally registered subtrees.


// TODO How do we handle globally defined root nodes?

// Our definition was valid!
return { succeeded: true };
}

function _checkForRootNodeCircularDependencies(rootNodeDefinitions: RootNodeDefinition[]): void {
// Create a mapping of root node id's to other root nodes that they reference via branch nodes.
const rootNodeMappings: { id: string | undefined, refs: string[] }[] = rootNodeDefinitions
.map((rootNodeDefinition) => ({
id: rootNodeDefinition.id,
refs: flattenDefinition(rootNodeDefinition).filter(isBranchNode).map(({ ref }) => ref)
}));

// TODO Create a recursive function to walk through the mappings, keeing track of which root nodes we have visited.
}

/**
* Validate an object that we expect to be a node definition.
* @param definition An object that we expect to be a node definition.
* @param depth The depth of the node in the definition tree.
*/
function validateNode(definition: any, depth: number): void {
// Every node must be valid object and have a non-empty 'type' string property.
if (typeof definition !== "object" || typeof definition.type !== "string" || definition.type.length === 0) {
Expand All @@ -123,6 +135,11 @@ function validateNode(definition: any, depth: number): void {
}
}

/**
* Validate any attributes for a given node definition.
* @param definition The node definition.
* @param depth The depth of the node in the behaviour tree definition.
*/
function validateNodeAttributes(definition: any, depth: number): void {
// Validate each of the attribute types for this node.
["while", "until", "entry", "exit", "step"].forEach((attributeName) => {
Expand Down
5 changes: 1 addition & 4 deletions src/mdsl/MDSLDefinitionParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,11 @@ import {
SucceedNodeDefinition,
WaitNodeDefinition
} from "../BehaviourTreeDefinition";
import { isCompositeNode, isDecoratorNode, isLeafNode, isRootNode } from "../BehaviourTreeDefinitionUtilities";
import { parseArgumentTokens } from "./MDSLNodeArgumentParser";
import { parseAttributeTokens } from "./MDSLNodeAttributeParser";
import {
StringLiteralPlaceholders,
isCompositeNode,
isDecoratorNode,
isLeafNode,
isRootNode,
parseTokensFromDefinition,
popAndCheck,
substituteStringLiterals
Expand Down
43 changes: 0 additions & 43 deletions src/mdsl/MDSLUtilities.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,8 @@
import {
CompositeNodeDefinition,
DecoratorNodeDefinition,
NodeDefinition,
RootNodeDefinition
} from "../BehaviourTreeDefinition";

/**
* A type defining an object that holds a reference to substitued string literals parsed from the definition.
*/
export type StringLiteralPlaceholders = { [key: string]: string };

/**
* A type guard function that returns true if the specified node satisfies the RootNodeDefinition type.
* @param node The node.
* @returns A value of true if the specified node satisfies the RootNodeDefinition type.
*/
export function isRootNode(node: NodeDefinition): node is RootNodeDefinition {
return node.type === "root";
}

/**
* A type guard function that returns true if the specified node satisfies the NodeDefinition type.
* @param node The node.
* @returns A value of true if the specified node satisfies the NodeDefinition type.
*/
export function isLeafNode(node: NodeDefinition): node is NodeDefinition {
return ["branch", "action", "condition", "wait"].includes(node.type);
}

/**
* A type guard function that returns true if the specified node satisfies the DecoratorNodeDefinition type.
* @param node The node.
* @returns A value of true if the specified node satisfies the DecoratorNodeDefinition type.
*/
export function isDecoratorNode(node: NodeDefinition): node is DecoratorNodeDefinition {
return ["root", "repeat", "retry", "flip", "succeed", "fail"].includes(node.type);
}

/**
* A type guard function that returns true if the specified node satisfies the CompositeNodeDefinition type.
* @param node The node.
* @returns A value of true if the specified node satisfies the CompositeNodeDefinition type.
*/
export function isCompositeNode(node: NodeDefinition): node is CompositeNodeDefinition {
return ["sequence", "selector", "lotto", "parallel"].includes(node.type);
}

/**
* Pop the next raw token from the specified array of tokens and throw an error if it wasn't the expected one.
* @param tokens The array of tokens.
Expand Down

0 comments on commit b1da1eb

Please sign in to comment.