LoopDecorator ignores RUNNING result
markpol opened this issue · comments
Hello,
I'm using the LoopDecorator as a way to dynamically add nodes to my trees. I queue up some actions by adding them to an array in the blackboard and then use a looped sequence to shift an action off of the queue and process it in a task. This continues until the action queue is empty at which point a FAILURE result is returned, breaking out of the Loop and continuing with the rest of the tree.
This pattern has been working fine so far but now i have some cases where I need to interrupt the Loop and then have it continue where it left off on a later step. Unfortunately, if i return a RUNNING status in one of the loop iterations it just continues to the next one.
Does it make sense to extend the LoopDecorator with a custom one that allows it to be stopped and resumed or is there perhaps a better way to handle the scenario I described above?
Thanks for a great library by the way! I'm currently using the typescript version (3.0.0-beta.1).
Hello @markpol.
Glad you like it. And also good to here that someone is trying out the typescript version. I think I should put it down as official release soon, since I haven't heared any typescript related problems yet. And it is pretty old by now :-D
But to your question/use case: That is an interesting one. It definitely sounds that you are misusing the sequence node for something it ain’t meant to do since it does not have a sequence of nodes to care for – since it is only dynamically done in the blackboards –, right?.
As far as I understand your problem, you need a new kind of node, that runs through the list in the blackboard, and can therefore remember where it left off.
Does your sequence have any real children?
Hi @Calamari,
Thanks for the quick reply. Here is some simplified code that perhaps better describes what I'm trying to do:
import BehaviorTree, { FAILURE, Introspector, RUNNING, Sequence, SUCCESS, Task } from "behaviortree"
import { LoopDecorator } from "behaviortree/lib/decorators"
const actionQueue = [() => 1, () => 2, () => 3, () => 4, () => 5]
const testActionsTree = new BehaviorTree({
tree: new Sequence({
nodes: [
new Task({
name: "populateQueue",
run: (b) => {
b.queuedActions = [...actionQueue]
console.log("Queued items:" + b.queuedActions.length)
return SUCCESS
},
}),
new LoopDecorator({
node: new Sequence({
nodes: [
new Task({
name: "shiftItem",
run: (b) => {
b.currentAction = b.queuedActions.shift()
if (b.currentAction) {
console.log("Picked item:", actionQueue.length - b.queuedActions.length)
return SUCCESS
}
return FAILURE
},
}),
new Task({
name: "processItem",
run: (b) => {
const actionResult = b.currentAction()
console.log("Executed item:", actionResult)
return actionResult === 3 ? RUNNING : SUCCESS
},
}),
],
}),
}),
],
}),
blackboard: {
queuedActions: [],
currentAction: undefined,
},
})
const introspector = new Introspector()
testActionsTree.step({ introspector })
console.log("lastresult: ", JSON.stringify(introspector.lastResult, null, 2))
testActionsTree.step({ introspector })
and the output:
npx ts-node --project tsconfig.test.json test-tree.ts ─╯
Queued items: 5
Picked item: 1
Executed item: 1
Picked item: 2
Executed item: 2
Picked item: 3
Executed item: 3
Picked item: 4
Executed item: 4
Picked item: 5
Executed item: 5
lastresult: {
"result": false,
"children": [
{
"name": "populateQueue",
"result": true
},
{
"result": false,
"children": [
{
"result": true,
"children": [
{
"name": "shiftItem",
"result": true
},
{
"name": "processItem",
"result": true
}
]
},
{
"result": true,
"children": [
{
"name": "shiftItem",
"result": true
},
{
"name": "processItem",
"result": true
}
]
},
{
"children": [
{
"name": "shiftItem",
"result": true
},
{
"name": "processItem"
}
]
},
{
"result": true,
"children": [
{
"name": "shiftItem",
"result": true
},
{
"name": "processItem",
"result": true
}
]
},
{
"result": true,
"children": [
{
"name": "shiftItem",
"result": true
},
{
"name": "processItem",
"result": true
}
]
},
{
"result": false,
"children": [
{
"name": "shiftItem",
"result": false
}
]
}
]
}
]
}
Queued items: 5
Picked item: 1
Executed item: 1
Picked item: 2
Executed item: 2
Picked item: 3
Executed item: 3
Picked item: 4
Executed item: 4
Picked item: 5
Executed item: 5
So ideally, i would want the tree to stop at action 3 where RUNNING is returned in the processItem task. Then be able to continue afterwards with actions 4 and 5 by calling step() on the tree again. I hope this makes more sense.
I might be able to get the desired result by introducing more state in the blackboard but wasn't sure if there's a better approach.
I solved this by extending the LoopDecorator to return early on RUNNING results in addition to FAILURE as well as by only having it wrap a single Task rather than the Sequence that I used above.
import BehaviorTree, {
FAILURE,
Introspector,
RunCallback,
RUNNING,
Sequence,
StatusWithState,
SUCCESS,
Task,
} from "behaviortree"
import { LoopDecorator } from "behaviortree/lib/decorators"
import { RunResult } from "behaviortree/lib/types"
class InterruptableLoopDecorator extends LoopDecorator {
nodeType = "InterruptableLoopDecorator"
decorate(run: RunCallback) {
let i = 0
let result: RunResult = FAILURE
while (i++ < this.config.loop) {
result = run()
if (result === FAILURE) return FAILURE
if (result === RUNNING) return RUNNING
}
return result
}
}
const actionQueue = [() => 1, () => 2, () => 3, () => 4, () => 5]
const testActionsTree = new BehaviorTree({
tree: new Sequence({
nodes: [
new Task({
name: "populateQueue",
run: (b) => {
b.queuedActions = [...actionQueue]
console.log("Queued items:" + b.queuedActions.length)
return SUCCESS
},
}),
new InterruptableLoopDecorator({
node: new Task({
name: "executeItem",
run: (b) => {
b.currentAction = b.queuedActions.shift()
if (b.currentAction) {
const actionResult = b.currentAction()
console.log("Executed item:", actionResult)
if (actionResult === 3) {
return RUNNING
}
return SUCCESS
}
return FAILURE
},
}),
}),
],
}),
blackboard: {
queuedActions: [],
currentAction: undefined,
},
})
const introspector = new Introspector()
testActionsTree.step({ introspector })
console.log("lastresult 1: ", JSON.stringify(introspector.lastResult, null, 2))
if ((testActionsTree.lastResult as StatusWithState)?.total === RUNNING) {
testActionsTree.step({ introspector })
console.log("lastresult 2: ", JSON.stringify(introspector.lastResult, null, 2))
}
which now outputs:
npx ts-node --project tsconfig.test.json test-tree.ts
Queued items:5
Executed item: 1
Executed item: 2
Executed item: 3
lastresult 1: {
"children": [
{
"name": "populateQueue",
"result": true
},
{
"children": [
{
"name": "executeItem",
"result": true
},
{
"name": "executeItem",
"result": true
},
{
"name": "executeItem"
}
]
}
]
}
Executed item: 4
Executed item: 5
lastresult 2: {
"result": false,
"children": [
{
"result": false,
"children": [
{
"name": "executeItem",
"result": true
},
{
"name": "executeItem",
"result": true
},
{
"name": "executeItem",
"result": false
}
]
}
]
}
Thanks again!
Nice, thanks for sharing. It is an interesting use case. Maybe useful for people to know about.