// The following code contains the implementation of the behavior trees and
// unit tests for it.
// https://hexdocs.pm/behavior_tree/BehaviorTree.html#content
// https://www.gamasutra.com/blogs/ChrisSimpson/20140717/221339/Behavior_trees_for_AI_How_they_work.php
// Possible states of a task
const State =
{
Undefined: "Undefined",
Failure: "Failure",
Success: "Success",
Running: "Running"
}
// A simple class to log the execution of the tasks in a string variable.
class Log {
constructor() {
this.text = '';
}
reset() {
this.text = '';
}
append(text) {
this.text += text + '\n';
}
toString() {
return this.text;
}
}
// Base class for all tasks. It has an id and a state. The run method is used
// to execute the task and log the execution if a log object is provided.
class Task {
constructor(id, state = State.Undefined) {
this.id = id;
this.state = state;
}
run(log = null, level = '') {
if (log) log.append(level + 'Task(' + this.id + ')');
if (log) log.append(level + '└ ' + this.state);
return this;
}
}
// Check the state of a task by comparing it with a given state. It returns
// success if the states match, and failure otherwise.
class CheckState extends Task {
constructor(checktask = null, checkstate = State.Success) {
super('CheckState');
this.checktask = checktask;
this.checkstate = checkstate;
}
run(log = null, level = '') {
if (log) log.append(level + 'CheckState(' + this.checktask.id + ',' + this.checkstate + ')');
this.state = (this.checktask.state == this.checkstate) ? State.Success : State.Failure;
if (log) log.append(level + '└ ' + this.state);
return this;
}
}
// Selector composite. It returns immediately with a success status code when one
// of its children runs successfully.
class Selector extends Task {
constructor(tasks = []) {
super('Selector');
this.tasks = tasks;
}
run(log = null, level = '') {
if (log) log.append(level + this.id + '()');
this.state = State.Failure;
for (let task of this.tasks) {
this.state = task.run(log, level + '│ ').state;
if (this.state != State.Failure)
break;
}
if (log) log.append(level + '└ ' + this.state);
return this;
}
}
// Sequence composite. It returns immediately with a failure status code when
// one of its children fails.
class Sequence extends Task {
constructor(tasks = []) {
super('Sequence');
this.tasks = tasks;
}
run(log = null, level = '') {
if (log) log.append(level + this.id + '()');
this.state = State.Success;
for (let task of this.tasks) {
this.state = task.run(log, level + '│ ').state;
if (this.state != State.Success)
break;
}
if (log) log.append(level + '└ ' + this.state);
return this;
}
}
// Random selector composite. It tries a random child and returns its state.
class RandomSelector extends Task {
constructor(tasks = []) {
super('RandomSelector');
this.tasks = tasks;
}
run(log = null, level = '') {
if (log) log.append(level + this.id + '()');
this.state = State.Failure;
if (this.tasks.length) {
let i = Math.floor(Math.random() * this.tasks.length);
this.state = this.tasks[i].run(log, level + '│ ').state;
}
if (log) log.append(level + '└ ' + this.state);
return this;
}
}
// The following classes are Decorators that modify the behavior of a single
// child task.
// Inverter decorator. It inverts the value returned by a task when the value
// is not "Running".
class Inverter extends Task {
constructor(task = null) {
super('Inverter');
this.task = task;
}
run(log = null, level = '') {
if (log) log.append(level + this.id + '()');
if (this.task) {
this.task.run(log, level + '│ ');
switch (this.task.state) {
case State.Running:
this.state = State.Running;
break;
case State.Failure:
this.state = State.Success;
break;
case State.Success:
this.state = State.Failure;
break;
default:
this.state = State.Undefined;
}
}
if (log) log.append(level + '└ ' + this.state);
return this;
}
}
// Succeeder decorator. It will always return success, irrespective of what the
// child node actually returned.
class Succeeder extends Task {
constructor(task = null) {
super('Succeeder');
this.task = task;
}
run(log = null, level = '') {
if (log) log.append(level + this.id + '()');
this.state = State.Success;
if (this.task)
this.task.run(log, level + '│ ');
if (log) log.append(level + '└ ' + this.state);
return this;
}
}
// Following classes repeatedly execute their child tasks until a certain
// condition is met.
// Repeater. A repeater will reprocess its child node each time its child
// returns a result. These are often used at the very base of the tree, to make
// the tree to run continuously. Repeaters may optionally run their children a
// set number of times before returning to their parent.
class Repeater extends Task {
constructor(task = null, counter = 0) {
super('Repeater');
this.task = task;
this.counter = counter;
}
run(log = null, level = '') {
if (log) log.append(level + this.id + '(' + this.counter + ')');
this.state = State.Success;
if (this.task)
{
let remaining = this.counter;
while ((remaining--) > 0)
this.task.run(log, level + '│ ');
}
if (log) log.append(level + '└ ' + this.state);
return this;
}
}
// Repeat_until_fail. Like a repeater, these decorators will continue to
// reprocess their child. That is until the child finally returns a failure,
// at which point the repeater will return success to its parent.
class Repeat_until_fail extends Task {
constructor(task = null) {
super('Repeat_until_fail');
this.task = task;
}
run(log = null, level = '') {
if (log) log.append(level + this.id + '()');
this.state = State.Success;
if (this.task)
while (this.task.run(log, level + '│ ').state == State.Success) { }
if (log) log.append(level + '└ ' + this.state);
return this;
}
}