behaviortree
Version:
A JavaScript implementation of Behavior Trees. They are useful for implementing AIs. For Browsers and NodeJS.
379 lines (378 loc) • 14.8 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/* eslint-env jest */
const constants_1 = require("./constants");
const BehaviorTree_1 = __importDefault(require("./BehaviorTree"));
const Task_1 = __importDefault(require("./Task"));
const InvertDecorator_1 = __importDefault(require("./decorators/InvertDecorator"));
const Introspector_1 = __importDefault(require("./Introspector"));
const Parallel_1 = __importDefault(require("./Parallel"));
const Selector_1 = __importDefault(require("./Selector"));
const Sequence_1 = __importDefault(require("./Sequence"));
const Random_1 = __importDefault(require("./Random"));
describe('Introspector', () => {
let bTree;
let blackboard;
let introspector;
const simpleTask = new Task_1.default({
name: 'The Task',
start: function (blackboard) {
++blackboard.start;
},
run: function (blackboard) {
++blackboard.run;
return blackboard.result;
},
end: function (blackboard) {
++blackboard.end;
}
});
const failingTask = new Task_1.default({
name: 'Bumm',
run: function () {
return constants_1.FAILURE;
}
});
const runningTask = new Task_1.default({
name: 'Forest',
run: function () {
return constants_1.RUNNING;
}
});
BehaviorTree_1.default.register('simpleTask', simpleTask);
BehaviorTree_1.default.register('failingTask', failingTask);
BehaviorTree_1.default.register('runningTask', runningTask);
beforeEach(() => {
blackboard = {
start: 0,
run: 0,
end: 0,
result: constants_1.SUCCESS
};
introspector = new Introspector_1.default();
});
it('is empty initially', () => {
expect(introspector.lastResult).toEqual(null);
expect(introspector.results).toEqual([]);
});
describe('with the simplest tree possible', () => {
beforeEach(() => {
bTree = new BehaviorTree_1.default({ tree: 'simpleTask', blackboard });
});
it('puts in the result of the last run', () => {
bTree.step({ introspector });
const resultFirstRun = {
name: 'The Task',
result: constants_1.SUCCESS
};
expect(introspector.lastResult).toEqual(resultFirstRun);
expect(introspector.results).toEqual([resultFirstRun]);
blackboard.result = constants_1.FAILURE;
bTree.step({ introspector });
const resultSecondRun = {
name: 'The Task',
result: constants_1.FAILURE
};
expect(introspector.lastResult).toEqual(resultSecondRun);
expect(introspector.results).toEqual([resultFirstRun, resultSecondRun]);
blackboard.result = constants_1.RUNNING;
bTree.step({ introspector });
const resultThirdRun = {
name: 'The Task',
result: constants_1.RUNNING
};
expect(introspector.lastResult).toEqual(resultThirdRun);
expect(introspector.results).toEqual([resultFirstRun, resultSecondRun, resultThirdRun]);
});
});
describe('with nameless tasks', () => {
beforeEach(() => {
blackboard = {
start: 0,
run: 0,
end: 0,
result: constants_1.SUCCESS
};
bTree = new BehaviorTree_1.default({ tree: new Task_1.default({ run: () => constants_1.RUNNING }), blackboard });
});
it('does not print a name', () => {
bTree.step({ introspector });
const resultFirstRun = {
result: constants_1.RUNNING
};
expect(introspector.lastResult).toEqual(resultFirstRun);
expect(introspector.results).toEqual([resultFirstRun]);
});
});
describe('with nameless branching nodes', () => {
beforeEach(() => {
blackboard = {
start: 0,
run: 0,
end: 0,
result: constants_1.SUCCESS
};
bTree = new BehaviorTree_1.default({ tree: new Sequence_1.default({ nodes: ['simpleTask'] }), blackboard });
});
it('does not print a name', () => {
bTree.step({ introspector });
const resultFirstRun = {
children: [
{
name: 'The Task',
result: constants_1.SUCCESS
}
],
result: constants_1.SUCCESS
};
expect(introspector.lastResult).toEqual(resultFirstRun);
expect(introspector.results).toEqual([resultFirstRun]);
});
});
describe('with a decorator', () => {
beforeEach(() => {
blackboard = {
start: 0,
run: 0,
end: 0,
result: constants_1.SUCCESS
};
const tree = new InvertDecorator_1.default({ name: 'inverter', node: 'simpleTask' });
bTree = new BehaviorTree_1.default({ tree, blackboard });
});
it('shows Task and Decorator', () => {
bTree.step({ introspector });
const result = {
name: 'inverter',
result: constants_1.FAILURE,
children: [
{
name: 'The Task',
result: constants_1.SUCCESS
}
]
};
expect(introspector.lastResult).toEqual(result);
expect(introspector.results).toEqual([result]);
});
});
describe('with a selector', () => {
beforeEach(() => {
blackboard = {
start: 0,
run: 0,
end: 0,
result: constants_1.SUCCESS
};
});
it('does not show task that did not run', () => {
const tree = new Selector_1.default({ name: 'select', nodes: ['simpleTask', 'failingTask'] });
bTree = new BehaviorTree_1.default({ tree, blackboard });
bTree.step({ introspector });
const result = {
name: 'select',
result: constants_1.SUCCESS,
children: [
{
name: 'The Task',
result: constants_1.SUCCESS
}
]
};
expect(introspector.lastResult).toEqual(result);
expect(introspector.results).toEqual([result]);
});
it('show all tasks if all did run', () => {
const tree = new Selector_1.default({ name: 'select', nodes: ['failingTask', 'simpleTask'] });
bTree = new BehaviorTree_1.default({ tree, blackboard });
bTree.step({ introspector });
const result = {
name: 'select',
result: constants_1.SUCCESS,
children: [
{
name: 'Bumm',
result: constants_1.FAILURE
},
{
name: 'The Task',
result: constants_1.SUCCESS
}
]
};
expect(introspector.lastResult).toEqual(result);
expect(introspector.results).toEqual([result]);
});
it('does not show more then was running', () => {
const tree = new Selector_1.default({ name: 'select', nodes: ['runningTask', 'simpleTask'] });
bTree = new BehaviorTree_1.default({ tree, blackboard });
bTree.step({ introspector });
const result = {
name: 'select',
result: constants_1.RUNNING,
children: [
{
name: 'Forest',
result: constants_1.RUNNING
}
]
};
expect(introspector.lastResult).toEqual(result);
expect(introspector.results).toEqual([result]);
});
});
describe('with a parallel node', () => {
beforeEach(() => {
blackboard = {
start: 0,
run: 0,
end: 0,
result: constants_1.SUCCESS
};
});
it('shows all tasks that did and did not run', () => {
const tree = new Parallel_1.default({ name: 'parallel', nodes: ['runningTask', 'simpleTask'] });
bTree = new BehaviorTree_1.default({ tree, blackboard });
bTree.step({ introspector });
const result = {
name: 'parallel',
result: constants_1.RUNNING,
children: [
{
name: 'Forest',
result: constants_1.RUNNING
},
{
name: 'The Task',
result: constants_1.SUCCESS
}
]
};
expect(introspector.lastResult).toEqual(result);
expect(introspector.results).toEqual([result]);
});
});
describe('a full scale tree', () => {
beforeEach(() => {
blackboard = {
start: 0,
run: 0,
end: 0,
result: constants_1.SUCCESS
};
});
it('shows all that did run', () => {
const invertedSimple = new InvertDecorator_1.default({ node: 'simpleTask' });
const selector1 = new Selector_1.default({ name: 'select1', nodes: ['failingTask', 'simpleTask'] });
const selector2 = new Selector_1.default({ name: 'select2', nodes: [invertedSimple, 'simpleTask', 'failingTask'] });
const tree = new Sequence_1.default({ name: 'sequence', nodes: [selector1, selector2] });
bTree = new BehaviorTree_1.default({ tree, blackboard });
bTree.step({ introspector });
const result = {
name: 'sequence',
result: constants_1.SUCCESS,
children: [
{
name: 'select1',
result: constants_1.SUCCESS,
children: [
{
name: 'Bumm',
result: constants_1.FAILURE
},
{
name: 'The Task',
result: constants_1.SUCCESS
}
]
},
{
name: 'select2',
result: constants_1.SUCCESS,
children: [
{
result: constants_1.FAILURE,
children: [
{
name: 'The Task',
result: constants_1.SUCCESS
}
]
},
{
name: 'The Task',
result: constants_1.SUCCESS
}
]
}
]
};
expect(introspector.lastResult).toEqual(result);
expect(introspector.results).toEqual([result]);
});
});
describe('with a Random node', () => {
beforeEach(() => {
blackboard = {
start: 0,
run: 0,
end: 0,
result: constants_1.SUCCESS
};
bTree = new BehaviorTree_1.default({ tree: new Random_1.default({ nodes: ['simpleTask', 'failingTask'] }), blackboard });
});
it('cleans the results', () => {
for (let i = 10; i--;) {
bTree.step({ introspector });
}
expect(introspector.lastResult).not.toEqual(null);
expect(introspector.results.length).toEqual(10);
expect(introspector.lastResult).toEqual(expect.objectContaining({
children: [{ name: expect.any(String), result: expect.any(Boolean) }],
result: expect.any(Boolean)
}));
});
});
describe('.reset method', () => {
it('cleans the results', () => {
bTree = new BehaviorTree_1.default({ tree: 'simpleTask', blackboard: {} });
bTree.step({ introspector });
expect(introspector.lastResult).not.toEqual(null);
expect(introspector.results).not.toEqual([]);
introspector.reset();
expect(introspector.lastResult).toEqual(null);
expect(introspector.results).toEqual([]);
});
});
describe('having a custom introspector module', () => {
class BlackboardChangesIntrospector extends Introspector_1.default {
start(tree) {
this.blackboardSnap = JSON.stringify(tree.blackboard);
super.start(tree);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_toResult(node, result, blackboard) {
const newSnap = JSON.stringify(blackboard);
const blackboardChanged = newSnap !== this.blackboardSnap;
this.blackboardSnap = newSnap;
return {
...(node.name ? { name: node.name } : {}),
result,
blackboardChanged
};
}
}
beforeEach(() => {
introspector = new BlackboardChangesIntrospector();
});
it('also has the blackboard available', () => {
bTree = new BehaviorTree_1.default({ tree: new Sequence_1.default({ nodes: ['simpleTask', 'failingTask'] }), blackboard });
bTree.step({ introspector });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((introspector.lastResult?.children || []).map((x) => x.blackboardChanged)).toEqual([true, false]);
});
});
});