Skip to content

Commit

Permalink
sync(BTCPP): ReactiveFallback
Browse files Browse the repository at this point in the history
  • Loading branch information
imere committed Mar 19, 2024
1 parent 2c8823a commit e303c76
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/TreeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import { FallbackNode } from "./controls/FallbackNode";
import { IfThenElseNode } from "./controls/IfThenElseNode";
import { ParallelAllNode } from "./controls/ParallelAllNode";
import { ReactiveFallback } from "./controls/ReactiveFallback";
import { ReactiveSequence } from "./controls/ReactiveSequence";
import { SequenceNode } from "./controls/SequenceNode";
import { createSwitchNode } from "./controls/SwitchNode";
Expand Down Expand Up @@ -98,6 +99,7 @@ export class TreeFactory {

this.registerNodeType(ParallelAllNode, "ParallelAll");
this.registerNodeType(ReactiveSequence, "ReactiveSequence", new PortList());
this.registerNodeType(ReactiveFallback, "ReactiveFallback", new PortList());
this.registerNodeType(IfThenElseNode, "IfThenElse", new PortList());

this.registerNodeType(InverterNode, "Inverter", new PortList());
Expand Down
88 changes: 88 additions & 0 deletions src/controls/ReactiveFallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { ControlNode } from "../ControlNode";
import { NodeStatus, type NodeUserStatus } from "../basic";

/**
* @brief The ReactiveFallback is similar to a ParallelNode.
* All the children are ticked from first to last:
*
* - If a child returns RUNNING, continue to the next sibling.
* - If a child returns FAILURE, continue to the next sibling.
* - If a child returns SUCCESS, stop and return SUCCESS.
*
* If all the children fail, than this node returns FAILURE.
*
* IMPORTANT: to work properly, this node should not have more than
* a single asynchronous child.
*
*/
export class ReactiveFallback extends ControlNode {
private runningChild = -1;

private static throwIfMultipleRunning = false;

private static enableException(enable: boolean): void {
ReactiveFallback.throwIfMultipleRunning = enable;
}

override tick(): NodeUserStatus {
let allSkipped = true;

if (this.status === NodeStatus.IDLE) this.runningChild = -1;

this.setStatus(NodeStatus.RUNNING);

const childrenCount = this.childrenCount();

for (let index = 0; index < childrenCount; index++) {
const currentChildNode = this.children[index];

const childStatus = currentChildNode.executeTick();

// switch to RUNNING state as soon as you find an active child
allSkipped &&= childStatus === NodeStatus.SKIPPED;

switch (childStatus) {
case NodeStatus.RUNNING: {
// reset the previous children, to make sure that they are
// in IDLE state the next time we tick them
for (let i = 0; i < childrenCount; i++) {
if (i !== index) this.haltChild(i);
}
if (this.runningChild === -1) {
this.runningChild = index;
} else if (ReactiveFallback.throwIfMultipleRunning && this.runningChild !== index) {
throw new Error(
"[ReactiveFallback]: only a single child can return RUNNING. This throw can be disabled with ReactiveFallback::enableException(false)"
);
}
return NodeStatus.RUNNING;
}
case NodeStatus.FAILURE: {
break;
}
case NodeStatus.SUCCESS: {
this.resetChildren();
return NodeStatus.SUCCESS;
}
case NodeStatus.SKIPPED: {
// to allow it to be skipped again, we must reset the node
this.haltChild(index);
break;
}
case NodeStatus.IDLE: {
throw new Error(`ReactiveFallback(${this.name}): A children should not return IDLE"`);
}
}
}

this.resetChildren();

// Skip if ALL the nodes have been skipped
return allSkipped ? NodeStatus.SKIPPED : NodeStatus.FAILURE;
}

override halt(): void {
this.runningChild = -1;
super.halt();
}
}
61 changes: 61 additions & 0 deletions src/fallback.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NodeConfig } from "./TreeNode";
import { NodeStatus } from "./basic";
import { FallbackNode } from "./controls/FallbackNode";
import { ReactiveFallback } from "./controls/ReactiveFallback";
import { AsyncActionTest } from "./testing/ActionTestNode";
import { ConditionTestNode } from "./testing/ConditionTestNode";

Expand Down Expand Up @@ -47,6 +48,66 @@ describe("SimpleFallbackTest", () => {
});
});

describe("ReactiveFallbackTest", () => {
let root: ReactiveFallback;
let condition_1: ConditionTestNode;
let condition_2: ConditionTestNode;
let action_1: AsyncActionTest;

beforeEach(() => {
root = new ReactiveFallback("root_fallback", new NodeConfig());
condition_1 = new ConditionTestNode("condition_1");
condition_2 = new ConditionTestNode("condition_2");
action_1 = new AsyncActionTest("action_1", new NodeConfig(), 100);

root.addChild(condition_1);
root.addChild(condition_2);
root.addChild(action_1);
});

test("Condition1ToTrue", () => {
condition_1.setExpectedResult(NodeStatus.FAILURE);
condition_2.setExpectedResult(NodeStatus.FAILURE);

let state = root.executeTick();

expect(state).toBe(NodeStatus.RUNNING);
expect(condition_1.status).toBe(NodeStatus.IDLE);
expect(condition_2.status).toBe(NodeStatus.IDLE);
expect(action_1.status).toBe(NodeStatus.RUNNING);

condition_1.setExpectedResult(NodeStatus.SUCCESS);

state = root.executeTick();

expect(state).toBe(NodeStatus.SUCCESS);
expect(condition_1.status).toBe(NodeStatus.IDLE);
expect(condition_2.status).toBe(NodeStatus.IDLE);
expect(action_1.status).toBe(NodeStatus.IDLE);
});

test("Condition2ToTrue", () => {
condition_1.setExpectedResult(NodeStatus.FAILURE);
condition_2.setExpectedResult(NodeStatus.FAILURE);

let state = root.executeTick();

expect(state).toBe(NodeStatus.RUNNING);
expect(condition_1.status).toBe(NodeStatus.IDLE);
expect(condition_2.status).toBe(NodeStatus.IDLE);
expect(action_1.status).toBe(NodeStatus.RUNNING);

condition_2.setExpectedResult(NodeStatus.SUCCESS);

state = root.executeTick();

expect(state).toBe(NodeStatus.SUCCESS);
expect(condition_1.status).toBe(NodeStatus.IDLE);
expect(condition_2.status).toBe(NodeStatus.IDLE);
expect(action_1.status).toBe(NodeStatus.IDLE);
});
});

describe("SimpleFallbackWithMemoryTest", () => {
let root: FallbackNode;
let action: AsyncActionTest;
Expand Down

0 comments on commit e303c76

Please sign in to comment.