Calamari / BehaviorTree.js

An JavaScript implementation of Behavior Trees.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.