├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── BehaviorTreeBuilder.ts ├── BehaviorTreeStatus.ts ├── Error │ ├── BehaviorTreeError.ts │ └── Errors.ts ├── Node │ ├── ActionNode.ts │ ├── BehaviorTreeNodeInterface.ts │ ├── InverterNode.ts │ ├── ParallelNode.ts │ ├── ParentBehaviorTreeNodeInterface.ts │ ├── SelectorNode.ts │ └── SequenceNode.ts ├── NodeEnumerator.ts ├── StateData.ts └── index.ts ├── test ├── BehaviorTreeBuilderTest.ts ├── Node │ ├── ActionNodeTest.ts │ ├── InverterNodeTest.ts │ ├── ParallelNodeTest.ts │ ├── SelectorNodeTest.ts │ └── SequenceNodeTest.ts └── tsconfig.json ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | dist 3 | test/dist 4 | .nyc_output 5 | node_modules 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | .travis.yml 3 | tsconfig.json 4 | tslint.json 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "6.11" 5 | - "7.10" 6 | - "8.4" 7 | - "9.0" 8 | sudo: false 9 | cache: 10 | directories: 11 | - node_modules 12 | script: 13 | - npm run test 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Aaron Scherer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fluent Behavior Tree 2 | [![Build Status](https://travis-ci.org/aequasi/fluent-behavior-tree.svg?branch=master)](https://travis-ci.org/aequasi/fluent-behavior-tree) [![npm version](https://badge.fury.io/js/fluent-behavior-tree.svg)](https://badge.fury.io/js/fluent-behavior-tree) 3 | 4 | This is a Typescript/Javascript implementation of https://github.com/codecapers/Fluent-Behaviour-Tree 5 | 6 | JS/TS behaviour tree library with a fluent API. 7 | 8 | For a background and walk-through please see [the accompanying article](http://www.what-could-possibly-go-wrong.com/fluent-behavior-trees-for-ai-and-game-logic/). 9 | 10 | ## Understanding Behaviour Trees 11 | 12 | Here are some resources to help you understand behaviour trees: 13 | 14 | - [Behaviour tree (Wikipedia)](https://en.wikipedia.org/wiki/Behavior_tree_(artificial_intelligence,_robotics_and_control)) 15 | - [Behavior trees for AI: How they work](http://www.gamasutra.com/blogs/ChrisSimpson/20140717/221339/Behavior_trees_for_AI_How_they_work.php) 16 | - [Understanding Behaviour Trees](http://aigamedev.com/open/article/bt-overview/) 17 | - [Introduction and implementation of Behaviour Trees](http://guineashots.com/2014/07/25/an-introduction-to-behavior-trees-part-1/) 18 | 19 | ## Installation 20 | 21 | Install with npm: 22 | 23 | ```shell 24 | npm install -s fluent-behavior-tree 25 | ``` 26 | 27 | ## Usage 28 | 29 | A behavior tree is created through *BehaviorTreeBuilder*. The tree is returned when the *build* function is called. 30 | 31 | ``` 32 | import {BehaviorTreeBuilder, BehaviorTreeStatus, TimeData} from "fluent-behavior-tree"; 33 | 34 | // ... 35 | 36 | const builder = new BehaviorTreeBuilder(); 37 | this.tree = builder 38 | .sequence("my-sequence") 39 | .do("action1", async (t) => { 40 | // Action 1. 41 | 42 | return BehaviorTreeStatus.Success; 43 | }) 44 | .do("action2", async (t) => { 45 | //Action 2. 46 | 47 | return BehaviorTreeStatus.Failure; 48 | }) 49 | .end() 50 | .build(); 51 | ``` 52 | 53 | Then, *Tick* the behavior tree on each *update* of your *loop* 54 | 55 | ``` 56 | public async update(deltaTime: number): Promise { 57 | await this.tree.tick(new TimeData(deltaTime)); 58 | } 59 | ``` 60 | 61 | ## Behavior Tree Status 62 | 63 | Behavior tree nodes must return the following status codes: 64 | 65 | * *BehaviorTreeStatus.Success*: The node has finished what it was doing and succeeded. 66 | * *BehaviorTreeStatus.Failure*: The node has finished, but failed. 67 | * *BehaviorTreeStatus.Running*: The node is still working on something. 68 | 69 | ## Node Types 70 | 71 | ### Action / Leaf-Node 72 | 73 | Call the *do* function to create an action node at the leaves of the behavior tree. 74 | 75 | ``` 76 | .do("do-something", async (t) => { 77 | // ... Do something ... 78 | 79 | return BehaviorTreeStatus.Success; 80 | }); 81 | ``` 82 | 83 | The return value defines the status of the node. Return one of the statuses from above. 84 | 85 | ### Sequence 86 | 87 | Runs each child node in sequence. Fails for the first child node that *fails*. Moves to the next child when the current running child *succeeds*. Stays on the current child node while it returns *running*. Succeeds when all child nodes have succeeded. 88 | 89 | ``` 90 | .sequence("my-sequence") 91 | .do("action1", async (t) => { // Run this. 92 | // Action 1. 93 | 94 | return BehaviorTreeStatus.Success; 95 | }) 96 | .do("action2", async (t) => { // Then run this. 97 | //Action 2. 98 | 99 | return BehaviorTreeStatus.Failure; 100 | }) 101 | .end() 102 | ``` 103 | 104 | ### Parallel 105 | 106 | Runs all child nodes in parallel. Continues to run until a required number of child nodes have either *failed* or *succeeded*. 107 | 108 | ``` 109 | let numRequiredToFail: number = 2; 110 | let numRequiredToSuccess: number = 2; 111 | 112 | .parallel("my-parallel"m numRequiredtoFail, numRequiredToSucceed) 113 | .do("action1", async (t) => { // Run this at the same time as action2 114 | // Parallel action 1 115 | 116 | return BehaviorTreeStatus.Running; 117 | }) 118 | .do("action12, async (t) => { // Run this at the same time as action1 119 | // Parallel action 2 120 | 121 | return BehaviorTreeStatus.Running; 122 | }) 123 | .end(); 124 | ``` 125 | 126 | ### Selector 127 | 128 | Runs child nodes in sequence until it finds one that *succeeds*. Succeeds when it finds the first child that *succeeds*. For child nodes that *fail*, it moves forward to the next child node. While a child is *running* it stays on that child node without moving forward. 129 | 130 | ``` 131 | .selector("my-selector") 132 | .do("action1", async (t) => { 133 | // Action 1 134 | 135 | return BehaviorTreeStatus.Failure; // Fail, move onto the next child 136 | }) 137 | .do("action2", async (t) => { 138 | // Action 2 139 | 140 | return BehaviorTreeStatus.Success; // Success, stop here. 141 | }) 142 | .do("action3", async (t) => { 143 | // Action 3 144 | 145 | return BehaviorTreeStatus.Success; // Doesn't get this far. 146 | }) 147 | .end(); 148 | ``` 149 | 150 | ### Condition 151 | 152 | The condition function is syntatic sugar for the *do* function. It allows the return of a boolean value that is then converted to *success* or *failure*. It is intended to be used with *Selector*. 153 | 154 | ``` 155 | .selector("my-selector") 156 | .Condition("condition1", async (t) => this.someBooleanConditional()) // Predicate that returns *true* or *false* 157 | .do("action1", async (t) => this.someAction()) // Action to run if the predicate evaluates to *true* 158 | .end() 159 | ``` 160 | 161 | ### Inverter 162 | 163 | Inverts the *success* or *failure* of the child node. Continues running while the child node is *running*. 164 | 165 | ``` 166 | .inverter("inverter1") 167 | .do("action1", async (t) => BehaviourTreeStatus.Success) // *Success* will be inverted to *failure*. 168 | .end() 169 | 170 | 171 | .inverter("inverter1") 172 | .do("action1", async (t) => BehaviourTreeStatus.Failure) // *Failure* will be inverted to *success*. 173 | .end() 174 | ``` 175 | 176 | ## Nesting Behaviour Trees 177 | 178 | Behaviour trees can be nested to any depth, for example: 179 | 180 | ``` 181 | .selector("parent") 182 | .sequence("child-1") 183 | ... 184 | .parallel("grand-child") 185 | ... 186 | .end() 187 | ... 188 | .end() 189 | .sequence("child-2") 190 | ... 191 | .end() 192 | .end() 193 | ``` 194 | 195 | ## Splicing a Sub-tree 196 | 197 | Separately created sub-trees can be spliced into parent trees. This makes it easy to build behaviour trees from reusable components. 198 | 199 | ``` 200 | private createSubTree(): BehaviorTreeNodeInterface 201 | { 202 | return new BehaviourTreeBuilder() 203 | .sequence("my-sub-tree") 204 | .do("action1", async (t) => { 205 | // Action 1. 206 | 207 | return BehaviourTreeStatus.Success; 208 | }) 209 | .Do("action2", async (t) => { 210 | // Action 2. 211 | 212 | return BehaviourTreeStatus.Success; 213 | }); 214 | .end() 215 | .build(); 216 | } 217 | 218 | public startup(): void 219 | { 220 | this.tree = new BehaviourTreeBuilder() 221 | .sequence("my-parent-sequence") 222 | .Splice(this.createSubTree()) // Splice the child tree in. 223 | .Splice(this.createSubTree()) // Splice again. 224 | .end() 225 | .build(); 226 | } 227 | ``` 228 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fluent-behavior-tree", 3 | "version": "1.2.6", 4 | "description": "Behavior tree library with a fluent API", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+ssh://git@github.com/aequasi/fluent-behavior-tree" 10 | }, 11 | "scripts": { 12 | "test": "rm -r test/dist; tsc -v && tsc -p test/ && ava -v", 13 | "compile": "npm run lint && tsc -p ./", 14 | "precommit": "npm run compile", 15 | "prepublishOnly": "npm run compile", 16 | "lint": "tslint -c tslint.json 'src/**/*.ts' --exclude 'src/**/*.d.ts' --fix", 17 | "watch": "nodemon -e ts -w src --exec 'npm run compile'" 18 | }, 19 | "author": "Aaron Scherer (http://aaronscherer.me)", 20 | "license": "MIT", 21 | "dependencies": { 22 | "ts-data.stack": "^1.0.6", 23 | "ts-iterable": "^1.0.1" 24 | }, 25 | "devDependencies": { 26 | "@types/es6-promise": "0.0.33", 27 | "@types/node": "^8.0.57", 28 | "ava": "^0.24.0", 29 | "husky": "^0.14.3", 30 | "tslint": "^5.8.0", 31 | "typemoq": "^2.1.0", 32 | "typescript": "^2.6.2" 33 | }, 34 | "ava": { 35 | "files": [ 36 | "test/dist/**/*Test.js" 37 | ], 38 | "source": [ 39 | "dist/**/*.js" 40 | ] 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/aequasi/fluent-behavior-tree/issues" 44 | }, 45 | "homepage": "https://github.com/aequasi/fluent-behavior-tree#readme" 46 | } 47 | -------------------------------------------------------------------------------- /src/BehaviorTreeBuilder.ts: -------------------------------------------------------------------------------- 1 | import Stack from "ts-data.stack"; 2 | import BehaviorTreeStatus from "./BehaviorTreeStatus"; 3 | import BehaviorTreeError from "./Error/BehaviorTreeError"; 4 | import Errors from "./Error/Errors"; 5 | import ActionNode from "./Node/ActionNode"; 6 | import BehaviorTreeNodeInterface from "./Node/BehaviorTreeNodeInterface"; 7 | import InverterNode from "./Node/InverterNode"; 8 | import ParallelNode from "./Node/ParallelNode"; 9 | import ParentBehaviorTreeNodeInterface from "./Node/ParentBehaviorTreeNodeInterface"; 10 | import SelectorNode from "./Node/SelectorNode"; 11 | import SequenceNode from "./Node/SequenceNode"; 12 | import StateData from "./StateData"; 13 | 14 | export default class BehaviorTreeBuilder { 15 | /** 16 | * Last node created 17 | */ 18 | private curNode?: BehaviorTreeNodeInterface; 19 | 20 | /** 21 | * Stack node nodes that we are build via the fluent API. 22 | * 23 | * @type {Stack} 24 | */ 25 | private parentNodeStack: Stack = new Stack(); 26 | 27 | /** 28 | * Create an action node. 29 | * 30 | * @param {string} name 31 | * @param {(state: StateData) => BehaviorTreeStatus} fn 32 | * @returns {BehaviorTreeBuilder} 33 | */ 34 | public do(name: string, fn: (state: StateData) => Promise): BehaviorTreeBuilder { 35 | if (this.parentNodeStack.isEmpty()) { 36 | throw new BehaviorTreeError(Errors.UNNESTED_ACTION_NODE); 37 | } 38 | 39 | const actionNode = new ActionNode(name, fn); 40 | this.parentNodeStack.peek().addChild(actionNode); 41 | 42 | return this; 43 | } 44 | 45 | /** 46 | * Like an action node... but the function can return true/false and is mapped to success/failure. 47 | * 48 | * @param {string} name 49 | * @param {(state: StateData) => boolean} fn 50 | * @returns {BehaviorTreeBuilder} 51 | */ 52 | public condition(name: string, fn: (state: StateData) => Promise): BehaviorTreeBuilder { 53 | return this.do(name, async (t) => await fn(t) ? BehaviorTreeStatus.Success : BehaviorTreeStatus.Failure); 54 | } 55 | 56 | /** 57 | * Create an inverter node that inverts the success/failure of its children. 58 | * 59 | * @param {string} name 60 | * @returns {BehaviorTreeBuilder} 61 | */ 62 | public inverter(name: string): BehaviorTreeBuilder { 63 | return this.addParentNode(new InverterNode(name)); 64 | } 65 | 66 | /** 67 | * Create a sequence node. 68 | * 69 | * @param {string} name 70 | * @param {boolean} keepState 71 | * @returns {BehaviorTreeBuilder} 72 | */ 73 | public sequence(name: string, keepState: boolean = true): BehaviorTreeBuilder { 74 | return this.addParentNode(new SequenceNode(name, keepState)); 75 | } 76 | 77 | /** 78 | * Create a parallel node. 79 | * 80 | * @param {string} name 81 | * @param {number} requiredToFail 82 | * @param {number} requiredToSucceed 83 | * @returns {BehaviorTreeBuilder} 84 | */ 85 | public parallel(name: string, requiredToFail: number, requiredToSucceed: number): BehaviorTreeBuilder { 86 | return this.addParentNode(new ParallelNode(name, requiredToFail, requiredToSucceed)); 87 | } 88 | 89 | /** 90 | * Create a selector node. 91 | * 92 | * @param {string} name 93 | * @param {boolean} keepState 94 | * @returns {BehaviorTreeBuilder} 95 | */ 96 | public selector(name: string, keepState: boolean = true): BehaviorTreeBuilder { 97 | return this.addParentNode(new SelectorNode(name, keepState)); 98 | } 99 | 100 | /** 101 | * Splice a sub tree into the parent tree. 102 | * 103 | * @param {BehaviorTreeNodeInterface} subTree 104 | * @returns {BehaviorTreeBuilder} 105 | */ 106 | public splice(subTree: BehaviorTreeNodeInterface): BehaviorTreeBuilder { 107 | if (this.parentNodeStack.isEmpty()) { 108 | throw new BehaviorTreeError(Errors.SPLICE_UNNESTED_TREE); 109 | } 110 | 111 | this.parentNodeStack.peek().addChild(subTree); 112 | 113 | return this; 114 | } 115 | 116 | /** 117 | * Build the actual tree 118 | * @returns {BehaviorTreeNodeInterface} 119 | */ 120 | public build(): BehaviorTreeNodeInterface { 121 | if (!this.curNode) { 122 | throw new BehaviorTreeError(Errors.NO_NODES); 123 | } 124 | 125 | return this.curNode; 126 | } 127 | 128 | /** 129 | * Ends a sequence of children. 130 | * 131 | * @returns {BehaviorTreeBuilder} 132 | */ 133 | public end(): BehaviorTreeBuilder { 134 | this.curNode = this.parentNodeStack.pop(); 135 | 136 | return this; 137 | } 138 | 139 | /** 140 | * Adds the parent node to the parentNodeStack 141 | * 142 | * @param {ParentBehaviorTreeNodeInterface} node 143 | * @returns {BehaviorTreeBuilder} 144 | */ 145 | private addParentNode(node: ParentBehaviorTreeNodeInterface): BehaviorTreeBuilder { 146 | if (!this.parentNodeStack.isEmpty()) { 147 | this.parentNodeStack.peek().addChild(node); 148 | } 149 | 150 | this.parentNodeStack.push(node); 151 | 152 | return this; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/BehaviorTreeStatus.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The return type when invoking behavior tree nodes. 3 | */ 4 | enum BehaviorTreeStatus { 5 | Success = "SUCCESS", 6 | Failure = "FAILURE", 7 | Running = "RUNNING", 8 | } 9 | 10 | export default BehaviorTreeStatus; 11 | -------------------------------------------------------------------------------- /src/Error/BehaviorTreeError.ts: -------------------------------------------------------------------------------- 1 | export default class BehaviorTreeError extends Error { 2 | } 3 | -------------------------------------------------------------------------------- /src/Error/Errors.ts: -------------------------------------------------------------------------------- 1 | enum Errors { 2 | NO_NODES = "Cannot create a behavior tree with zero nodes.", 3 | SPLICE_UNNESTED_TREE = "Cannot splice an unnested sub-tree. There must be a parent-tree.", 4 | INVERTER_NO_CHILDREN = "InverterNode must have a child node!", 5 | INVERTER_MULTIPLE_CHILDREN = "Can't add more than a single child to InverterNode!", 6 | UNNESTED_ACTION_NODE = "Can't create an unnested ActionNode. It must be a leaf node.", 7 | NO_RETURN_VALUE = "Node must return a BehaviorTreeStatus", 8 | } 9 | 10 | export default Errors; 11 | -------------------------------------------------------------------------------- /src/Node/ActionNode.ts: -------------------------------------------------------------------------------- 1 | import BehaviorTreeStatus from "../BehaviorTreeStatus"; 2 | import BehaviorTreeError from "../Error/BehaviorTreeError"; 3 | import Errors from "../Error/Errors"; 4 | import StateData from "../StateData"; 5 | import BehaviorTreeNodeInterface from "./BehaviorTreeNodeInterface"; 6 | 7 | /** 8 | * A behavior tree leaf node for running an action 9 | * 10 | * @property {string} name - The name of the node 11 | * @property {(state: StateData) => BehaviorTreeStatus} fn - Function to invoke for the action. 12 | */ 13 | export default class ActionNode implements BehaviorTreeNodeInterface { 14 | public constructor( 15 | public readonly name: string, 16 | public readonly fn: (state: StateData) => Promise, 17 | ) { 18 | } 19 | 20 | public async tick(state: StateData): Promise { 21 | const result = await this.fn(state); 22 | if (!result) { 23 | throw new BehaviorTreeError(Errors.NO_RETURN_VALUE); 24 | } 25 | 26 | return result; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Node/BehaviorTreeNodeInterface.ts: -------------------------------------------------------------------------------- 1 | import BehaviorTreeStatus from "../BehaviorTreeStatus"; 2 | import StateData from "../StateData"; 3 | 4 | export default interface BehaviorTreeNodeInterface { 5 | tick(state: StateData): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /src/Node/InverterNode.ts: -------------------------------------------------------------------------------- 1 | import BehaviorTreeStatus from "../BehaviorTreeStatus"; 2 | import BehaviorTreeError from "../Error/BehaviorTreeError"; 3 | import Errors from "../Error/Errors"; 4 | import StateData from "../StateData"; 5 | import BehaviorTreeNodeInterface from "./BehaviorTreeNodeInterface"; 6 | import ParentBehaviorTreeNodeInterface from "./ParentBehaviorTreeNodeInterface"; 7 | 8 | /** 9 | * Decorator node that inverts the success/failure of its child. 10 | * 11 | * @property {string} name - The name of the node 12 | */ 13 | export default class InverterNode implements ParentBehaviorTreeNodeInterface { 14 | /** 15 | * The child to be inverted 16 | */ 17 | private childNode?: BehaviorTreeNodeInterface; 18 | 19 | public constructor(public readonly name: string) { 20 | } 21 | 22 | public async tick(state: StateData): Promise { 23 | if (!this.childNode) { 24 | throw new BehaviorTreeError(Errors.INVERTER_NO_CHILDREN); 25 | } 26 | 27 | const result = await this.childNode.tick(state); 28 | if (result === BehaviorTreeStatus.Failure) { 29 | return BehaviorTreeStatus.Success; 30 | } else if (result === BehaviorTreeStatus.Success) { 31 | return BehaviorTreeStatus.Failure; 32 | } 33 | 34 | return result; 35 | } 36 | 37 | public addChild(child: BehaviorTreeNodeInterface): void { 38 | if (!!this.childNode) { 39 | throw new BehaviorTreeError(Errors.INVERTER_MULTIPLE_CHILDREN); 40 | } 41 | 42 | this.childNode = child; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Node/ParallelNode.ts: -------------------------------------------------------------------------------- 1 | import BehaviorTreeStatus from "../BehaviorTreeStatus"; 2 | import StateData from "../StateData"; 3 | import BehaviorTreeNodeInterface from "./BehaviorTreeNodeInterface"; 4 | import ParentBehaviorTreeNodeInterface from "./ParentBehaviorTreeNodeInterface"; 5 | 6 | /** 7 | * Runs child's nodes in parallel. 8 | * 9 | * @property {string} name - The name of the node. 10 | * @property {number} requiredToFail - Number of child failures required to terminate with failure. 11 | * @property {number} requiredToSucceed - Number of child successes required to terminate with success. 12 | */ 13 | export default class ParallelNode implements ParentBehaviorTreeNodeInterface { 14 | /** 15 | * List of child nodes. 16 | * 17 | * @type {BehaviorTreeNodeInterface[]} 18 | */ 19 | private children: BehaviorTreeNodeInterface[] = []; 20 | 21 | public constructor( 22 | public readonly name: string, 23 | public readonly requiredToFail: number, 24 | public readonly requiredToSucceed: number, 25 | ) { 26 | } 27 | 28 | public async tick(state: StateData): Promise { 29 | const statuses: BehaviorTreeStatus[] = await Promise.all(this.children.map((c) => this.tickChildren(state, c))); 30 | const succeeded = statuses.filter((x) => x === BehaviorTreeStatus.Success).length; 31 | const failed = statuses.filter((x) => x === BehaviorTreeStatus.Failure).length; 32 | 33 | if (this.requiredToSucceed > 0 && succeeded >= this.requiredToSucceed) { 34 | return BehaviorTreeStatus.Success; 35 | } 36 | if (this.requiredToFail > 0 && failed >= this.requiredToFail) { 37 | return BehaviorTreeStatus.Failure; 38 | } 39 | 40 | return BehaviorTreeStatus.Running; 41 | } 42 | 43 | public addChild(child: BehaviorTreeNodeInterface): void { 44 | this.children.push(child); 45 | } 46 | 47 | private async tickChildren(state: StateData, child: BehaviorTreeNodeInterface): Promise { 48 | try { 49 | return await child.tick(state); 50 | } catch (e) { 51 | return BehaviorTreeStatus.Failure; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Node/ParentBehaviorTreeNodeInterface.ts: -------------------------------------------------------------------------------- 1 | import BehaviorTreeNodeInterface from "./BehaviorTreeNodeInterface"; 2 | 3 | export default interface ParentBehaviorTreeNodeInterface extends BehaviorTreeNodeInterface { 4 | /** 5 | * Add a child node to the selector. 6 | * 7 | * @param {BehaviorTreeNodeInterface} child 8 | */ 9 | addChild(child: BehaviorTreeNodeInterface): void; 10 | } 11 | -------------------------------------------------------------------------------- /src/Node/SelectorNode.ts: -------------------------------------------------------------------------------- 1 | import BehaviorTreeStatus from "../BehaviorTreeStatus"; 2 | import NodeEnumerator from "../NodeEnumerator"; 3 | import StateData from "../StateData"; 4 | import BehaviorTreeNodeInterface from "./BehaviorTreeNodeInterface"; 5 | import ParentBehaviorTreeNodeInterface from "./ParentBehaviorTreeNodeInterface"; 6 | 7 | /** 8 | * Selects the first node that succeeds. Tries successive nodes until it finds one that doesn't fail. 9 | * 10 | * @property {string} name - The name of the node. 11 | */ 12 | export default class SelectorNode implements ParentBehaviorTreeNodeInterface { 13 | /** 14 | * List of child nodes. 15 | * 16 | * @type {BehaviorTreeNodeInterface[]} 17 | */ 18 | private children: BehaviorTreeNodeInterface[] = []; 19 | 20 | /** 21 | * Enumerator to keep state 22 | */ 23 | private enumerator?: NodeEnumerator; 24 | 25 | public constructor(public readonly name: string, private readonly keepState: boolean = false) { 26 | } 27 | 28 | public init(): void { 29 | this.enumerator = new NodeEnumerator(this.children); 30 | } 31 | 32 | public async tick(state: StateData): Promise { 33 | if (!this.enumerator || !this.keepState) { 34 | this.init(); 35 | } 36 | 37 | if (!this.enumerator.current) { 38 | return BehaviorTreeStatus.Running; 39 | } 40 | 41 | do { 42 | const status = await this.enumerator.current.tick(state); 43 | if (status !== BehaviorTreeStatus.Failure) { 44 | if (status === BehaviorTreeStatus.Success) { 45 | this.enumerator.reset(); 46 | } 47 | 48 | return status; 49 | } 50 | 51 | } while (this.enumerator.next()); 52 | this.enumerator.reset(); 53 | 54 | return BehaviorTreeStatus.Failure; 55 | } 56 | 57 | public addChild(child: BehaviorTreeNodeInterface): void { 58 | this.children.push(child); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Node/SequenceNode.ts: -------------------------------------------------------------------------------- 1 | import BehaviorTreeStatus from "../BehaviorTreeStatus"; 2 | import NodeEnumerator from "../NodeEnumerator"; 3 | import StateData from "../StateData"; 4 | import BehaviorTreeNodeInterface from "./BehaviorTreeNodeInterface"; 5 | import ParentBehaviorTreeNodeInterface from "./ParentBehaviorTreeNodeInterface"; 6 | 7 | /** 8 | * Runs child nodes in sequence, until one fails. 9 | * 10 | * @property {string} name - The name of the node. 11 | */ 12 | export default class SequenceNode implements ParentBehaviorTreeNodeInterface { 13 | /** 14 | * List of child nodes. 15 | * 16 | * @type {BehaviorTreeNodeInterface[]} 17 | */ 18 | private children: BehaviorTreeNodeInterface[] = []; 19 | 20 | /** 21 | * Enumerator to keep state 22 | */ 23 | private enumerator?: NodeEnumerator; 24 | 25 | public constructor(public readonly name: string, private readonly keepState: boolean = false) { 26 | } 27 | 28 | public init(): void { 29 | this.enumerator = new NodeEnumerator(this.children); 30 | } 31 | 32 | public async tick(state: StateData): Promise { 33 | if (!this.enumerator || !this.keepState) { 34 | this.init(); 35 | } 36 | 37 | if (!this.enumerator.current) { 38 | return BehaviorTreeStatus.Running; 39 | } 40 | 41 | do { 42 | const status = await this.enumerator.current.tick(state); 43 | if (status !== BehaviorTreeStatus.Success) { 44 | if (status === BehaviorTreeStatus.Failure) { 45 | this.enumerator.reset(); 46 | } 47 | 48 | return status; 49 | } 50 | 51 | } while (this.enumerator.next()); 52 | this.enumerator.reset(); 53 | 54 | return BehaviorTreeStatus.Success; 55 | } 56 | 57 | public addChild(child: BehaviorTreeNodeInterface): void { 58 | this.children.push(child); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/NodeEnumerator.ts: -------------------------------------------------------------------------------- 1 | import BehaviorTreeNodeInterface from "./Node/BehaviorTreeNodeInterface"; 2 | 3 | export default class NodeEnumerator implements Iterable { 4 | public currentIndex: number = 0; 5 | 6 | public get current(): BehaviorTreeNodeInterface { 7 | return this.nodes[this.currentIndex]; 8 | } 9 | 10 | public constructor(public nodes: BehaviorTreeNodeInterface[]) { 11 | this.nodes = nodes; 12 | } 13 | 14 | public [Symbol.iterator](): Iterator { 15 | return { 16 | next: (): IteratorResult => { 17 | let result; 18 | 19 | if (this.currentIndex < this.nodes.length) { 20 | result = {value: this.current, done: false}; 21 | this.next(); 22 | } else { 23 | result = {done: true}; 24 | } 25 | 26 | return result; 27 | }, 28 | }; 29 | } 30 | 31 | public next(): boolean { 32 | if (this.hasNext()) { 33 | this.currentIndex++; 34 | 35 | return true; 36 | } 37 | 38 | return false; 39 | } 40 | 41 | public hasNext(): boolean { 42 | return !!this.nodes[this.currentIndex + 1]; 43 | } 44 | 45 | public reset(): void { 46 | this.currentIndex = 0; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/StateData.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents time and state. Used to pass time values to behavior tree nodes. 3 | * 4 | * @property {number} deltaTime - The current time of this state representation 5 | * @property {object} state - Any state data you would like to pass to the nodes. 6 | */ 7 | export default class StateData { 8 | public constructor(public readonly deltaTime: number = 0, public readonly state: any = {}) { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import BehaviorTreeBuilder from "./BehaviorTreeBuilder"; 2 | import BehaviorTreeStatus from "./BehaviorTreeStatus"; 3 | import BehaviorTreeErorr from "./Error/BehaviorTreeError"; 4 | import Errors from "./Error/Errors"; 5 | import ActionNode from "./Node/ActionNode"; 6 | import BehaviorTreeNodeInterface from "./Node/BehaviorTreeNodeInterface"; 7 | import InverterNode from "./Node/InverterNode"; 8 | import ParallelNode from "./Node/ParallelNode"; 9 | import ParentBehaviorTreeNodeInterface from "./Node/ParentBehaviorTreeNodeInterface"; 10 | import SelectorNode from "./Node/SelectorNode"; 11 | import SequenceNode from "./Node/SequenceNode"; 12 | import StateData from "./StateData"; 13 | 14 | export { 15 | BehaviorTreeBuilder, 16 | BehaviorTreeStatus, 17 | StateData, 18 | BehaviorTreeNodeInterface, 19 | ParentBehaviorTreeNodeInterface, 20 | ActionNode, 21 | InverterNode, 22 | ParallelNode, 23 | SelectorNode, 24 | SequenceNode, 25 | BehaviorTreeErorr, 26 | Errors, 27 | }; 28 | -------------------------------------------------------------------------------- /test/BehaviorTreeBuilderTest.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import BehaviorTreeBuilder from "../src/BehaviorTreeBuilder"; 3 | import BehaviorTreeError from "../src/Error/BehaviorTreeError"; 4 | import BehaviorTreeStatus from "../src/BehaviorTreeStatus"; 5 | import InverterNode from "../src/Node/InverterNode"; 6 | import StateData from "../src/StateData"; 7 | import SequenceNode from "../src/Node/SequenceNode"; 8 | import ParallelNode from "../src/Node/ParallelNode"; 9 | import SelectorNode from "../src/Node/SelectorNode"; 10 | import Errors from "../src/Error/Errors"; 11 | 12 | let testObject: BehaviorTreeBuilder; 13 | 14 | function init(): void { 15 | testObject = new BehaviorTreeBuilder(); 16 | } 17 | 18 | test("can't create a behavior tree with zero nodes", async (assert) => { 19 | init(); 20 | 21 | const error = assert.throws(() => testObject.build(), BehaviorTreeError); 22 | assert.is(error.message, Errors.NO_NODES); 23 | }); 24 | 25 | test("can't create an unested action node", async (assert) => { 26 | init(); 27 | const error = assert.throws(() => { 28 | testObject.do("some-node-1", async () => BehaviorTreeStatus.Running).build(); 29 | }, BehaviorTreeError); 30 | assert.is(error.message, Errors.UNNESTED_ACTION_NODE); 31 | }); 32 | 33 | test("can create inverter node", async (assert) => { 34 | init(); 35 | 36 | const node = testObject 37 | .inverter("some-inverter") 38 | .do("some-node", async () => BehaviorTreeStatus.Success) 39 | .end() 40 | .build(); 41 | 42 | assert.is(InverterNode, node.constructor); 43 | assert.is(BehaviorTreeStatus.Failure, await node.tick(new StateData())); 44 | }); 45 | 46 | test("can't create an unbalanced behavior tree", async (assert) => { 47 | init(); 48 | 49 | const error = assert.throws(() => { 50 | testObject.inverter("some-inverter").do("some-node", async () => BehaviorTreeStatus.Success).build() 51 | }, BehaviorTreeError); 52 | 53 | assert.is(error.message, Errors.NO_NODES); 54 | }); 55 | 56 | test("condition is syntactic sugar for do", async (assert) => { 57 | init(); 58 | const node = testObject 59 | .inverter("some-inverter") 60 | .condition("some-node", async () => true) 61 | .end() 62 | .build(); 63 | 64 | assert.is(InverterNode, node.constructor); 65 | assert.is(BehaviorTreeStatus.Failure, await node.tick(new StateData())); 66 | }); 67 | 68 | test("can invert an inverter", async (assert) => { 69 | init(); 70 | const node = testObject 71 | .inverter("some-inverter") 72 | .inverter("some-inverter") 73 | .do("some-node", async () => BehaviorTreeStatus.Success) 74 | .end() 75 | .end() 76 | .build(); 77 | 78 | assert.is(InverterNode, node.constructor); 79 | assert.is(BehaviorTreeStatus.Success, await node.tick(new StateData())); 80 | }); 81 | 82 | test("adding more than a single child to inverter throws exception", async (assert) => { 83 | init(); 84 | const error = assert.throws(() => { 85 | testObject 86 | .inverter("some-inverter") 87 | .do("some-node", async () => BehaviorTreeStatus.Success) 88 | .do("some-node", async () => BehaviorTreeStatus.Success) 89 | .end() 90 | .build(); 91 | }, BehaviorTreeError); 92 | 93 | assert.is(error.message, Errors.INVERTER_MULTIPLE_CHILDREN); 94 | }); 95 | 96 | test("can create a sequence", async (assert) => { 97 | init(); 98 | let invokeCount = 0; 99 | const sequence = testObject 100 | .sequence("some-sequence") 101 | .do("some-action-1", async () => { 102 | ++invokeCount; 103 | 104 | return BehaviorTreeStatus.Success; 105 | }) 106 | .do("some-action-2", async () => { 107 | ++invokeCount; 108 | 109 | return BehaviorTreeStatus.Success; 110 | }) 111 | .end() 112 | .build(); 113 | 114 | assert.is(SequenceNode, sequence.constructor); 115 | assert.is(BehaviorTreeStatus.Success, await sequence.tick(new StateData())); 116 | assert.is(2, invokeCount); 117 | }); 118 | 119 | test("can create a parallel", async (assert) => { 120 | init(); 121 | let invokeCount = 0; 122 | const parallel = testObject 123 | .parallel("some-parallel", 2, 2) 124 | .do("some-action-1", async () => { 125 | ++invokeCount; 126 | 127 | return BehaviorTreeStatus.Success; 128 | }) 129 | .do("some-action-2", async () => { 130 | ++invokeCount; 131 | 132 | return BehaviorTreeStatus.Success; 133 | }) 134 | .end() 135 | .build(); 136 | 137 | assert.is(ParallelNode, parallel.constructor); 138 | assert.is(BehaviorTreeStatus.Success, await parallel.tick(new StateData())); 139 | assert.is(2, invokeCount); 140 | }); 141 | 142 | test("can create a selector", async (assert) => { 143 | init(); 144 | let invokeCount = 0; 145 | const selector = testObject 146 | .selector("some-parallel") 147 | .do("some-action-1", async () => { 148 | ++invokeCount; 149 | 150 | return BehaviorTreeStatus.Failure; 151 | }) 152 | .do("some-action-2", async () => { 153 | ++invokeCount; 154 | 155 | return BehaviorTreeStatus.Success; 156 | }) 157 | .end() 158 | .build(); 159 | 160 | assert.is(SelectorNode, selector.constructor); 161 | assert.is(BehaviorTreeStatus.Success, await selector.tick(new StateData())); 162 | assert.is(2, invokeCount); 163 | }); 164 | 165 | test("can splice sub tree", async (assert) => { 166 | init(); 167 | let invokeCount = 0; 168 | const spliced = testObject 169 | .sequence("spliced") 170 | .do("test", async () => { 171 | ++invokeCount; 172 | 173 | return BehaviorTreeStatus.Success 174 | }) 175 | .end() 176 | .build(); 177 | 178 | const tree = testObject 179 | .sequence("parent-tree") 180 | .splice(spliced) 181 | .splice(spliced) 182 | .end() 183 | .build(); 184 | 185 | await tree.tick(new StateData); 186 | 187 | assert.is(2, invokeCount); 188 | }); 189 | 190 | test("splicing an unnested sub tree throws exception", async (assert) => { 191 | init(); 192 | const error = assert.throws(() => { 193 | testObject.splice( 194 | testObject.sequence("spliced") 195 | .do("test", async () => BehaviorTreeStatus.Success) 196 | .end() 197 | .build() 198 | ); 199 | }, BehaviorTreeError); 200 | assert.is(error.message, Errors.SPLICE_UNNESTED_TREE); 201 | }) 202 | -------------------------------------------------------------------------------- /test/Node/ActionNodeTest.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import StateData from "../../src/StateData"; 3 | import ActionNode from "../../src/Node/ActionNode"; 4 | import BehaviorTreeStatus from "../../src/BehaviorTreeStatus"; 5 | 6 | test("can run action", async (assert) => { 7 | const state = new StateData(); 8 | let invokeCount = 0; 9 | const testObject = new ActionNode( 10 | "some-action", 11 | async (s) => { 12 | assert.is(state, s); 13 | ++invokeCount; 14 | 15 | return BehaviorTreeStatus.Running; 16 | } 17 | ); 18 | 19 | assert.is(BehaviorTreeStatus.Running, await testObject.tick(state)); 20 | assert.is(1, invokeCount); 21 | }); 22 | 23 | 24 | test("state is available to nodes", async (assert) => { 25 | const state = new StateData(0, {test: 'foo', bar: 'baz'}); 26 | const testObject = new ActionNode("some-action", async (s) => { 27 | assert.is(state.state.test, s.state.test); 28 | 29 | return BehaviorTreeStatus.Success 30 | }); 31 | 32 | assert.is(BehaviorTreeStatus.Success, await testObject.tick(state)); 33 | }); 34 | -------------------------------------------------------------------------------- /test/Node/InverterNodeTest.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import * as TypeMoq from "typemoq"; 3 | import StateData from "../../src/StateData"; 4 | import InverterNode from "../../src/Node/InverterNode"; 5 | import BehaviorTreeNodeInterface from "../../src/Node/BehaviorTreeNodeInterface"; 6 | import BehaviorTreeStatus from "../../src/BehaviorTreeStatus"; 7 | import BehaviorTreeError from "../../src/Error/BehaviorTreeError"; 8 | import Errors from "../../src/Error/Errors"; 9 | 10 | let testObject: InverterNode; 11 | test.beforeEach(() => { 12 | testObject = new InverterNode("some-node"); 13 | }); 14 | 15 | test.afterEach.always(() => { 16 | testObject = undefined; 17 | }); 18 | 19 | test("ticking with no child node throws error", async (assert) => { 20 | try { 21 | await testObject.tick(new StateData()); 22 | } catch (e) { 23 | assert.throws(() => {throw e}, BehaviorTreeError, "should have thrown"); 24 | assert.is(e.message, Errors.INVERTER_NO_CHILDREN); 25 | } 26 | }); 27 | 28 | test("inverts success of child node", async (assert) => { 29 | const state = new StateData(); 30 | const mockChildNode = TypeMoq.Mock.ofType(); 31 | 32 | mockChildNode 33 | .setup(async (m) => await m.tick(state)) 34 | .returns(() => Promise.resolve(BehaviorTreeStatus.Success)); 35 | 36 | testObject.addChild(mockChildNode.object); 37 | assert.is(BehaviorTreeStatus.Failure, await testObject.tick(state)); 38 | mockChildNode.verify((m) => m.tick(state), TypeMoq.Times.once()); 39 | }); 40 | 41 | test("inverts failure of child node", async (assert) => { 42 | const state = new StateData(); 43 | const mockChildNode = TypeMoq.Mock.ofType(); 44 | 45 | mockChildNode 46 | .setup(async (m) => await m.tick(state)) 47 | .returns(() => Promise.resolve(BehaviorTreeStatus.Failure)); 48 | 49 | testObject.addChild(mockChildNode.object); 50 | assert.is(BehaviorTreeStatus.Success, await testObject.tick(state)); 51 | mockChildNode.verify((m) => m.tick(state), TypeMoq.Times.once()); 52 | }); 53 | 54 | test("pass through running of child node", async (assert) => { 55 | const state = new StateData(); 56 | const mockChildNode = TypeMoq.Mock.ofType(); 57 | 58 | mockChildNode 59 | .setup(async (m) => await m.tick(state)) 60 | .returns(() => Promise.resolve(BehaviorTreeStatus.Running)); 61 | 62 | testObject.addChild(mockChildNode.object); 63 | assert.is(BehaviorTreeStatus.Running, await testObject.tick(state)); 64 | mockChildNode.verify((m) => m.tick(state), TypeMoq.Times.once()); 65 | }); 66 | 67 | test("adding more than a single child throws exception", async (assert) => { 68 | testObject.addChild(TypeMoq.Mock.ofType() as any); 69 | const error = assert.throws( 70 | () => testObject.addChild(TypeMoq.Mock.ofType() as any), 71 | BehaviorTreeError, 72 | ); 73 | assert.is(error.message, Errors.INVERTER_MULTIPLE_CHILDREN); 74 | }); 75 | -------------------------------------------------------------------------------- /test/Node/ParallelNodeTest.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import * as TypeMoq from "typemoq"; 3 | import StateData from "../../src/StateData"; 4 | import BehaviorTreeNodeInterface from "../../src/Node/BehaviorTreeNodeInterface"; 5 | import BehaviorTreeStatus from "../../src/BehaviorTreeStatus"; 6 | import ParallelNode from "../../src/Node/ParallelNode"; 7 | 8 | const util = require('util'); 9 | 10 | let testObject: ParallelNode; 11 | 12 | function init(requiredToFail: number = 0, requiredToSucceed: number = 0): void { 13 | testObject = new ParallelNode("some-parallel", requiredToFail, requiredToSucceed); 14 | } 15 | 16 | test("runs all nodes in order", async (assert) => { 17 | init(); 18 | const state = new StateData(); 19 | let callOrder = 0; 20 | 21 | const mockChild1 = TypeMoq.Mock.ofType(); 22 | mockChild1.setup(async (m) => await m.tick(state)) 23 | .returns(() => { 24 | assert.is(1, ++callOrder); 25 | 26 | return Promise.resolve(BehaviorTreeStatus.Running) 27 | }); 28 | 29 | const mockChild2 = TypeMoq.Mock.ofType(); 30 | mockChild2.setup(async (m) => await m.tick(state)) 31 | .returns(() => { 32 | assert.is(2, ++callOrder); 33 | 34 | return Promise.resolve(BehaviorTreeStatus.Running) 35 | }); 36 | 37 | testObject.addChild(mockChild1.object); 38 | testObject.addChild(mockChild2.object); 39 | assert.is(BehaviorTreeStatus.Running, await testObject.tick(state)); 40 | assert.is(2, callOrder); 41 | mockChild1.verify((m) => m.tick(state), TypeMoq.Times.once()); 42 | mockChild2.verify((m) => m.tick(state), TypeMoq.Times.once()); 43 | }); 44 | 45 | test("fails when required number of children fail", async (assert) => { 46 | init(2, 2); 47 | const state = new StateData(); 48 | 49 | const mockChild1 = TypeMoq.Mock.ofType(); 50 | mockChild1.setup(async (m) => await m.tick(state)) 51 | .returns(() => { 52 | return Promise.resolve(BehaviorTreeStatus.Failure) 53 | }); 54 | 55 | const mockChild2 = TypeMoq.Mock.ofType(); 56 | mockChild2.setup(async (m) => await m.tick(state)) 57 | .returns(() => { 58 | return Promise.resolve(BehaviorTreeStatus.Failure) 59 | }); 60 | 61 | const mockChild3 = TypeMoq.Mock.ofType(); 62 | mockChild3.setup(async (m) => await m.tick(state)) 63 | .returns(() => { 64 | return Promise.resolve(BehaviorTreeStatus.Running) 65 | }); 66 | 67 | testObject.addChild(mockChild1.object); 68 | testObject.addChild(mockChild2.object); 69 | testObject.addChild(mockChild3.object); 70 | 71 | assert.is(BehaviorTreeStatus.Failure, await testObject.tick(state)); 72 | 73 | mockChild1.verify((m) => m.tick(state), TypeMoq.Times.once()); 74 | mockChild2.verify((m) => m.tick(state), TypeMoq.Times.once()); 75 | mockChild3.verify((m) => m.tick(state), TypeMoq.Times.once()); 76 | }); 77 | 78 | test("succeeds when required number of children succeed", async (assert) => { 79 | init(2, 2); 80 | const state = new StateData(); 81 | 82 | const mockChild1 = TypeMoq.Mock.ofType(); 83 | mockChild1.setup(async (m) => await m.tick(state)) 84 | .returns(() => { 85 | return Promise.resolve(BehaviorTreeStatus.Success) 86 | }); 87 | 88 | const mockChild2 = TypeMoq.Mock.ofType(); 89 | mockChild2.setup(async (m) => await m.tick(state)) 90 | .returns(() => { 91 | return Promise.resolve(BehaviorTreeStatus.Success) 92 | }); 93 | 94 | const mockChild3 = TypeMoq.Mock.ofType(); 95 | mockChild3.setup(async (m) => await m.tick(state)) 96 | .returns(() => { 97 | return Promise.resolve(BehaviorTreeStatus.Running) 98 | }); 99 | 100 | testObject.addChild(mockChild1.object); 101 | testObject.addChild(mockChild2.object); 102 | testObject.addChild(mockChild3.object); 103 | 104 | assert.is(BehaviorTreeStatus.Success, await testObject.tick(state)); 105 | 106 | mockChild1.verify((m) => m.tick(state), TypeMoq.Times.once()); 107 | mockChild2.verify((m) => m.tick(state), TypeMoq.Times.once()); 108 | mockChild3.verify((m) => m.tick(state), TypeMoq.Times.once()); 109 | }); 110 | 111 | test("continues to run if the required number of children neither succeed or fail", async (assert) => { 112 | init(2, 2); 113 | const state = new StateData(); 114 | 115 | const mockChild1 = TypeMoq.Mock.ofType(); 116 | mockChild1.setup(async (m) => await m.tick(state)) 117 | .returns(() => { 118 | return Promise.resolve(BehaviorTreeStatus.Success) 119 | }); 120 | 121 | const mockChild2 = TypeMoq.Mock.ofType(); 122 | mockChild2.setup(async (m) => await m.tick(state)) 123 | .returns(() => { 124 | return Promise.resolve(BehaviorTreeStatus.Failure) 125 | }); 126 | 127 | testObject.addChild(mockChild1.object); 128 | testObject.addChild(mockChild2.object); 129 | 130 | assert.is(BehaviorTreeStatus.Running, await testObject.tick(state)); 131 | 132 | mockChild1.verify((m) => m.tick(state), TypeMoq.Times.once()); 133 | mockChild2.verify((m) => m.tick(state), TypeMoq.Times.once()); 134 | }); 135 | -------------------------------------------------------------------------------- /test/Node/SelectorNodeTest.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import * as TypeMoq from "typemoq"; 3 | import StateData from "../../src/StateData"; 4 | import BehaviorTreeNodeInterface from "../../src/Node/BehaviorTreeNodeInterface"; 5 | import BehaviorTreeStatus from "../../src/BehaviorTreeStatus"; 6 | import SelectorNode from "../../src/Node/SelectorNode"; 7 | 8 | let testObject: SelectorNode; 9 | 10 | function init(keepState: boolean = false): void { 11 | testObject = new SelectorNode("some-selector", keepState); 12 | } 13 | 14 | test("runs the first node if it succeeds", async (assert) => { 15 | init(); 16 | const state = new StateData(); 17 | 18 | const mockChild1 = TypeMoq.Mock.ofType(); 19 | mockChild1.setup(async (m) => await m.tick(state)) 20 | .returns(() => Promise.resolve(BehaviorTreeStatus.Success)); 21 | 22 | const mockChild2 = TypeMoq.Mock.ofType(); 23 | 24 | testObject.addChild(mockChild1.object); 25 | testObject.addChild(mockChild2.object); 26 | assert.is(BehaviorTreeStatus.Success, await testObject.tick(state)); 27 | mockChild1.verify((m) => m.tick(state), TypeMoq.Times.once()); 28 | mockChild2.verify((m) => m.tick(state), TypeMoq.Times.never()); 29 | }); 30 | 31 | test("stops on the first node when it is running", async (assert) => { 32 | init(); 33 | const state = new StateData(); 34 | 35 | const mockChild1 = TypeMoq.Mock.ofType(); 36 | mockChild1.setup(async (m) => await m.tick(state)) 37 | .returns(() => Promise.resolve(BehaviorTreeStatus.Running)); 38 | 39 | const mockChild2 = TypeMoq.Mock.ofType(); 40 | 41 | testObject.addChild(mockChild1.object); 42 | testObject.addChild(mockChild2.object); 43 | assert.is(BehaviorTreeStatus.Running, await testObject.tick(state)); 44 | mockChild1.verify((m) => m.tick(state), TypeMoq.Times.once()); 45 | mockChild2.verify((m) => m.tick(state), TypeMoq.Times.never()); 46 | }); 47 | 48 | test("runs the second node if the first fails", async (assert) => { 49 | init(); 50 | const state = new StateData(); 51 | 52 | const mockChild1 = TypeMoq.Mock.ofType(); 53 | mockChild1.setup(async (m) => await m.tick(state)) 54 | .returns(() => Promise.resolve(BehaviorTreeStatus.Failure)); 55 | 56 | const mockChild2 = TypeMoq.Mock.ofType(); 57 | mockChild2.setup(async (m) => await m.tick(state)) 58 | .returns(() => Promise.resolve(BehaviorTreeStatus.Success)); 59 | 60 | testObject.addChild(mockChild1.object); 61 | testObject.addChild(mockChild2.object); 62 | assert.is(BehaviorTreeStatus.Success, await testObject.tick(state)); 63 | mockChild1.verify((m) => m.tick(state), TypeMoq.Times.once()); 64 | mockChild2.verify((m) => m.tick(state), TypeMoq.Times.once()); 65 | }); 66 | 67 | test("fails when all children fail", async (assert) => { 68 | init(); 69 | const state = new StateData(); 70 | 71 | const mockChild1 = TypeMoq.Mock.ofType(); 72 | mockChild1.setup(async (m) => await m.tick(state)) 73 | .returns(() => Promise.resolve(BehaviorTreeStatus.Failure)); 74 | 75 | const mockChild2 = TypeMoq.Mock.ofType(); 76 | mockChild2.setup(async (m) => await m.tick(state)) 77 | .returns(() => Promise.resolve(BehaviorTreeStatus.Failure)); 78 | 79 | testObject.addChild(mockChild1.object); 80 | testObject.addChild(mockChild2.object); 81 | assert.is(BehaviorTreeStatus.Failure, await testObject.tick(state)); 82 | mockChild1.verify((m) => m.tick(state), TypeMoq.Times.once()); 83 | mockChild2.verify((m) => m.tick(state), TypeMoq.Times.once()); 84 | }); 85 | 86 | test("only evaluates the current node", async (assert) => { 87 | init(true); 88 | const state = new StateData(); 89 | const mockChild1 = TypeMoq.Mock.ofType(); 90 | mockChild1.setup(async (m) => await m.tick(state)) 91 | .returns(() => Promise.resolve(BehaviorTreeStatus.Failure)); 92 | const mockChild2 = TypeMoq.Mock.ofType(); 93 | mockChild2.setup(async (m) => await m.tick(state)) 94 | .returns(() => Promise.resolve(BehaviorTreeStatus.Running)); 95 | const mockChild3 = TypeMoq.Mock.ofType(); 96 | mockChild3.setup(async (m) => await m.tick(state)) 97 | .returns(() => Promise.resolve(BehaviorTreeStatus.Success)); 98 | 99 | testObject.addChild(mockChild1.object); 100 | testObject.addChild(mockChild2.object); 101 | testObject.addChild(mockChild3.object); 102 | 103 | assert.is(BehaviorTreeStatus.Running, await testObject.tick(state)); 104 | assert.is(BehaviorTreeStatus.Running, await testObject.tick(state)); 105 | 106 | mockChild1.verify((m) => m.tick(state), TypeMoq.Times.once()); 107 | mockChild2.verify((m) => m.tick(state), TypeMoq.Times.exactly(2)); 108 | mockChild3.verify((m) => m.tick(state), TypeMoq.Times.never()); 109 | }); 110 | -------------------------------------------------------------------------------- /test/Node/SequenceNodeTest.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import * as TypeMoq from "typemoq"; 3 | import StateData from "../../src/StateData"; 4 | import BehaviorTreeNodeInterface from "../../src/Node/BehaviorTreeNodeInterface"; 5 | import BehaviorTreeStatus from "../../src/BehaviorTreeStatus"; 6 | import SequenceNode from "../../src/Node/SequenceNode"; 7 | 8 | let testObject: SequenceNode; 9 | 10 | function init(keepState: boolean = false): void { 11 | testObject = new SequenceNode("some-sequence", keepState); 12 | } 13 | 14 | test("can run all children in order", async (assert) => { 15 | init(); 16 | const state = new StateData(); 17 | let callOrder = 0; 18 | 19 | const mockChild1 = TypeMoq.Mock.ofType(); 20 | mockChild1.setup(async (m) => await m.tick(state)) 21 | .returns(() => { 22 | assert.is(1, ++callOrder); 23 | 24 | return Promise.resolve(BehaviorTreeStatus.Success) 25 | }); 26 | 27 | const mockChild2 = TypeMoq.Mock.ofType(); 28 | mockChild2.setup(async (m) => await m.tick(state)) 29 | .returns(() => { 30 | assert.is(2, ++callOrder); 31 | 32 | return Promise.resolve(BehaviorTreeStatus.Success) 33 | }); 34 | 35 | testObject.addChild(mockChild1.object); 36 | testObject.addChild(mockChild2.object); 37 | assert.is(BehaviorTreeStatus.Success, await testObject.tick(state)); 38 | assert.is(2, callOrder); 39 | mockChild1.verify((m) => m.tick(state), TypeMoq.Times.once()); 40 | mockChild2.verify((m) => m.tick(state), TypeMoq.Times.once()); 41 | }); 42 | 43 | test("when first child is running, second child is supressed", async (assert) => { 44 | init(); 45 | const state = new StateData(); 46 | 47 | const mockChild1 = TypeMoq.Mock.ofType(); 48 | mockChild1.setup(async (m) => await m.tick(state)) 49 | .returns(() => Promise.resolve(BehaviorTreeStatus.Running)); 50 | const mockChild2 = TypeMoq.Mock.ofType(); 51 | 52 | testObject.addChild(mockChild1.object); 53 | testObject.addChild(mockChild2.object); 54 | assert.is(BehaviorTreeStatus.Running, await testObject.tick(state)); 55 | mockChild1.verify((m) => m.tick(state), TypeMoq.Times.once()); 56 | mockChild2.verify((m) => m.tick(state), TypeMoq.Times.never()); 57 | }); 58 | 59 | test("when first child fails, then entire sequence fails", async (assert) => { 60 | init(); 61 | const state = new StateData(); 62 | 63 | const mockChild1 = TypeMoq.Mock.ofType(); 64 | mockChild1.setup(async (m) => await m.tick(state)) 65 | .returns(() => Promise.resolve(BehaviorTreeStatus.Failure)); 66 | const mockChild2 = TypeMoq.Mock.ofType(); 67 | 68 | testObject.addChild(mockChild1.object); 69 | testObject.addChild(mockChild2.object); 70 | assert.is(BehaviorTreeStatus.Failure, await testObject.tick(state)); 71 | mockChild1.verify((m) => m.tick(state), TypeMoq.Times.once()); 72 | mockChild2.verify((m) => m.tick(state), TypeMoq.Times.never()); 73 | }); 74 | 75 | test("when second child fails, then entire sequence fails", async (assert) => { 76 | init(); 77 | const state = new StateData(); 78 | 79 | const mockChild1 = TypeMoq.Mock.ofType(); 80 | mockChild1.setup(async (m) => await m.tick(state)) 81 | .returns(() => Promise.resolve(BehaviorTreeStatus.Success)); 82 | const mockChild2 = TypeMoq.Mock.ofType(); 83 | mockChild2.setup(async (m) => await m.tick(state)) 84 | .returns(() => Promise.resolve(BehaviorTreeStatus.Failure)); 85 | 86 | testObject.addChild(mockChild1.object); 87 | testObject.addChild(mockChild2.object); 88 | assert.is(BehaviorTreeStatus.Failure, await testObject.tick(state)); 89 | mockChild1.verify((m) => m.tick(state), TypeMoq.Times.once()); 90 | mockChild2.verify((m) => m.tick(state), TypeMoq.Times.once()); 91 | }); 92 | 93 | test("only evaluates the current node", async (assert) => { 94 | init(true); 95 | const state = new StateData(); 96 | const mockChild1 = TypeMoq.Mock.ofType(); 97 | mockChild1.setup(async (m) => await m.tick(state)) 98 | .returns(() => Promise.resolve(BehaviorTreeStatus.Success)); 99 | const mockChild2 = TypeMoq.Mock.ofType(); 100 | mockChild2.setup(async (m) => await m.tick(state)) 101 | .returns(() => Promise.resolve(BehaviorTreeStatus.Running)); 102 | const mockChild3 = TypeMoq.Mock.ofType(); 103 | mockChild3.setup(async (m) => await m.tick(state)) 104 | .returns(() => Promise.resolve(BehaviorTreeStatus.Failure)); 105 | 106 | testObject.addChild(mockChild1.object); 107 | testObject.addChild(mockChild2.object); 108 | testObject.addChild(mockChild3.object); 109 | 110 | assert.is(BehaviorTreeStatus.Running, await testObject.tick(state)); 111 | assert.is(BehaviorTreeStatus.Running, await testObject.tick(state)); 112 | 113 | mockChild1.verify((m) => m.tick(state), TypeMoq.Times.once()); 114 | mockChild2.verify((m) => m.tick(state), TypeMoq.Times.exactly(2)); 115 | mockChild3.verify((m) => m.tick(state), TypeMoq.Times.never()); 116 | }); 117 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "Node", 4 | "module": "commonjs", 5 | "target": "es6", 6 | "sourceMap": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "noImplicitAny": false, 10 | "noImplicitThis": true, 11 | "declaration": true, 12 | "outDir": "./dist", 13 | "lib": [ 14 | "es2017.object", 15 | "es2015", 16 | "es2017", 17 | "esnext" 18 | ], 19 | "pretty": true, 20 | "skipLibCheck": true 21 | }, 22 | "include": [ 23 | "./**/*.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "Node", 4 | "module": "commonjs", 5 | "target": "es2015", 6 | "sourceMap": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "noImplicitAny": false, 10 | "noImplicitThis": true, 11 | "declaration": true, 12 | "outDir": "./dist", 13 | "lib": [ 14 | "es2017.object", 15 | "es2015", 16 | "es2017", 17 | "esnext" 18 | ], 19 | "pretty": true, 20 | "skipLibCheck": true 21 | }, 22 | "include": [ 23 | "./src/**/*.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "curly": true, 5 | "variable-name": [ 6 | true, 7 | "ban-keywords", 8 | "check-format", 9 | "allow-leading-underscore", 10 | "allow-pascal-case" 11 | ], 12 | "no-console": false, 13 | "no-bitwise": false, 14 | "no-empty": false, 15 | "no-eval": false, 16 | "no-string-literal": false, 17 | "object-literal-sort-keys": false, 18 | "no-var-requires": false, 19 | "interface-name": [ 20 | true, 21 | "never-prefix" 22 | ] 23 | }, 24 | "exclude": [ 25 | "src/extensions.ts" 26 | ], 27 | "jsRules": { 28 | "curly": true 29 | }, 30 | "rulesDirectory": [ 31 | ] 32 | } 33 | --------------------------------------------------------------------------------