Skip to content

Commit

Permalink
stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
Nikolas Howard committed Feb 5, 2024
1 parent fca74d8 commit 6e38ea4
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 127 deletions.
175 changes: 73 additions & 102 deletions src/BehaviourTree.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import GuardPath, { GuardPathPart } from "./attributes/guards/GuardPath";
import buildRootASTNodes, { AnyArgument, RootAstNode } from "./RootAstNodesBuilder";
import State, { AnyState } from "./State";
import Lookup from "./Lookup";
import Node from "./nodes/Node";
import Root from "./nodes/decorator/Root";
import Composite from "./nodes/composite/Composite";
import Decorator from "./nodes/decorator/Decorator";
import { AnyArgument } from "./RootAstNodesBuilder";
import { Agent, GlobalFunction } from "./Agent";
import { CallbackAttributeDetails } from "./attributes/callbacks/Callback";
import { GuardAttributeDetails } from "./attributes/guards/Guard";
import { BehaviourTreeOptions } from "./BehaviourTreeOptions";
import { convertMDSLToJSON } from "./mdsl/MDSLDefinitionParser";
import { RootNodeDefinition } from "./BehaviourTreeDefinition";
import { validateJSONDefinition } from "./BehaviourTreeDefinitionValidator";
import buildRootNode from "./BehaviourTreeBuilder";

// Purely for outside inspection of the tree.
export type FlattenedTreeNode = {
Expand Down Expand Up @@ -39,19 +41,24 @@ export class BehaviourTree {
* @param agent The agent instance that this behaviour tree is modelling behaviour for.
* @param options The behaviour tree options object.
*/
constructor(definition: string, private agent: Agent, private options: BehaviourTreeOptions = {}) {
// The tree definition must be defined and a valid string.
if (typeof definition !== "string") {
throw new Error("the tree definition must be a string");
constructor(definition: string | RootNodeDefinition | RootNodeDefinition[], private agent: Agent, private options: BehaviourTreeOptions = {}) {
// The tree definition must be defined.
if (!definition) {
throw new Error("the tree definition must be a string ro");
}

// The agent must be defined and not null.
if (typeof agent !== "object" || agent === null) {
throw new Error("the agent must be defined and not null");
}

// Parse the behaviour tree definition, create the populated tree of behaviour tree nodes, and get the root.
this.rootNode = BehaviourTree._createRootNode(definition);
try {
// Parse the behaviour tree definition, create the populated tree of behaviour tree nodes, and get the root.
this.rootNode = this._createRootNode(definition);
} catch (exception) {
// There was an issue in trying build and populate the behaviour tree.
throw new Error(`error building tree: ${(exception as Error).message}`);
}
}

/**
Expand Down Expand Up @@ -153,30 +160,61 @@ export class BehaviourTree {
* @param name The name of the function or subtree to register.
* @param value The function or subtree definition to register.
*/
static register(name: string, value: GlobalFunction | string) {
static register(name: string, value: GlobalFunction | string | RootNodeDefinition) {
// Are we going to register a action/condition/guard/callback function?
if (typeof value === "function") {
// We are going to register a action/condition/guard/callback function.
Lookup.setFunc(name, value);
} else if (typeof value === "string") {
// We are going to register a subtree.
let rootASTNodes: RootAstNode[];
return;
}

// We are not registering an action/condition/guard/callback function, so we must be registering a subtree.
if (typeof value === "string") {
let rootNodeDefinitions: RootNodeDefinition[];

// We will assume that any string passed in will be a mdsl definition.
try {
// Try to create the behaviour tree AST based on the definition provided, this could fail if the definition is invalid.
rootASTNodes = buildRootASTNodes(value);
rootNodeDefinitions = convertMDSLToJSON(value);
} catch (exception) {
// There was an issue in trying to parse and build the tree definition.
throw new Error(`error registering definition: ${(exception as Error).message}`);
throw new Error(`error registering definition, invalid MDSL: ${(exception as Error).message}`);
}

// This function should only ever be called with a definition containing a single unnamed root node.
if (rootASTNodes.length != 1 || rootASTNodes[0].name !== null) {
if (rootNodeDefinitions.length != 1 || rootNodeDefinitions[0].id !== null) {
throw new Error("error registering definition: expected a single unnamed root node");
}

Lookup.setSubtree(name, rootASTNodes[0]);
// We should validate the subtree as we don't want invalid subtrees available via the lookup.
try {
const { succeeded, errorMessage } = validateJSONDefinition(rootNodeDefinitions[0]);

// Did our validation fail without error?
if (!succeeded) {
throw new Error(errorMessage);
}
} catch (exception) {
throw new Error(`error registering definition: ${(exception as Error).message}`);
}

// Everything seems hunky-dory, register the subtree.
Lookup.setSubtree(name, rootNodeDefinitions[0]);
} else if (typeof value === "object" && !Array.isArray(value)) {
// We will assume that any object passed in is a root node definition.
// We should validate the subtree as we don't want invalid subtrees available via the lookup.
try {
const { succeeded, errorMessage } = validateJSONDefinition(value);

// Did our validation fail without error?
if (!succeeded) {
throw new Error(errorMessage);
}
} catch (exception) {
throw new Error(`error registering definition: ${(exception as Error).message}`);
}

// Everything seems hunky-dory, register the subtree.
Lookup.setSubtree(name, value);
} else {
throw new Error("unexpected value, expected string definition or function");
throw new Error("unexpected value, expected string mdsl definition, root node json definition or function");
}
}

Expand All @@ -196,93 +234,26 @@ export class BehaviourTree {
}

/**
* Parses a behaviour tree definition and creates a tree of behaviour tree nodes.
* @param {string} definition The behaviour tree definition.
* Parses a behaviour tree definition and creates a tree of behaviour tree nodes populated at a root.
* @param {string | RootNodeDefinition | RootNodeDefinition[]} definition The behaviour tree definition.
* @returns The root behaviour tree node.
*/
private static _createRootNode(definition: string): Root {
// TODO Remove!
try {
// parseToJSON(definition);
} catch (exception) {
console.log(exception);
}

try {
// Try to create the behaviour tree AST based on the definition provided, this could fail if the definition is invalid.
const rootASTNodes = buildRootASTNodes(definition);

// Create a symbol to use as the main root key in our root node mapping.
const mainRootNodeKey = Symbol("__root__");
private _createRootNode(definition: string | RootNodeDefinition | RootNodeDefinition[]): Root {
let resolvedDefinition: RootNodeDefinition | RootNodeDefinition[]

// Create a mapping of root node names to root AST tokens. The main root node will have a key of Symbol("__root__").
const rootNodeMap: { [key: string | symbol]: RootAstNode } = {};
for (const rootASTNode of rootASTNodes) {
rootNodeMap[rootASTNode.name === null ? mainRootNodeKey : rootASTNode.name!] = rootASTNode;
// If the definition is a string then we will assume that it is an mdsl string which needs to be converted to a JSON definition.
if (typeof definition === "string") {
try {
resolvedDefinition = convertMDSLToJSON(definition);
} catch (exception) {
throw new Error(`invalid mdsl definition: ${(exception as Error).message}`);
}

// Convert the AST to our actual tree and get the root node.
const rootNode: Root = rootNodeMap[mainRootNodeKey].createNodeInstance(
// Create a provider for named root nodes that are part of our definition or have been registered. Prioritising the former.
(name: string): RootAstNode => (rootNodeMap[name] ? rootNodeMap[name] : Lookup.getSubtree(name)),
[]
);

// Set a guard path on every leaf of the tree to evaluate as part of its update.
BehaviourTree._applyLeafNodeGuardPaths(rootNode);

// Return the root node.
return rootNode;
} catch (exception) {
// There was an issue in trying to parse and build the tree definition.
throw new Error(`error parsing tree: ${(exception as Error).message}`);
} else {
// The definition is not a string, so we should assume that it is already a JSON definition.
resolvedDefinition = definition;
}
}

/**
* Applies a guard path to every leaf of the tree to evaluate as part of each update.
* @param rootNode The main root tree node.
*/
private static _applyLeafNodeGuardPaths(rootNode: Root) {
const nodePaths: Node[][] = [];

const findLeafNodes = (path: Node[], node: Node) => {
// Add the current node to the path.
path = path.concat(node);

// Check whether the current node is a leaf node.
if (node.isLeafNode()) {
nodePaths.push(path);
} else {
(node as Composite | Decorator).getChildren().forEach((child) => findLeafNodes(path, child));
}
};

// Find all leaf node paths, starting from the root.
findLeafNodes([], rootNode);

nodePaths.forEach((path) => {
// Each node in the current path will have to be assigned a guard path, working from the root outwards.
for (let depth = 0; depth < path.length; depth++) {
// Get the node in the path at the current depth.
const currentNode = path[depth];

// The node may already have been assigned a guard path, if so just skip it.
if (currentNode.hasGuardPath()) {
continue;
}

// Create the guard path for the current node.
const guardPath = new GuardPath(
path
.slice(0, depth + 1)
.map<GuardPathPart>((node) => ({ node, guards: node.getGuardAttributes() }))
.filter((details) => details.guards.length > 0)
);

// Assign the guard path to the current node.
currentNode.setGuardPath(guardPath);
}
});
// Build and populate the root node.
return buildRootNode(Array.isArray(resolvedDefinition) ? resolvedDefinition : [resolvedDefinition]);
}
}
121 changes: 121 additions & 0 deletions src/BehaviourTreeBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@

import { AnyNodeDefinition, RootNodeDefinition } from "./BehaviourTreeDefinition";
import { validateJSONDefinition } from "./BehaviourTreeDefinitionValidator";
import Parallel from "./nodes/composite/Parallel";
import Selector from "./nodes/composite/Selector";
import Sequence from "./nodes/composite/Sequence";
import Lotto from "./nodes/composite/Lotto";
import Fail from "./nodes/decorator/Fail";
import Flip from "./nodes/decorator/Flip";
import Repeat from "./nodes/decorator/Repeat";
import Retry from "./nodes/decorator/Retry";
import Root from "./nodes/decorator/Root";
import Succeed from "./nodes/decorator/Succeed";
import Action from "./nodes/leaf/Action";
import Condition from "./nodes/leaf/Condition";
import Wait from "./nodes/leaf/Wait";
import Lookup from "./Lookup";
import Attribute from "./attributes/Attribute";
import While from "./attributes/guards/While";

/**
* A type representing any node instance in a behaviour tree.
*/
type AnyNode = Root | Action | Condition | Wait | Sequence | Selector | Lotto | Parallel | Repeat | Retry | Flip | Succeed | Fail;

/**
* A type defining a mapping of root node identifiers to root node definitions.
*/
type RootNodeDefinitionMap = { [key: string | symbol]: RootNodeDefinition };

/**
* A symbol to use as the main root key in any root node mappings.
*/
const MAIN_ROOT_NODE_KEY = Symbol("__root__");

/**
* Build and populate the root nodes based on the provided definition, assuming that the definition has been validated.
* @param definition The root node definitions.
* @returns The built and populated root node definitions.
*/
export default function buildRootNode(definition: RootNodeDefinition[]): Root {
// Create a mapping of root node identifers to root node definitions, including globally registered subtree root node definitions.
const rootNodeDefinitionMap = createRootNodeDefinitionMap(definition);

// Now that we have all of our root node definitons (those part of the tree definition and those globally registered) we should validate
// the definition. This will also double-check that we dont have any circular depdendencies in our branch -> root node references.
try {
const { succeeded, errorMessage } = validateJSONDefinition(Object.values(rootNodeDefinitionMap));

// Did our validation fail without error?
if (!succeeded) {
throw new Error(errorMessage);
}
} catch (exception) {
throw new Error(`root node validation failed: '${(exception as Error).message}'`);
}

// Create our populated tree of node instances, starting with our main root node.
const rootNode = nodeFactory(rootNodeDefinitionMap[MAIN_ROOT_NODE_KEY]) as Root;

// TODO Set a guard path on every leaf of the tree to evaluate as part of its update. (see BehaviourTree._applyLeafNodeGuardPaths)

// We only need to return the main root node.
return rootNode;
}

/**
* A factory function which creates a node instance based on the specified definition.
* @param definition The node definition.
* @returns A node instance based on the specified definition.
*/
function nodeFactory(definition: AnyNodeDefinition): AnyNode {
// Get the attributes for the node.
const attributes = nodeAttributesFactory(definition);

// Create the node instance based on the definition type.
switch (definition.type) {
case "root":
return new Root(attributes, nodeFactory(definition.child));

// ...

default:
throw new Error(`unexpected node type of '${definition.type}'`);
}
}

function nodeAttributesFactory(definition: AnyNodeDefinition): Attribute[] {
const attributes: Attribute[] = [];

if (definition.while) {
// TODO does this take args as any? We have AnyArgument type but is that just for mdsl parsing???
// TODO Double check that validateJSONDefinition handles the args, surely they can be 'any' at this point?
attributes.push(new While(definition.while.call, definition.while.args));

Check failure on line 94 in src/BehaviourTreeBuilder.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Argument of type 'any[] | undefined' is not assignable to parameter of type 'AnyArgument[]'.

Check failure on line 94 in src/BehaviourTreeBuilder.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Argument of type 'any[] | undefined' is not assignable to parameter of type 'AnyArgument[]'.
}

return attributes;
}

/**
* Creates a mapping of root node identifers to root node definitions, mixing in globally registered subtree root node definitions.
* @param definition The root node definitions.
* @returns A mapping of root node identifers to root node definitions, including globally registered subtree root node definitions.
*/
function createRootNodeDefinitionMap(definition: RootNodeDefinition[]): RootNodeDefinitionMap {
// Create a mapping of root node identifers to root node definitions.
const rootNodeMap: RootNodeDefinitionMap = {};

// Add in any registered subtree root node definitions.
for (const [name, rootNodeDefinition] of Object.entries(Lookup.getSubtrees())) {
rootNodeMap[name] = rootNodeDefinition;
}

// Populate the map with the root node definitions that were included with the tree definition.
// We do this after adding any registered subtrees as we want these to take presedence.
for (const rootNodeDefinition of definition) {
rootNodeMap[rootNodeDefinition.id ?? MAIN_ROOT_NODE_KEY] = rootNodeDefinition;
}

return rootNodeMap;
}
10 changes: 5 additions & 5 deletions src/BehaviourTreeDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export interface CompositeNodeDefinition extends NodeDefinition {
/**
* The child nodes of this composite node.
*/
children: AnyChildNode[];
children: AnyChildNodeDefinition[];
}

/**
Expand All @@ -59,7 +59,7 @@ export interface DecoratorNodeDefinition extends NodeDefinition {
/**
* The child node of this decorator node.
*/
child: AnyChildNode;
child: AnyChildNodeDefinition;
}

/**
Expand Down Expand Up @@ -234,9 +234,9 @@ export interface FailNodeDefinition extends DecoratorNodeDefinition {
}

/**
* A type defining any node type.
* A type defining any node definition.
*/
export type AnyNode =
export type AnyNodeDefinition =
| BranchNodeDefinition
| ActionNodeDefinition
| ConditionNodeDefinition
Expand All @@ -255,4 +255,4 @@ export type AnyNode =
/**
* A type defining any node type that can be a child of composite parent node.
*/
export type AnyChildNode = Exclude<AnyNode, RootNodeDefinition>;
export type AnyChildNodeDefinition = Exclude<AnyNodeDefinition, RootNodeDefinition>;
Loading

0 comments on commit 6e38ea4

Please sign in to comment.