BehaviorTree.js icon indicating copy to clipboard operation
BehaviorTree.js copied to clipboard

LoopDecorator ignores RUNNING result

Open markpol opened this issue 1 year ago • 4 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).

markpol avatar Mar 07 '23 16:03 markpol

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?

Calamari avatar Mar 07 '23 17:03 Calamari

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.

markpol avatar Mar 07 '23 18:03 markpol

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!

markpol avatar Mar 07 '23 23:03 markpol

Nice, thanks for sharing. It is an interesting use case. Maybe useful for people to know about.

Calamari avatar Mar 09 '23 10:03 Calamari