Skip to content

Commit

Permalink
Features: Any, All, Background
Browse files Browse the repository at this point in the history
Adds the new stateless group tick Any, the stateful group tick
wrapper All, and the stateful tick wrapper Background. The theme
of this release is backgroundable long running tasks, and an
example has been added which demonstrates a background job
mechanism, utilising Sync and Async.
  • Loading branch information
joeycumines committed Aug 26, 2019
1 parent 3b9e827 commit 5d99fb7
Show file tree
Hide file tree
Showing 8 changed files with 1,104 additions and 3 deletions.
159 changes: 156 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@ func (n Node) Tick() (Status, error)
- Core implementation as above
- Sequence and Selector also provided as per the
[Wikipedia page](https://en.wikipedia.org/wiki/Behavior_tree_(artificial_intelligence,_robotics_and_control))
- All, a stateless tick similar to Sequence which will attempt to run all nodes, even on (non-error) failure
- Any, a stateful tick which uses encapsulation to patch a group tick's return status to be success if any children
succeeded, or failure if all children failed
- Async and Sync wrappers allow for the definition of time consuming logic that gets performed in serial, but without
blocking the tick operation, and can be used to implement complex behavior such as conditional exit of running nodes
- Fork provides a group tick like Sequence and Selector, that runs it's children concurrently, and to completion
- Background, a stateful tick that supports multiple concurrent executions of _either_ static or dynamically generated
instances of another tick implementation, where all statuses are propagated 1-1, but not necessarily in order,
utilising the running status as it's cue to background a newly generated tick (supporting conditional backgrounding)
- RateLimit is a common building block, and implements a Tick that will return Failure as necessary to maintain a rate
- Not simply inverts a Tick's Status, but retains error handling (Failure on error value) behavior
- Implementations to actually run behavior trees are provided, and include a Ticker and Manager, but both are defined
Expand All @@ -44,9 +50,7 @@ will ever see a V2, but quite likely that I will be supporting V1 for years.

## Example Usage

The example below is straight from `example_test.go`.

TODO: more complicated example using `Async` and `Sync`
The examples below are straight from `example_test.go`.

```go
// ExampleNewTickerStopOnFailure_counter demonstrates the use of NewTickerStopOnFailure to implement more complex "run
Expand Down Expand Up @@ -135,4 +139,153 @@ func ExampleNewTickerStopOnFailure_counter() {
// < 20: 19
// < 20: 20
}

// ExampleBackground_asyncJobQueue implements a basic example of backgrounding of long-running tasks that may be
// performed concurrently, see ExampleNewTickerStopOnFailure_counter for an explanation of the ticker
func ExampleBackground_asyncJobQueue() {
type (
Job struct {
Name string
Duration time.Duration
Done chan struct{}
}
)
var (
// doWorker performs the actual "work" for a Job
doWorker = func(job Job) {
fmt.Printf("[worker] job \"%s\" STARTED\n", job.Name)
time.Sleep(job.Duration)
fmt.Printf("[worker] job \"%s\" FINISHED\n", job.Name)
close(job.Done)
}
// queue be sent jobs, which will be received within the ticker
queue = make(chan Job, 50)
// doClient sends and waits for a job
doClient = func(name string, duration time.Duration) {
job := Job{name, duration, make(chan struct{})}
ts := time.Now()
fmt.Printf("[client] job \"%s\" STARTED\n", job.Name)
queue <- job
<-job.Done
fmt.Printf("[client] job \"%s\" FINISHED\n", job.Name)
t := time.Now().Sub(ts)
d := t - job.Duration
if d < 0 {
d *= -1
}
if d > time.Millisecond*50 {
panic(fmt.Errorf(`job "%s" expected %s actual %s`, job.Name, job.Duration.String(), t.String()))
}
}
// running keeps track of the number of running jobs
running = func() func(delta int64) int64 {
var (
value int64
mutex sync.Mutex
)
return func(delta int64) int64 {
mutex.Lock()
defer mutex.Unlock()
value += delta
return value
}
}()
// done will be closed when it's time to exit the ticker
done = make(chan struct{})
ticker = NewTickerStopOnFailure(
context.Background(),
time.Millisecond,
New(
Sequence,
New(func(children []Node) (Status, error) {
select {
case <-done:
return Failure, nil
default:
return Success, nil
}
}),
func() Node {
// the tick is initialised once, and is stateful (though the tick it's wrapping isn't)
tick := Background(func() Tick { return Selector })
return func() (Tick, []Node) {
// this block will be refreshed each time that a new job is started
var (
job Job
)
return tick, []Node{
New(
Sequence,
Sync([]Node{
New(func(children []Node) (Status, error) {
select {
case job = <-queue:
running(1)
return Success, nil
default:
return Failure, nil
}
}),
New(Async(func(children []Node) (Status, error) {
defer running(-1)
doWorker(job)
return Success, nil
})),
})...,
),
// no job available - success
New(func(children []Node) (Status, error) {
return Success, nil
}),
}
}
}(),
),
)
wg sync.WaitGroup
)
wg.Add(1)
run := func(name string, duration time.Duration) {
wg.Add(1)
defer wg.Done()
doClient(name, duration)
}

fmt.Printf("running jobs: %d\n", running(0))

go run(`1. 120ms`, time.Millisecond*120)
time.Sleep(time.Millisecond * 25)
go run(`2. 70ms`, time.Millisecond*70)
time.Sleep(time.Millisecond * 25)
fmt.Printf("running jobs: %d\n", running(0))

doClient(`3. 150ms`, time.Millisecond*150)
time.Sleep(time.Millisecond * 50)
fmt.Printf("running jobs: %d\n", running(0))

time.Sleep(time.Millisecond * 50)
wg.Done()
wg.Wait()
close(done)
<-ticker.Done()
if err := ticker.Err(); err != nil {
panic(err)
}
//output:
//running jobs: 0
//[client] job "1. 120ms" STARTED
//[worker] job "1. 120ms" STARTED
//[client] job "2. 70ms" STARTED
//[worker] job "2. 70ms" STARTED
//running jobs: 2
//[client] job "3. 150ms" STARTED
//[worker] job "3. 150ms" STARTED
//[worker] job "2. 70ms" FINISHED
//[client] job "2. 70ms" FINISHED
//[worker] job "1. 120ms" FINISHED
//[client] job "1. 120ms" FINISHED
//[worker] job "3. 150ms" FINISHED
//[client] job "3. 150ms" FINISHED
//running jobs: 0
}
```
40 changes: 40 additions & 0 deletions all.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
Copyright 2019 Joseph Cumines
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package behaviortree

// All implements a tick which will tick all children sequentially until the first running status or error is
// encountered (propagated), and will return success only if all children were ticked and returned success (returns
// success if there were no children, like sequence).
func All(children []Node) (Status, error) {
success := true
for _, child := range children {
status, err := child.Tick()
if err != nil {
return Failure, err
}
if status == Running {
return Running, nil
}
if status != Success {
success = false
}
}
if !success {
return Failure, nil
}
return Success, nil
}
35 changes: 35 additions & 0 deletions all_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
Copyright 2019 Joseph Cumines
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package behaviortree

import (
"errors"
"testing"
)

func TestAll_error(t *testing.T) {
e := errors.New(`some_error`)
status, err := New(All, New(func(children []Node) (Status, error) {
return Success, e
})).Tick()
if status != Failure {
t.Error(status)
}
if err != e {
t.Error(err)
}
}
77 changes: 77 additions & 0 deletions any.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
Copyright 2019 Joseph Cumines
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package behaviortree

import "sync"

// Any wraps a tick such that non-error non-running statuses will be overridden with a success if at least one child
// succeeded - which is achieved by encapsulation of children, before passing them into the wrapped tick. Nil will be
// returned if tick is nil, and nil children will be passed through as such.
func Any(tick Tick) Tick {
if tick == nil {
return nil
}
var (
mutex sync.Mutex
success bool
)
return func(children []Node) (Status, error) {
children = func(src []Node) (dst []Node) {
if src == nil {
return
}
dst = make([]Node, len(src))
copy(dst, src)
return
}(children)
for i := range children {
child := children[i]
if child == nil {
continue
}
children[i] = func() (Tick, []Node) {
tick, nodes := child()
if tick == nil {
return nil, nodes
}
return func(children []Node) (Status, error) {
status, err := tick(children)
if err == nil && status == Success {
mutex.Lock()
success = true
mutex.Unlock()
}
return status, err
}, nodes
}
}
status, err := tick(children)
if err != nil {
return Failure, err
}
if status == Running {
return Running, nil
}
mutex.Lock()
defer mutex.Unlock()
if !success {
return Failure, nil
}
success = false
return Success, nil
}
}
Loading

0 comments on commit 5d99fb7

Please sign in to comment.