Calamari / BehaviorTree.js

An JavaScript implementation of Behavior Trees.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

The first task of a nested sequence does not seem to inoke start

EnricoPietrocola opened this issue · comments

I have three sequences: A, B and C
A has two tasks
B has two tasks
C is A,B

When I run C in my behavior tree, the first task of B doesn't call start (but calls Run), the rest of the tasks work fine.
It even happens if C is A,A or B,B

I read that the latest release 2.0.4 fixed this issue but I am still encountering it

Hey @EnricoPietrocola. Thanks for reaching out. Can you provide an example or a test case where that happens?

I tried reproducing it, but it seems to work as expected, that is the test example I build after reading your description:

  describe('test Enricos example', () => {
    function createTask(name) {
      return new Task({
        start: function (blackboard) {
          if (blackboard[`${name}start`]) {
            ++blackboard[`${name}start`]
          } else {
            blackboard[`${name}start`] = 1
          }
        },
        end: function (blackboard) {
          if (blackboard[`${name}end`]) {
            ++blackboard[`${name}end`]
          } else {
            blackboard[`${name}end`] = 1
          }
        },
        run: function (blackboard) {
          if (blackboard[`${name}run`]) {
            ++blackboard[`${name}run`]
          } else {
            blackboard[`${name}run`] = 1
          }
          return SUCCESS
        }
      })
    }

    it('calls start of all task where appropriate', () => {
      const a1 = createTask('a1')
      const a2 = createTask('a2')
      const b1 = createTask('b1')
      const b2 = createTask('b2')

      const aSeq = new Sequence({
        nodes: [a1, a2]
      })
      const bSeq = new Sequence({
        nodes: [b1, b2]
      })
      const cSeq = new Sequence({
        nodes: [aSeq, bSeq]
      })
      const blackboard = {}

      const bTree = new BehaviorTree({
        tree: cSeq,
        blackboard
      })

      bTree.step()

      expect(blackboard).toEqual({
        a1start: 1,
        a1run: 1,
        a1end: 1,
        a2start: 1,
        a2run: 1,
        a2end: 1,
        b1start: 1,
        b1run: 1,
        b1end: 1,
        b2start: 1,
        b2run: 1,
        b2end: 1
      })
    })
  })

And it is green (all starts, runs and ends have been called).

Hey! Thank you for helping me :)

Running mainSequence I would expect to have this output

BEGIN
NOTE 60
NOTE 67
END
BEGIN
NOTE 67
NOTE 67
END

What I am getting is this:

BEGIN
NOTE 60
NOTE 67
END
NOTE 67
NOTE 67
END

The second BEGIN is not printing, therefore I think the start function is note invoked, but RUN does since the "NOTE" lines are done in Run instead of start.

const { BehaviorTree, Sequence, Task, Selector, SUCCESS, FAILURE, RUNNING } = require('behaviortree')

const CTask = new Task({
    start: function (blackboard) {
        blackboard.isStarted = true;

    },
    end: function (blackboard) {
        blackboard.isStarted = false;
        blackboard.deltaTime = 0;
    },
    run: function (blackboard) {
        if(blackboard.deltaTime === 0) {
            console.log("NOTE 60")
        }

        if(blackboard.deltaTime <= 50){
            blackboard.deltaTime++
            return RUNNING
        }
        else{
            return SUCCESS
        }
    }
})

const GTask = new Task({
    start: function (blackboard) {
        blackboard.isStarted = true;
    },
    end: function (blackboard) {
        blackboard.isStarted = false;
        blackboard.deltaTime = 0;
    },
    run: function (blackboard) {
        //console.log(blackboard.deltaTime)
        if(blackboard.deltaTime === 0){
            console.log("NOTE 67")
        }
        if(blackboard.deltaTime <= 50){
            blackboard.deltaTime++
            return RUNNING
        }
        else{
            return SUCCESS
        }
    }
})

const BeginTask = new Task({
    start: function (blackboard) {
        blackboard.isStarted = true;
        console.log("BEGIN")
    },
    end: function (blackboard) {
        blackboard.isStarted = false;
    },
    run: function (blackboard) {
        return SUCCESS
    }
})

const EndTask = new Task({
    start: function (blackboard) {
        blackboard.isStarted = true;
        console.log("END")
    },
    end: function (blackboard) {
        blackboard.isStarted = false;
    },
    run: function (blackboard) {
        return SUCCESS
    }
})

let deltaTime = {
    deltaTime: 0
};

const seq1 = new Sequence({
    nodes: [BeginTask, CTask, GTask, EndTask
    ]
})

const seq2 = new Sequence({
    nodes: [BeginTask, GTask, GTask, EndTask
    ]
})

const mainSequence = new Sequence({
    nodes: [seq1, seq2
    ]
})

let bTree = new BehaviorTree({
    tree: mainSequence,
    blackboard: deltaTime
});

setInterval(function() {
    bTree.step()
}, 10)

A little update, I have been playing a bit with source code (mainly to learn webpack honestly, your code is awesome material to study)

My bt seems to work as expected when removing if(!rerun) check of line 13 in the Node.js src file.
I wouldn't say I think I fixed the issue as I still don't really have proper understanding of what that line is doing, but I figured I could notify this as a possible help

P.s. Clearly it's not a solution, after checking I realized that removing the rerun check runs start for each evaluation as long as the previous returned RUNNING. I would think that rerun is mistakingly set to true at the end of a sequence, therefore the following sequence will skip its start call, I'll investigate more

After more testing with my code I have found more:

start() does not run at all on the step() of first task of the second sequence (seq2) when using the BT like the following:
let bTree = new BehaviorTree({ tree: mainSequence, blackboard: deltaTime });

in fact, creating the same sequence in the tree like this works as expected:

let bTree = new BehaviorTree({
    tree: seq1, seq2
    blackboard: deltaTime
});

In my opinion, the issue might have to do with this line in BehaviorTree.js

const rerun = this.lastResult === RUNNING || indexes.length > 0

It seems to be true when the first node of the second nested sequence is executed, when it should be false

I got deeper in the code and it now seems to be working by moving the rerun = false line out of the else statement in BranchNode.js on line 33. This is how my branchnode looks like, I'm not sure everything work, I am not accustomed with testing, but my codes outputs as expected for the moment. I still have to try if selectors and more complex sequences would work with this change

import { RUNNING } from './constants'
import Node from './Node'

export default class BranchNode extends Node {
  nodeType = 'BranchNode'

  constructor (blueprint) {
    super(blueprint)

    this.numNodes = blueprint.nodes.length
    this.wasRunning = false
  }

  run (blackboard = null, { indexes = [], rerun, runData, registryLookUp = x => x } = {}) {
    const subRunData = runData ? [] : null
    this.blueprint.start(blackboard)
    let overallResult = this.START_CASE
    let currentIndex = indexes.shift() || 0
    while (currentIndex < this.numNodes) {
      const node = registryLookUp(this.blueprint.nodes[currentIndex])
      const result = node.run(blackboard, { indexes, rerun, runData: subRunData, registryLookUp })
      if (result === RUNNING) {
        this.wasRunning = true
        return [currentIndex, ...indexes]
      } else if (typeof result === 'object') { // array
        return [...indexes, currentIndex, ...result]
      } else if (result === this.OPT_OUT_CASE) {
        overallResult = result
        break
      } else {
        if (this.wasRunning) {
          this.wasRunning = false
        }
        rerun = false
        ++currentIndex
      }
    }
    this.blueprint.end(blackboard)
    if (runData) {
      ++currentIndex
      // collect data of unfinished nodes
      while (currentIndex < this.numNodes) {
        subRunData.push(registryLookUp(this.blueprint.nodes[currentIndex]).collectData())
        ++currentIndex
      }
      runData.push({
        name: this.name,
        type: this.nodeType,
        nodes: subRunData,
        result: overallResult
      })
    }
    return overallResult
  }

  get collectData () {
    return {
      name: this.name,
      type: this.nodeType,
      nodes: this.nodes.map(node => node.collectData())
    }
  }
}

First. Thanks for pointing out that there is flaw somewhere, and taking the time to investigate. I also just spent some time on it and I think you might be right, or not far away from the culprit.
Btw. you can see that when you just run npm run test within the repo, that should run all the tests and tell you, if you did break anything.

But that is indeed a nice catch, and a curious case. I will create a new test for it.

And while invetigating as well I also found some strange irregularities (I cannot really pinpoint yet) with the debug: flag. I'll have to figure that one out two. But that's second prio.

I didn't really remove it, I just moved it outside to run every time the if clause gets in there, but I totally see what you are talking about.
Out of curiosity I tried the test and I get all green, anyways, I'll wait for your fix, thank you very much for finding the time to help me.

May I ask how does the this.lastResult === RUNNING code works? It's still a bit cryptic to me, I understand that RUNNING is a symbol and I was curious to understand how it could be compared with an array like lastResult (I don't even really know if lastResult is an array at this point, it's been a long day :) )

So @EnricoPietrocola please check out #42, that should be the fix. And indeed, your fix worked perfectly. (It's the first commit, the second one I did is just running prettier over everything, to make the code style consistent.)

About the this.lastRunning stuff: You know the feeling that if you look at your own code from years ago? :-)
The result RUNNING is the response of any node to tell the parent node, that it is not finished yet, and has to be invoked the within the next step of the tree again. I guess so far it is clear. But in case of a branching node (Sequences and Selectors and there like) the tree also needs a mechanism to determine, which of the nodes was still running to start running on that node. I don't know why I thought it was a good idea to reuse the response for that. 🤷
The next thing I will do is to rewrite that part of code, so it will get more clear, since I have to admit it took myself quite some time to reunderstand why I did that. Also, it affects the instrumentation, I want to fix that as well.

Thanks for the commits and explanation, all clear! Should I close the issue or wait for a merge?

It should auto-close when I merge.

Yes, it worked :-)

Thanks again for the analysis and the fix.

I also released version 2.0.5 containing that fix