behaviortree
Version:
A JavaScript implementation of Behavior Trees. They are useful for implementing AIs. For Browsers and NodeJS.
463 lines (462 loc) • 17.6 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 Sequence_1 = __importDefault(require("./Sequence"));
const Selector_1 = __importDefault(require("./Selector"));
const Task_1 = __importDefault(require("./Task"));
const InvertDecorator_1 = __importDefault(require("./decorators/InvertDecorator"));
const Decorator_1 = __importDefault(require("./Decorator"));
describe('BehaviorTree', () => {
let bTree;
let blackboard;
describe('with a medium complex tree', () => {
const aTask = new Task_1.default({
run: function (blackboard) {
++blackboard.aCounter;
return constants_1.SUCCESS;
}
});
const bTask = new Task_1.default({
run: function (blackboard) {
++blackboard.bCounter;
return constants_1.FAILURE;
}
});
const switchTask = new Task_1.default({
run: function (blackboard) {
++blackboard.switchCounter;
return blackboard.switchResult;
}
});
const tree = new Sequence_1.default({
nodes: [
aTask,
aTask,
new Selector_1.default({
nodes: [bTask, switchTask, bTask]
}),
aTask
]
});
beforeEach(() => {
blackboard = {
aCounter: 0,
bCounter: 0,
switchCounter: 0,
switchResult: constants_1.RUNNING
};
bTree = new BehaviorTree_1.default({ tree, blackboard });
});
it('controls stepping with running and stuff', () => {
bTree.step();
expect(blackboard.aCounter).toEqual(2);
expect(blackboard.bCounter).toEqual(1);
expect(blackboard.switchCounter).toEqual(1);
bTree.step();
expect(blackboard.aCounter).toEqual(2);
expect(blackboard.bCounter).toEqual(1);
expect(blackboard.switchCounter).toEqual(2);
blackboard.switchResult = constants_1.FAILURE;
bTree.step();
expect(blackboard.aCounter).toEqual(2);
expect(blackboard.bCounter).toEqual(2);
expect(blackboard.switchCounter).toEqual(3);
blackboard.switchResult = constants_1.RUNNING;
bTree.step();
expect(blackboard.aCounter).toEqual(4);
expect(blackboard.bCounter).toEqual(3);
expect(blackboard.switchCounter).toEqual(4);
blackboard.switchResult = constants_1.SUCCESS;
bTree.step();
expect(blackboard.aCounter).toEqual(5);
expect(blackboard.bCounter).toEqual(3);
expect(blackboard.switchCounter).toEqual(5);
bTree.step();
expect(blackboard.aCounter).toEqual(8);
expect(blackboard.bCounter).toEqual(4);
expect(blackboard.switchCounter).toEqual(6);
});
});
describe('with the simplest tree possible', () => {
beforeEach(() => {
blackboard = {
start: 0,
run: 0,
end: 0,
result: constants_1.RUNNING
};
const tree = new Task_1.default({
start: function (blackboard) {
++blackboard.start;
},
run: function (blackboard) {
++blackboard.run;
return blackboard.result;
},
end: function (blackboard) {
++blackboard.end;
}
});
bTree = new BehaviorTree_1.default({ tree, blackboard });
});
it('running does not call start multiple times', () => {
bTree.step();
expect(blackboard.start).toEqual(1);
expect(blackboard.run).toEqual(1);
expect(blackboard.end).toEqual(0);
bTree.step();
expect(blackboard.start).toEqual(1);
expect(blackboard.run).toEqual(2);
expect(blackboard.end).toEqual(0);
blackboard.result = constants_1.FAILURE;
bTree.step();
expect(blackboard.start).toEqual(1);
expect(blackboard.run).toEqual(3);
expect(blackboard.end).toEqual(1);
blackboard.result = constants_1.SUCCESS;
bTree.step();
expect(blackboard.start).toEqual(2);
expect(blackboard.run).toEqual(4);
expect(blackboard.end).toEqual(2);
});
});
describe('with the simplest tree possible', () => {
beforeEach(() => {
blackboard = {
start: 0,
run: 0,
end: 0,
result: constants_1.RUNNING
};
const tree = new Task_1.default({
start: function (blackboard) {
++blackboard.start;
},
run: function (blackboard) {
++blackboard.run;
return blackboard.result;
},
end: function (blackboard) {
++blackboard.end;
}
});
bTree = new BehaviorTree_1.default({ tree, blackboard });
});
it('running does not call start multiple times', () => {
bTree.step();
expect(blackboard.start).toEqual(1);
expect(blackboard.run).toEqual(1);
expect(blackboard.end).toEqual(0);
bTree.step();
expect(blackboard.start).toEqual(1);
expect(blackboard.run).toEqual(2);
expect(blackboard.end).toEqual(0);
blackboard.result = constants_1.FAILURE;
bTree.step();
expect(blackboard.start).toEqual(1);
expect(blackboard.run).toEqual(3);
expect(blackboard.end).toEqual(1);
blackboard.result = constants_1.SUCCESS;
bTree.step();
expect(blackboard.start).toEqual(2);
expect(blackboard.run).toEqual(4);
expect(blackboard.end).toEqual(2);
});
});
describe('registering of tasks', () => {
beforeEach(() => {
blackboard = {
taskA: 0,
taskB: 0,
taskC: 0,
result: constants_1.RUNNING
};
BehaviorTree_1.default.register('taskA', new Task_1.default({
run: function (blackboard) {
++blackboard.taskA;
return constants_1.SUCCESS;
}
}));
BehaviorTree_1.default.register('taskB', new Task_1.default({
run: function (blackboard) {
++blackboard.taskB;
return constants_1.FAILURE;
}
}));
});
it('looks up previously registered tasks', () => {
bTree = new BehaviorTree_1.default({
blackboard,
tree: new Sequence_1.default({
nodes: ['taskA', 'taskB', 'taskA']
})
});
bTree.step();
expect(blackboard.taskA).toEqual(1);
expect(blackboard.taskB).toEqual(1);
});
it('can use function-shortcut to create tasks with only a run method', () => {
BehaviorTree_1.default.register('taskC', () => {
++blackboard.taskC;
return constants_1.FAILURE;
});
bTree = new BehaviorTree_1.default({
blackboard,
tree: new Sequence_1.default({
nodes: ['taskA', 'taskC', 'taskB']
})
});
bTree.step();
expect(blackboard.taskA).toEqual(1);
expect(blackboard.taskB).toEqual(0);
expect(blackboard.taskC).toEqual(1);
});
it('can be erased', () => {
BehaviorTree_1.default.cleanRegistry();
bTree = new BehaviorTree_1.default({
blackboard,
tree: new Selector_1.default({
nodes: ['taskA', 'taskC', 'taskB']
})
});
expect(() => {
bTree.step();
}).toThrowError('No node with name taskA registered.');
});
it('can load tree directly as registered sequence', () => {
BehaviorTree_1.default.register('awesome behavior', new Sequence_1.default({
nodes: ['taskA', 'taskB', 'taskA']
}));
bTree = new BehaviorTree_1.default({
blackboard,
tree: 'awesome behavior'
});
bTree.step();
expect(blackboard.taskA).toEqual(1);
expect(blackboard.taskB).toEqual(1);
});
it('looks up previously registered sequences with sub sequences as well', () => {
BehaviorTree_1.default.register('mySubSequence', new Sequence_1.default({
nodes: ['taskA', 'taskB', 'taskA']
}));
BehaviorTree_1.default.register('mySequence', new Sequence_1.default({
nodes: ['mySubSequence', 'taskB']
}));
bTree = new BehaviorTree_1.default({
blackboard,
tree: new Sequence_1.default({
nodes: ['mySequence']
})
});
bTree.step();
expect(blackboard.taskA).toEqual(1);
expect(blackboard.taskB).toEqual(1);
});
it('looks up previously registered task within a decorators', () => {
bTree = new BehaviorTree_1.default({
blackboard,
tree: new Selector_1.default({
nodes: [new InvertDecorator_1.default({ node: 'taskA' }), 'taskB']
})
});
bTree.step();
expect(blackboard.taskA).toEqual(1);
expect(blackboard.taskB).toEqual(1);
});
});
describe('behavior with running nodes', () => {
it('calls start of all task where appropriate', () => {
const task1 = new Task_1.default({
start: function (blackboard) {
++blackboard.start1;
},
end: function (blackboard) {
++blackboard.end1;
},
run: function (blackboard) {
++blackboard.run1;
return constants_1.SUCCESS;
}
});
const task2 = new Task_1.default({
start: function (blackboard) {
++blackboard.start2;
},
end: function (blackboard) {
++blackboard.end2;
},
run: function (blackboard) {
++blackboard.run2;
return blackboard.task2Result;
}
});
const task3 = new Task_1.default({
start: function (blackboard) {
++blackboard.start3;
},
end: function (blackboard) {
++blackboard.end3;
},
run: function (blackboard) {
++blackboard.run3;
return constants_1.SUCCESS;
}
});
const decoratedTask2 = new Decorator_1.default({
start: function (blackboard) {
++blackboard.startDeco;
},
end: function (blackboard) {
++blackboard.endDeco;
},
node: task2
});
const sequence = new Sequence_1.default({
start: function (blackboard) {
++blackboard.startSeq;
},
end: function (blackboard) {
++blackboard.endSeq;
},
nodes: [task1, decoratedTask2, task3]
});
const blackboard = {
task2Result: constants_1.RUNNING,
start1: 0,
run1: 0,
end1: 0,
start2: 0,
run2: 0,
end2: 0,
start3: 0,
run3: 0,
end3: 0,
startSeq: 0,
endSeq: 0,
startDeco: 0,
endDeco: 0
};
const bTree = new BehaviorTree_1.default({
tree: sequence,
blackboard
});
bTree.step();
expect(blackboard.startSeq).toEqual(1);
expect(blackboard.endSeq).toEqual(0);
expect(blackboard.startDeco).toEqual(1);
expect(blackboard.endDeco).toEqual(0);
expect(blackboard.start1).toEqual(1);
expect(blackboard.run1).toEqual(1);
expect(blackboard.end1).toEqual(1);
expect(blackboard.start2).toEqual(1);
expect(blackboard.run2).toEqual(1);
expect(blackboard.end2).toEqual(0);
expect(blackboard.start3).toEqual(0);
expect(blackboard.run3).toEqual(0);
expect(blackboard.end3).toEqual(0);
bTree.step();
expect(blackboard.startSeq).toEqual(1);
expect(blackboard.endSeq).toEqual(0);
expect(blackboard.startDeco).toEqual(1);
expect(blackboard.endDeco).toEqual(0);
expect(blackboard.start1).toEqual(1);
expect(blackboard.run1).toEqual(1);
expect(blackboard.end1).toEqual(1);
expect(blackboard.start2).toEqual(1);
expect(blackboard.run2).toEqual(2);
expect(blackboard.end2).toEqual(0);
expect(blackboard.start3).toEqual(0);
expect(blackboard.run3).toEqual(0);
expect(blackboard.end3).toEqual(0);
blackboard.task2Result = constants_1.SUCCESS;
bTree.step();
expect(blackboard.startSeq).toEqual(1);
expect(blackboard.endSeq).toEqual(1);
expect(blackboard.startDeco).toEqual(1);
expect(blackboard.endDeco).toEqual(1);
expect(blackboard.start1).toEqual(1);
expect(blackboard.run1).toEqual(1);
expect(blackboard.end1).toEqual(1);
expect(blackboard.start2).toEqual(1);
expect(blackboard.run2).toEqual(3);
expect(blackboard.end2).toEqual(1);
expect(blackboard.start3).toEqual(1);
expect(blackboard.run3).toEqual(1);
expect(blackboard.end3).toEqual(1);
});
});
describe('some curious edge cases', () => {
function createTask(name) {
return new Task_1.default({
name,
start: function (blackboard) {
if (blackboard.result[`${name}start`]) {
++blackboard.result[`${name}start`];
}
else {
blackboard.result[`${name}start`] = 1;
}
},
end: function (blackboard) {
if (blackboard.result[`${name}end`]) {
++blackboard.result[`${name}end`];
}
else {
blackboard.result[`${name}end`] = 1;
}
},
run: function (blackboard) {
if (blackboard.result[`${name}run`]) {
++blackboard.result[`${name}run`];
}
else {
blackboard.result[`${name}run`] = 1;
}
return blackboard.running[name] ? constants_1.RUNNING : constants_1.SUCCESS;
}
});
}
it('start of second sequence is called after first reruns', () => {
const a1 = createTask('a1');
const b1 = createTask('b1');
const aSeq = new Sequence_1.default({
nodes: [a1]
});
const bSeq = new Sequence_1.default({
nodes: [b1]
});
const cSeq = new Sequence_1.default({
nodes: [aSeq, bSeq]
});
const blackboard = {
result: {},
running: {}
};
const bTree = new BehaviorTree_1.default({
tree: cSeq,
blackboard
});
blackboard.running.a1 = true;
bTree.step();
expect(blackboard.result).toEqual({
a1start: 1,
a1run: 1
});
blackboard.running.a1 = false;
bTree.step();
expect(blackboard.result).toEqual({
a1start: 1,
a1run: 2,
a1end: 1,
b1start: 1,
b1run: 1,
b1end: 1
});
});
});
});