├── go.mod ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── codeql-analysis.yml │ └── go.yml ├── CONTRIBUTING.md ├── all_test.go ├── ratelimit.go ├── selector.go ├── sequence.go ├── not.go ├── all.go ├── ratelimit_test.go ├── shuffle.go ├── go.sum ├── async.go ├── switch.go ├── not_test.go ├── background.go ├── any.go ├── fork.go ├── sync.go ├── memorize.go ├── frame.go ├── sync_test.go ├── context_test.go ├── shuffle_test.go ├── behaviortree.go ├── memorize_test.go ├── frame_test.go ├── async_test.go ├── context.go ├── value.go ├── ticker.go ├── manager.go ├── CODE_OF_CONDUCT.md ├── selector_test.go ├── behaviortree_test.go ├── printer.go ├── sequence_test.go ├── any_test.go ├── fork_test.go ├── background_test.go ├── value_test.go ├── ticker_test.go ├── switch_test.go ├── printer_test.go ├── manager_test.go ├── LICENSE ├── example_test.go └── README.md /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/joeycumines/go-behaviortree 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/go-test/deep v1.1.0 7 | github.com/joeycumines/go-bigbuff v1.15.1 8 | github.com/xlab/treeprint v1.2.0 9 | ) 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | version: 2 3 | updates: 4 | - package-ecosystem: gomod 5 | directory: / 6 | schedule: 7 | interval: daily 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Contributions are welcome! 4 | 5 | In the interest of preserving your enthusiasm (for future contributions), please open an issue, prior to creating a pull request, to discuss the feature / bug / change. 6 | 7 | If you have a use case / feature request outside of the scope of this package, you are still quite welcome to open an issue, for discussion purposes. 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[bug]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | Preferably, a short / minimal code example, that reproduces the behavior. 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Additional context** 21 | Add any other context about the problem here. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[feature request]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | schedule: 10 | - cron: '17 10 * * 3' 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | language: 23 | - go 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v4 27 | - name: Initialize CodeQL 28 | uses: github/codeql-action/init@v3 29 | with: 30 | languages: ${{ matrix.language }} 31 | - name: Autobuild 32 | uses: github/codeql-action/autobuild@v3 33 | - name: Perform CodeQL Analysis 34 | uses: github/codeql-action/analyze@v3 35 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | actions: read 14 | contents: read 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | go-version: 19 | - '1.21' 20 | - '1.20' 21 | steps: 22 | - name: Set up Go 23 | uses: actions/setup-go@v4 24 | with: 25 | go-version: ${{ matrix.go-version }} 26 | check-latest: true 27 | - name: Set up Staticcheck 28 | run: go install honnef.co/go/tools/cmd/staticcheck@latest 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | - name: Vet 32 | run: go vet -v ./... 33 | - name: Staticcheck 34 | run: staticcheck ./... 35 | - name: Build 36 | run: go build -v ./... 37 | - name: Test 38 | run: go test -v -cover -race -count=3 ./... 39 | -------------------------------------------------------------------------------- /all_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | ) 23 | 24 | func TestAll_error(t *testing.T) { 25 | e := errors.New(`some_error`) 26 | status, err := New(All, New(func(children []Node) (Status, error) { 27 | return Success, e 28 | })).Tick() 29 | if status != Failure { 30 | t.Error(status) 31 | } 32 | if err != e { 33 | t.Error(err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ratelimit.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import "time" 20 | 21 | // RateLimit generates a stateful Tick that will return success at most once per a given duration 22 | func RateLimit(d time.Duration) Tick { 23 | var last *time.Time 24 | return func(children []Node) (Status, error) { 25 | now := time.Now() 26 | if last != nil && now.Add(-d).Before(*last) { 27 | return Failure, nil 28 | } 29 | last = &now 30 | return Success, nil 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /selector.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | // Selector is a tick implementation that ticks each child sequentially, until the the first error (returning the 20 | // error), the first non-failure status (returning the status), or all children are ticked (returning failure) 21 | func Selector(children []Node) (Status, error) { 22 | for _, c := range children { 23 | status, err := c.Tick() 24 | if err != nil { 25 | return Failure, err 26 | } 27 | if status == Running { 28 | return Running, nil 29 | } 30 | if status == Success { 31 | return Success, nil 32 | } 33 | } 34 | return Failure, nil 35 | } 36 | -------------------------------------------------------------------------------- /sequence.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | // Sequence is a tick implementation that ticks each child sequentially, until the the first error (returning the 20 | // error), the first non-success status (returning the status), or all children are ticked (returning success) 21 | func Sequence(children []Node) (Status, error) { 22 | for _, c := range children { 23 | status, err := c.Tick() 24 | if err != nil { 25 | return Failure, err 26 | } 27 | if status == Running { 28 | return Running, nil 29 | } 30 | if status != Success { 31 | return Failure, nil 32 | } 33 | } 34 | return Success, nil 35 | } 36 | -------------------------------------------------------------------------------- /not.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | // Not inverts a Tick, such that any failure cases will be success and success cases will be failure, note that any 20 | // error or invalid status will still result in a failure 21 | func Not(tick Tick) Tick { 22 | if tick == nil { 23 | return nil 24 | } 25 | return func(children []Node) (Status, error) { 26 | status, err := tick(children) 27 | if err != nil { 28 | return Failure, err 29 | } 30 | switch status { 31 | case Running: 32 | return Running, nil 33 | case Failure: 34 | return Success, nil 35 | default: 36 | return Failure, nil 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /all.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | // All implements a tick which will tick all children sequentially until the first running status or error is 20 | // encountered (propagated), and will return success only if all children were ticked and returned success (returns 21 | // success if there were no children, like sequence). 22 | func All(children []Node) (Status, error) { 23 | success := true 24 | for _, child := range children { 25 | status, err := child.Tick() 26 | if err != nil { 27 | return Failure, err 28 | } 29 | if status == Running { 30 | return Running, nil 31 | } 32 | if status != Success { 33 | success = false 34 | } 35 | } 36 | if !success { 37 | return Failure, nil 38 | } 39 | return Success, nil 40 | } 41 | -------------------------------------------------------------------------------- /ratelimit_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "testing" 21 | "time" 22 | ) 23 | 24 | func TestRateLimit(t *testing.T) { 25 | var ( 26 | count = 0 27 | duration = time.Millisecond * 100 28 | node = New( 29 | Sequence, 30 | New(RateLimit(duration)), 31 | New(func(children []Node) (Status, error) { 32 | count++ 33 | return Success, nil 34 | }), 35 | ) 36 | ) 37 | timer := time.NewTimer(duration*4 + (duration / 2)) 38 | defer timer.Stop() 39 | ticker := time.NewTicker(time.Millisecond) 40 | defer ticker.Stop() 41 | for { 42 | select { 43 | case <-ticker.C: 44 | if _, err := node.Tick(); err != nil { 45 | t.Fatal(err) 46 | } 47 | continue 48 | case <-timer.C: 49 | } 50 | if count != 5 { 51 | t.Fatal(count) 52 | } 53 | return 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /shuffle.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "math/rand" 21 | ) 22 | 23 | // Shuffle implements randomised child execution order via encapsulation, using the provided source to shuffle the 24 | // children prior to passing through to the provided tick (a nil source will use global math/rand), note that this 25 | // function will return nil if a nil tick is provided 26 | func Shuffle(tick Tick, source rand.Source) Tick { 27 | if tick == nil { 28 | return nil 29 | } 30 | if source == nil { 31 | source = defaultSource{} 32 | } 33 | return func(children []Node) (Status, error) { 34 | children = copyNodes(children) 35 | rand.New(source).Shuffle(len(children), func(i, j int) { children[i], children[j] = children[j], children[i] }) 36 | return tick(children) 37 | } 38 | } 39 | 40 | type defaultSource struct{ rand.Source } 41 | 42 | func (d defaultSource) Int63() int64 { 43 | return rand.Int63() 44 | } 45 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 4 | github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= 5 | github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 6 | github.com/joeycumines/go-bigbuff v1.15.1 h1:lJNsoghg7Kh5HGfCs8/FFaGls/B669y9Vydt5byB4jQ= 7 | github.com/joeycumines/go-bigbuff v1.15.1/go.mod h1:7hqtGnMDT3v+yOvHUb+hx+JSxhlTF2W9BGIkvNQizaA= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 12 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 13 | github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= 14 | github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 16 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 17 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 18 | -------------------------------------------------------------------------------- /async.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | // Async wraps a tick so that it runs asynchronously, note nil ticks will return nil 20 | func Async(tick Tick) Tick { 21 | if tick == nil { 22 | return nil 23 | } 24 | var done chan struct { 25 | Status Status 26 | Error error 27 | } 28 | return func(children []Node) (Status, error) { 29 | if done == nil { 30 | // start the async tick, the non-nil done indicates that we are running 31 | done = make(chan struct { 32 | Status Status 33 | Error error 34 | }, 1) 35 | go func() { 36 | var status struct { 37 | Status Status 38 | Error error 39 | } 40 | defer func() { 41 | done <- status 42 | }() 43 | status.Status, status.Error = tick(children) 44 | }() 45 | return Running, nil 46 | } 47 | // the node is currently running 48 | select { 49 | case status := <-done: 50 | done = nil 51 | return status.Status, status.Error 52 | default: 53 | return Running, nil 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /switch.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | // Switch is a tick implementation that provides switch-like functionality, where each switch case is comprised of a 20 | // condition and statement, formed by a pair of (contiguous) children. If there are an odd number of children, then the 21 | // final child will be treated as a statement with an always-true condition (used as the default case). The first error 22 | // or first running status will be returned (if any). Otherwise, the result will be either that of the statement 23 | // corresponding to the first successful condition, or success. 24 | // 25 | // This implementation is compatible with both Memorize and Sync. 26 | func Switch(children []Node) (Status, error) { 27 | for i := 0; i < len(children); i += 2 { 28 | if i == len(children)-1 { 29 | // statement (default case) 30 | return children[i].Tick() 31 | } 32 | // condition (normal case) 33 | status, err := children[i].Tick() 34 | if err != nil { 35 | return Failure, err 36 | } 37 | if status == Running { 38 | return Running, nil 39 | } 40 | if status == Success { 41 | // statement (normal case) 42 | return children[i+1].Tick() 43 | } 44 | } 45 | // no matching condition and no default statement 46 | return Success, nil 47 | } 48 | -------------------------------------------------------------------------------- /not_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | ) 23 | 24 | func TestNot(t *testing.T) { 25 | children := make([]Node, 3) 26 | if status, err := Not(func(children []Node) (Status, error) { 27 | if len(children) != 3 { 28 | t.Error(children) 29 | } 30 | return Failure, errors.New(`some_err`) 31 | })(children); status != Failure || err == nil || err.Error() != `some_err` { 32 | t.Error(status, err) 33 | } 34 | if status, err := Not(func(children []Node) (Status, error) { 35 | if len(children) != 3 { 36 | t.Error(children) 37 | } 38 | return Running, nil 39 | })(children); status != Running || err != nil { 40 | t.Error(status, err) 41 | } 42 | if status, err := Not(func(children []Node) (Status, error) { 43 | if len(children) != 3 { 44 | t.Error(children) 45 | } 46 | return Failure, nil 47 | })(children); status != Success || err != nil { 48 | t.Error(status, err) 49 | } 50 | if status, err := Not(func(children []Node) (Status, error) { 51 | if len(children) != 3 { 52 | t.Error(children) 53 | } 54 | return Success, nil 55 | })(children); status != Failure || err != nil { 56 | t.Error(status, err) 57 | } 58 | if status, err := Not(func(children []Node) (Status, error) { 59 | if len(children) != 3 { 60 | t.Error(children) 61 | } 62 | return 1243145, nil 63 | })(children); status != Failure || err != nil { 64 | t.Error(status, err) 65 | } 66 | if Not(nil) != nil { 67 | t.Fatal(`wat`) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /background.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | // Background pushes running nodes into the background, allowing multiple concurrent ticks (potentially running 20 | // independent children, depending on the behavior of the node). It accepts a tick via closure, in order to support 21 | // stateful ticks. On tick, backgrounded nodes are ticked from oldest to newest, until the first non-running status is 22 | // returned, which will trigger removal from the backgrounded node list, and propagating status and any error, without 23 | // modification. All other normal operation will result in a new node being generated and ticked, backgrounding it on 24 | // running, otherwise discarding the node and propagating it's return values immediately. Passing a nil value will 25 | // cause nil to be returned. 26 | // WARNING there is no upper bound to the number of backgrounded nodes (the caller must manage that externally). 27 | func Background(tick func() Tick) Tick { 28 | if tick == nil { 29 | return nil 30 | } 31 | var nodes []Node 32 | return func(children []Node) (Status, error) { 33 | for i, node := range nodes { 34 | status, err := node.Tick() 35 | if err == nil && status == Running { 36 | continue 37 | } 38 | copy(nodes[i:], nodes[i+1:]) 39 | nodes[len(nodes)-1] = nil 40 | nodes = nodes[:len(nodes)-1] 41 | return status, err 42 | } 43 | node := NewNode(tick(), children) 44 | status, err := node.Tick() 45 | if err != nil || status != Running { 46 | return status, err 47 | } 48 | nodes = append(nodes, node) 49 | return Running, nil 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /any.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import "sync" 20 | 21 | // Any wraps a tick such that non-error non-running statuses will be overridden with a success if at least one child 22 | // succeeded - which is achieved by encapsulation of children, before passing them into the wrapped tick. Nil will be 23 | // returned if tick is nil, and nil children will be passed through as such. 24 | func Any(tick Tick) Tick { 25 | if tick == nil { 26 | return nil 27 | } 28 | var ( 29 | mutex sync.Mutex 30 | success bool 31 | ) 32 | return func(children []Node) (Status, error) { 33 | children = copyNodes(children) 34 | for i := range children { 35 | child := children[i] 36 | if child == nil { 37 | continue 38 | } 39 | children[i] = func() (Tick, []Node) { 40 | tick, nodes := child() 41 | if tick == nil { 42 | return nil, nodes 43 | } 44 | return func(children []Node) (Status, error) { 45 | status, err := tick(children) 46 | if err == nil && status == Success { 47 | mutex.Lock() 48 | success = true 49 | mutex.Unlock() 50 | } 51 | return status, err 52 | }, nodes 53 | } 54 | } 55 | status, err := tick(children) 56 | if err != nil { 57 | return Failure, err 58 | } 59 | if status == Running { 60 | return Running, nil 61 | } 62 | mutex.Lock() 63 | defer mutex.Unlock() 64 | if !success { 65 | return Failure, nil 66 | } 67 | success = false 68 | return Success, nil 69 | } 70 | } 71 | 72 | func copyNodes(src []Node) (dst []Node) { 73 | if src == nil { 74 | return 75 | } 76 | dst = make([]Node, len(src)) 77 | copy(dst, src) 78 | return 79 | } 80 | -------------------------------------------------------------------------------- /fork.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "fmt" 21 | ) 22 | 23 | // Fork generates a stateful Tick which will tick all children at once, returning after all children return a result, 24 | // returning running if any children did so, and ticking only those which returned running in subsequent calls, until 25 | // all children have returned a non-running status, combining any errors, and returning success if there were no 26 | // failures or errors (otherwise failure), repeating this cycle for subsequent ticks 27 | func Fork() Tick { 28 | var ( 29 | remaining []Node 30 | status Status 31 | err error 32 | ) 33 | return func(children []Node) (Status, error) { 34 | if status == 0 && err == nil { 35 | // cycle start 36 | status = Success 37 | remaining = make([]Node, len(children)) 38 | copy(remaining, children) 39 | } 40 | count := len(remaining) 41 | outputs := make(chan func(), count) 42 | for _, node := range remaining { 43 | go func(node Node) { 44 | rs, re := node.Tick() 45 | outputs <- func() { 46 | if re != nil { 47 | rs = Failure 48 | if err != nil { 49 | err = fmt.Errorf("%s | %s", err.Error(), re.Error()) 50 | } else { 51 | err = re 52 | } 53 | } 54 | switch rs { 55 | case Running: 56 | remaining = append(remaining, node) 57 | case Success: 58 | // success is the initial status (until 1+ failures) 59 | default: 60 | status = Failure 61 | } 62 | } 63 | }(node) 64 | } 65 | remaining = remaining[:0] 66 | for x := 0; x < count; x++ { 67 | (<-outputs)() 68 | } 69 | if len(remaining) == 0 { 70 | // cycle end 71 | rs, re := status, err 72 | status, err = 0, nil 73 | return rs, re 74 | } 75 | return Running, nil 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /sync.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import "sync" 20 | 21 | // Sync will wrap a set of nodes in such a way that their real ticks will only be triggered when either the node 22 | // being ticked was previously running, or no other nodes are running, synchronising calling their Node and Tick calls. 23 | // 24 | // NOTE the Memorize function provides similar functionality, and should be preferred, where both are suitable. 25 | func Sync(nodes []Node) []Node { 26 | if nodes == nil { 27 | return nil 28 | } 29 | s := &syc{ 30 | nodes: nodes, 31 | mutex: new(sync.Mutex), 32 | statuses: make([]Status, len(nodes)), 33 | } 34 | result := make([]Node, 0, len(nodes)) 35 | for i := range nodes { 36 | result = append(result, s.node(i)) 37 | } 38 | return result 39 | } 40 | 41 | type syc struct { 42 | nodes []Node 43 | statuses []Status 44 | mutex *sync.Mutex 45 | } 46 | 47 | func (s *syc) running() bool { 48 | for _, status := range s.statuses { 49 | if status == Running { 50 | return true 51 | } 52 | } 53 | return false 54 | } 55 | 56 | func (s *syc) node(i int) Node { 57 | if s.nodes[i] == nil { 58 | return nil 59 | } 60 | return func() (Tick, []Node) { 61 | s.mutex.Lock() 62 | defer s.mutex.Unlock() 63 | tick, children := s.nodes[i]() 64 | if tick == nil { 65 | return nil, children 66 | } 67 | status := s.statuses[i] 68 | if status != Running && s.running() { 69 | return func(children []Node) (Status, error) { 70 | // disabled tick - we just return the last status 71 | return status, nil 72 | }, children 73 | } 74 | return func(children []Node) (Status, error) { 75 | s.mutex.Lock() 76 | defer s.mutex.Unlock() 77 | status, err := tick(children) 78 | s.statuses[i] = status 79 | return status, err 80 | }, children 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /memorize.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | // Memorize encapsulates a tick, and will cache the first non-running status for each child, per "execution", defined 20 | // as the period until the first non-running status, of the encapsulated tick, facilitating execution of asynchronous 21 | // nodes in serial with their siblings, using stateless tick implementations, such as sequence and selector. 22 | // 23 | // Sync provides a similar but more flexible mechanism, at the expense of greater complexity, and more cumbersome 24 | // usage. Sync supports modification of children mid-execution, and may be used to implement complex guarding behavior 25 | // as children of a single Tick, equivalent to more complex structures using multiple memorized sequence nodes. 26 | func Memorize(tick Tick) Tick { 27 | if tick == nil { 28 | return nil 29 | } 30 | var ( 31 | started bool 32 | nodes []Node 33 | ) 34 | return func(children []Node) (status Status, err error) { 35 | if !started { 36 | nodes = copyNodes(children) 37 | for i := range nodes { 38 | var ( 39 | child = nodes[i] 40 | override Tick 41 | ) 42 | if child == nil { 43 | continue 44 | } 45 | nodes[i] = func() (Tick, []Node) { 46 | tick, nodes := child() 47 | if override != nil { 48 | return override, nodes 49 | } 50 | if tick == nil { 51 | return nil, nodes 52 | } 53 | return func(children []Node) (Status, error) { 54 | status, err := tick(children) 55 | if err != nil || status != Running { 56 | override = func(children []Node) (Status, error) { return status, err } 57 | } 58 | return status, err 59 | }, nodes 60 | } 61 | } 62 | started = true 63 | } 64 | status, err = tick(nodes) 65 | if err != nil || status != Running { 66 | started = false 67 | nodes = nil 68 | } 69 | return 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /frame.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "reflect" 21 | ) 22 | 23 | type ( 24 | // Frame is a partial copy of runtime.Frame. 25 | // 26 | // This packages captures details about the caller of it's New and NewNode functions, embedding them into the 27 | // nodes themselves, for tree printing / tracing purposes. 28 | Frame struct { 29 | // PC is the program counter for the location in this frame. 30 | // For a frame that calls another frame, this will be the 31 | // program counter of a call instruction. Because of inlining, 32 | // multiple frames may have the same PC value, but different 33 | // symbolic information. 34 | PC uintptr 35 | // Function is the package path-qualified function name of 36 | // this call frame. If non-empty, this string uniquely 37 | // identifies a single function in the program. 38 | // This may be the empty string if not known. 39 | Function string 40 | // File and Line are the file name and line number of the 41 | // location in this frame. For non-leaf frames, this will be 42 | // the location of a call. These may be the empty string and 43 | // zero, respectively, if not known. 44 | File string 45 | Line int 46 | // Entry point program counter for the function; may be zero 47 | // if not known. 48 | Entry uintptr 49 | } 50 | 51 | vkFrame struct{} 52 | ) 53 | 54 | // Frame will return the call frame for the caller of New/NewNode, an approximation based on the receiver, or nil. 55 | // 56 | // This method uses the Value mechanism and is subject to the same warnings / performance limitations. 57 | func (n Node) Frame() *Frame { 58 | if v, _ := n.Value(vkFrame{}).(*Frame); v != nil { 59 | v := *v 60 | return &v 61 | } 62 | return newFrame(n) 63 | } 64 | 65 | // Frame will return an approximation of a call frame based on the receiver, or nil. 66 | func (t Tick) Frame() *Frame { return newFrame(t) } 67 | 68 | func newFrame(v interface{}) (f *Frame) { 69 | if v := reflect.ValueOf(v); v.IsValid() && v.Kind() == reflect.Func && !v.IsNil() { 70 | p := v.Pointer() 71 | if v := runtimeFuncForPC(p); v != nil { 72 | f = &Frame{ 73 | PC: p, 74 | Function: v.Name(), 75 | Entry: v.Entry(), 76 | } 77 | f.File, f.Line = v.FileLine(f.Entry) 78 | } 79 | } 80 | return 81 | } 82 | -------------------------------------------------------------------------------- /sync_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "errors" 21 | "reflect" 22 | "testing" 23 | ) 24 | 25 | func TestSync_empty(t *testing.T) { 26 | nodes := make([]Node, 0) 27 | newNodes := Sync(nodes) 28 | if len(newNodes) != 0 { 29 | t.Error("bad nodes") 30 | } 31 | } 32 | 33 | func TestSync_set(t *testing.T) { 34 | nodes := make([]Node, 2) 35 | child := NewNode(Sequence, nil) 36 | called := false 37 | node := NewNode(func(children []Node) (Status, error) { 38 | if len(children) != 1 || reflect.ValueOf(child).Pointer() != reflect.ValueOf(children[0]).Pointer() { 39 | t.Fatal("bad child in node") 40 | } 41 | called = true 42 | return Success, errors.New("suk") 43 | }, []Node{child}) 44 | nodes[1] = node 45 | newNodes := Sync(nodes) 46 | if len(newNodes) != 2 { 47 | t.Error("bad nodes") 48 | } 49 | if newNodes[0] != nil { 50 | t.Error("bad value") 51 | } 52 | nodes[0] = NewNode(Sequence, nil) 53 | if newNodes[0] != nil { 54 | t.Error("bad value") 55 | } 56 | if newNodes[1] == nil { 57 | t.Fatal("bad value") 58 | } 59 | if reflect.ValueOf(node).Pointer() == reflect.ValueOf(newNodes[1]).Pointer() { 60 | t.Fatal("bad value") 61 | } 62 | tick, children := newNodes[1]() 63 | if len(children) != 1 { 64 | t.Fatal("bad value") 65 | } 66 | status, err := tick(children) 67 | if !called { 68 | t.Fatal("bad call") 69 | } 70 | if status != Success || err == nil || err.Error() != "suk" { 71 | t.Fatal("bad tick value") 72 | } 73 | } 74 | 75 | func TestSync_nil(t *testing.T) { 76 | if nodes := Sync(nil); nodes != nil { 77 | t.Fatal("expected nil nodes") 78 | } 79 | } 80 | 81 | func TestSync_nilTick(t *testing.T) { 82 | var node Node = func() (Tick, []Node) { 83 | return nil, nil 84 | } 85 | nodes := Sync([]Node{ 86 | func() (Tick, []Node) { 87 | return nil, []Node{node} 88 | }, 89 | }) 90 | if len(nodes) != 1 { 91 | t.Fatal("unexpected nodes", nodes) 92 | } 93 | tick, children := nodes[0]() 94 | if tick != nil { 95 | t.Error("expected a nil tick") 96 | } 97 | if len(children) != 1 || reflect.ValueOf(children[0]).Pointer() != reflect.ValueOf(node).Pointer() { 98 | t.Error("expected children to be returned", children) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "context" 21 | "testing" 22 | "time" 23 | ) 24 | 25 | func TestContext_Tick_nilFn(t *testing.T) { 26 | if v := new(Context).Tick(nil); v != nil { 27 | t.Error(`expected nil`) 28 | } 29 | } 30 | 31 | func TestContext_Cancel_noContext(t *testing.T) { 32 | if status, err := new(Context).Cancel(nil); err != nil || status != Success { 33 | t.Error(status, err) 34 | } 35 | } 36 | 37 | func TestContext_Done_noContext(t *testing.T) { 38 | if status, err := new(Context).Done(nil); err != nil || status != Success { 39 | t.Error(status, err) 40 | } 41 | } 42 | 43 | func TestContext_Init_default(t *testing.T) { 44 | c := new(Context) 45 | if status, err := c.Init(nil); err != nil || status != Success { 46 | t.Fatal(status, err) 47 | } 48 | ctx := c.ctx 49 | if err := ctx.Err(); err != nil { 50 | t.Fatal(err) 51 | } 52 | if status, err := c.Init(nil); err != nil || status != Success { 53 | t.Fatal(status, err) 54 | } 55 | if err := ctx.Err(); err == nil { 56 | t.Error(c) 57 | } 58 | if c.ctx == nil || c.cancel == nil || c.ctx.Err() != nil { 59 | t.Fatal(c) 60 | } 61 | c.cancel() 62 | if c.ctx.Err() == nil { 63 | t.Fatal(c) 64 | } 65 | } 66 | 67 | func TestContext_WithCancel(t *testing.T) { 68 | ctx, cancel := context.WithCancel(context.Background()) 69 | defer cancel() 70 | c := new(Context) 71 | if v := c.WithCancel(ctx); v != c { 72 | t.Error(v) 73 | } 74 | if c.ctx != nil || c.cancel != nil || c.parent == nil { 75 | t.Fatal(c) 76 | } 77 | if status, err := c.Init(nil); err != nil || status != Success { 78 | t.Fatal(status, err) 79 | } 80 | if c.ctx == nil || c.cancel == nil || c.ctx.Err() != nil { 81 | t.Fatal(c) 82 | } 83 | cancel() 84 | if err := c.ctx.Err(); err != context.Canceled { 85 | t.Fatal(err) 86 | } 87 | } 88 | 89 | func TestContext_WithDeadline(t *testing.T) { 90 | c := new(Context) 91 | if v := c.WithDeadline(context.Background(), time.Now().Add(-time.Second)); v != c { 92 | t.Error(v) 93 | } 94 | if c.ctx != nil || c.cancel != nil || c.parent == nil { 95 | t.Fatal(c) 96 | } 97 | if status, err := c.Init(nil); err != nil || status != Success { 98 | t.Fatal(status, err) 99 | } 100 | if err := c.ctx.Err(); err != context.DeadlineExceeded { 101 | t.Fatal(err) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /shuffle_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "math/rand" 23 | "testing" 24 | "time" 25 | ) 26 | 27 | func ExampleShuffle() { 28 | randSource := rand.NewSource(1231244) 29 | var ( 30 | newPrintlnFn = func(fn func() []interface{}) Tick { 31 | return func([]Node) (Status, error) { 32 | fmt.Println(fn()...) 33 | return Success, nil 34 | } 35 | } 36 | newPrintln = func(v ...interface{}) Tick { return newPrintlnFn(func() []interface{} { return v }) } 37 | done bool 38 | ticker = NewTickerStopOnFailure(context.Background(), time.Millisecond, New( 39 | Sequence, 40 | New(newPrintlnFn(func() func() []interface{} { 41 | var i int 42 | return func() []interface{} { 43 | i++ 44 | return []interface{}{`tick number`, i} 45 | } 46 | }())), 47 | New( 48 | Shuffle(Sequence, randSource), 49 | New(newPrintln(`node 1`)), 50 | New(newPrintln(`node 2`)), 51 | New( 52 | Selector, 53 | New(func() func(children []Node) (Status, error) { 54 | remaining := 5 55 | return func(children []Node) (Status, error) { 56 | if remaining > 0 { 57 | remaining-- 58 | return Success, nil 59 | } 60 | return Failure, nil 61 | } 62 | }()), 63 | New( 64 | Shuffle(Selector, randSource), 65 | New(newPrintln(`node 3`)), 66 | New(newPrintln(`node 4`)), 67 | New(newPrintln(`node 5`)), 68 | New(newPrintln(`node 6`)), 69 | New(func([]Node) (Status, error) { 70 | done = true 71 | return Success, nil 72 | }), 73 | ), 74 | ), 75 | ), 76 | New(func([]Node) (Status, error) { 77 | if done { 78 | return Failure, nil 79 | } 80 | return Success, nil 81 | }), 82 | )) 83 | ) 84 | <-ticker.Done() 85 | if err := ticker.Err(); err != nil { 86 | panic(err) 87 | } 88 | //output: 89 | //tick number 1 90 | //node 1 91 | //node 2 92 | //tick number 2 93 | //node 2 94 | //node 1 95 | //tick number 3 96 | //node 1 97 | //node 2 98 | //tick number 4 99 | //node 2 100 | //node 1 101 | //tick number 5 102 | //node 2 103 | //node 1 104 | //tick number 6 105 | //node 1 106 | //node 2 107 | //node 5 108 | //tick number 7 109 | //node 6 110 | //node 1 111 | //node 2 112 | //tick number 8 113 | //node 2 114 | //node 5 115 | //node 1 116 | //tick number 9 117 | //node 3 118 | //node 2 119 | //node 1 120 | //tick number 10 121 | //node 2 122 | //node 1 123 | } 124 | 125 | func TestShuffle_nil(t *testing.T) { 126 | if v := Shuffle(nil, nil); v != nil { 127 | t.Fatal(v) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /behaviortree.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package behaviortree provides a simple and powerful Go implementation of behavior trees without fluff. 18 | package behaviortree 19 | 20 | import ( 21 | "errors" 22 | "fmt" 23 | ) 24 | 25 | const ( 26 | _ Status = iota 27 | // Running indicates that the Tick for a given Node is currently running 28 | Running 29 | // Success indicates that the Tick for a given Node completed successfully 30 | Success 31 | // Failure indicates that the Tick for a given Node failed to complete successfully 32 | Failure 33 | ) 34 | 35 | type ( 36 | // Node represents an node in a tree, that can be ticked 37 | Node func() (Tick, []Node) 38 | 39 | // Tick represents the logic for a node, which may or may not be stateful 40 | Tick func(children []Node) (Status, error) 41 | 42 | // Status is a type with three valid values, Running, Success, and Failure, the three possible states for BTs 43 | Status int 44 | ) 45 | 46 | // New constructs a new behavior tree and is equivalent to NewNode with vararg support for less indentation 47 | func New(tick Tick, children ...Node) Node { return factory(tick, children) } 48 | 49 | // NewNode constructs a new node out of a tick and children 50 | func NewNode(tick Tick, children []Node) Node { return factory(tick, children) } 51 | 52 | var factory = func(tick Tick, children []Node) (node Node) { 53 | var ( 54 | frame *Frame 55 | ) 56 | if v := make([]uintptr, 1); runtimeCallers(3, v[:]) >= 1 { 57 | if v, _ := runtimeCallersFrames(v).Next(); v.PC != 0 { 58 | frame = &Frame{ 59 | PC: v.PC, 60 | Function: v.Function, 61 | File: v.File, 62 | Line: v.Line, 63 | Entry: v.Entry, 64 | } 65 | } 66 | } 67 | node = func() (Tick, []Node) { 68 | if frame != nil { 69 | node.valueHandle(func(key interface{}) (interface{}, bool) { 70 | if key != (vkFrame{}) { 71 | return nil, false 72 | } 73 | frame := *frame 74 | return &frame, true 75 | }) 76 | } 77 | return tick, children 78 | } 79 | return 80 | } 81 | 82 | // Tick runs the node's tick function with it's children 83 | func (n Node) Tick() (Status, error) { 84 | if n == nil { 85 | return Failure, errors.New("behaviortree.Node cannot tick a nil node") 86 | } 87 | tick, children := n() 88 | if tick == nil { 89 | return Failure, errors.New("behaviortree.Node cannot tick a node with a nil tick") 90 | } 91 | return tick(children) 92 | } 93 | 94 | // Status returns the status value, but defaults to failure on out of bounds 95 | func (s Status) Status() Status { 96 | switch s { 97 | case Running: 98 | return Running 99 | case Success: 100 | return Success 101 | default: 102 | return Failure 103 | } 104 | } 105 | 106 | // String returns a string representation of the status 107 | func (s Status) String() string { 108 | switch s { 109 | case Running: 110 | return `running` 111 | case Success: 112 | return `success` 113 | case Failure: 114 | return `failure` 115 | default: 116 | return fmt.Sprintf("unknown status (%d)", s) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /memorize_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | ) 23 | 24 | func TestMemorize_nilTick(t *testing.T) { 25 | if v := Memorize(nil); v != nil { 26 | t.Error(`expected nil`) 27 | } 28 | } 29 | 30 | func TestMemorize_nilChildCases(t *testing.T) { 31 | var ( 32 | i int 33 | j int 34 | e = errors.New(`some_error`) 35 | ) 36 | _, _ = New( 37 | Memorize(func(children []Node) (status Status, err error) { 38 | if len(children) != 3 || 39 | children[0] == nil || 40 | children[1] != nil || 41 | children[2] == nil { 42 | t.Fatal(children) 43 | } 44 | { 45 | tick, c := children[0]() 46 | if tick != nil { 47 | t.Error(`expected nil`) 48 | } 49 | if len(c) != 1 || c[0] == nil { 50 | t.Error(c) 51 | } 52 | if status, err := c[0].Tick(); err != nil || status != Failure { 53 | t.Error(status, err) 54 | } 55 | } 56 | for x := 0; x < 10; x++ { 57 | tick, c := children[2]() 58 | if c != nil { 59 | t.Error(c) 60 | } 61 | status, err := tick(c) 62 | if status != Running || err != e { 63 | t.Error(status, err) 64 | } 65 | } 66 | i++ 67 | return 68 | }), 69 | New(nil, New(Selector)), 70 | nil, 71 | New(func(children []Node) (status Status, err error) { 72 | j++ 73 | status = Running 74 | err = e 75 | return 76 | }), 77 | ).Tick() 78 | if i != 1 { 79 | t.Error(i) 80 | } 81 | if j != 1 { 82 | t.Error(j) 83 | } 84 | } 85 | 86 | func TestMemorize_errorResets(t *testing.T) { 87 | var ( 88 | i int 89 | j int 90 | e error 91 | node = New( 92 | Memorize(func(children []Node) (Status, error) { 93 | // this is just sequence but without normalisation of status to failure on error 94 | for _, c := range children { 95 | status, err := c.Tick() 96 | if status != Success || err != nil { 97 | return status, err 98 | } 99 | } 100 | return Success, nil 101 | }), 102 | New(func([]Node) (Status, error) { 103 | i++ 104 | return Success, nil 105 | }), 106 | New(func([]Node) (Status, error) { 107 | j++ 108 | return Running, e 109 | }), 110 | ) 111 | ) 112 | if i != 0 || j != 0 { 113 | t.Fatal(i, j) 114 | } 115 | if status, err := node.Tick(); err != nil || status != Running { 116 | t.Fatal(status, err) 117 | } 118 | if i != 1 || j != 1 { 119 | t.Fatal(i, j) 120 | } 121 | if status, err := node.Tick(); err != nil || status != Running { 122 | t.Fatal(status, err) 123 | } 124 | if i != 1 || j != 2 { 125 | t.Fatal(i, j) 126 | } 127 | if status, err := node.Tick(); err != nil || status != Running { 128 | t.Fatal(status, err) 129 | } 130 | if i != 1 || j != 3 { 131 | t.Fatal(i, j) 132 | } 133 | e = errors.New(`some_error`) 134 | if status, err := node.Tick(); err != e || status != Running { 135 | t.Fatal(status, err) 136 | } 137 | if i != 1 || j != 4 { 138 | t.Fatal(i, j) 139 | } 140 | e = nil 141 | if status, err := node.Tick(); err != nil || status != Running { 142 | t.Fatal(status, err) 143 | } 144 | if i != 2 || j != 5 { 145 | t.Fatal(i, j) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /frame_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "strings" 21 | "testing" 22 | ) 23 | 24 | func TestNode_Frame(t *testing.T) { 25 | for _, test := range []struct { 26 | Name string 27 | Node Node 28 | Frame *Frame 29 | }{ 30 | { 31 | Name: `nil`, 32 | }, 33 | { 34 | Name: `nn with value`, 35 | Node: nn(nil, nil).WithValue(1, 2), 36 | Frame: &Frame{ 37 | Function: `github.com/joeycumines/go-behaviortree.Node.WithValue.func1`, 38 | File: `value.go`, 39 | }, 40 | }, 41 | { 42 | Name: `nn with value explicit frame`, 43 | Node: nn(nil, nil).WithValue(vkFrame{}, &Frame{ 44 | PC: 0x568dc0, 45 | Function: "github.com/joeycumines/go-behaviortree.glob..func1.1", 46 | File: "C:/Users/under/go/src/github.com/joeycumines/go-behaviortree/behaviortree.go", 47 | Line: 53, 48 | Entry: 0x568dc0, 49 | }), 50 | Frame: &Frame{ 51 | Function: "github.com/joeycumines/go-behaviortree.glob..func1.1", 52 | File: "behaviortree.go", 53 | }, 54 | }, 55 | } { 56 | t.Run(test.Name, func(t *testing.T) { 57 | for i := 0; i < 2; i++ { 58 | frame := test.Node.Frame() 59 | if frame != nil { 60 | if frame.Line == 0 { 61 | t.Error(frame.Line) 62 | } else { 63 | frame.Line = 0 64 | } 65 | if frame.PC == 0 { 66 | t.Error(frame.PC) 67 | } else { 68 | frame.PC = 0 69 | } 70 | if frame.Entry == 0 { 71 | t.Error(frame.Entry) 72 | } else { 73 | frame.Entry = 0 74 | } 75 | if i := strings.LastIndex(frame.File, "/"); i >= 0 { 76 | frame.File = frame.File[i+1:] 77 | } else { 78 | t.Error(frame.File) 79 | } 80 | } 81 | if (frame == nil) != (test.Frame == nil) || (frame != nil && *frame != *test.Frame) { 82 | t.Errorf("%+v", frame) 83 | } 84 | } 85 | }) 86 | } 87 | } 88 | 89 | func TestTick_Frame(t *testing.T) { 90 | for _, test := range []struct { 91 | Name string 92 | Tick Tick 93 | Frame *Frame 94 | }{ 95 | { 96 | Name: `nil`, 97 | }, 98 | { 99 | Name: `sequence`, 100 | Tick: Sequence, 101 | Frame: &Frame{ 102 | Function: `github.com/joeycumines/go-behaviortree.Sequence`, 103 | File: `sequence.go`, 104 | }, 105 | }, 106 | } { 107 | t.Run(test.Name, func(t *testing.T) { 108 | for i := 0; i < 2; i++ { 109 | frame := test.Tick.Frame() 110 | if frame != nil { 111 | if frame.Line == 0 { 112 | t.Error(frame.Line) 113 | } else { 114 | frame.Line = 0 115 | } 116 | if frame.PC == 0 { 117 | t.Error(frame.PC) 118 | } else { 119 | frame.PC = 0 120 | } 121 | if frame.Entry == 0 { 122 | t.Error(frame.Entry) 123 | } else { 124 | frame.Entry = 0 125 | } 126 | if i := strings.LastIndex(frame.File, "/"); i >= 0 { 127 | frame.File = frame.File[i+1:] 128 | } else { 129 | t.Error(frame.File) 130 | } 131 | } 132 | if (frame == nil) != (test.Frame == nil) || (frame != nil && *frame != *test.Frame) { 133 | t.Errorf("%+v", frame) 134 | } 135 | } 136 | }) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /async_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "errors" 21 | "github.com/go-test/deep" 22 | "testing" 23 | "time" 24 | ) 25 | 26 | func TestAsync_behaviour(t *testing.T) { 27 | out := make(chan int, 50) 28 | assertOut := func(expected []int) { 29 | actual := make([]int, 0) 30 | for running := true; running; { 31 | select { 32 | case i := <-out: 33 | actual = append(actual, i) 34 | default: 35 | running = false 36 | } 37 | } 38 | if diff := deep.Equal(expected, actual); diff != nil { 39 | t.Fatal("unexpected out diff:", diff) 40 | } 41 | } 42 | 43 | node := NewNode( 44 | Sequence, 45 | Sync([]Node{ 46 | NewNode(func(children []Node) (Status, error) { 47 | out <- 1 48 | return Success, nil 49 | }, nil), 50 | NewNode( 51 | Async(func(children []Node) (Status, error) { 52 | time.Sleep(time.Millisecond * 100) 53 | out <- 2 54 | return Success, nil 55 | }), 56 | nil, 57 | ), 58 | NewNode(func(children []Node) (Status, error) { 59 | out <- 3 60 | return Success, nil 61 | }, nil), 62 | }), 63 | ) 64 | 65 | run := func() { 66 | assertOut([]int{}) 67 | if status, err := node.Tick(); status != Running { 68 | t.Error("status was meant to be running but it was", status) 69 | } else if err != nil { 70 | t.Error("error was non-nil", err) 71 | } 72 | assertOut([]int{1}) 73 | // still running 74 | if status, err := node.Tick(); status != Running { 75 | t.Error("status was meant to be running but it was", status) 76 | } else if err != nil { 77 | t.Error("error was non-nil", err) 78 | } 79 | // sleep for a bit 80 | time.Sleep(time.Millisecond * 50) 81 | // still running 82 | assertOut([]int{}) 83 | if status, err := node.Tick(); status != Running { 84 | t.Error("status was meant to be running but it was", status) 85 | } else if err != nil { 86 | t.Error("error was non-nil", err) 87 | } 88 | assertOut([]int{}) 89 | // sleep for a bit more 90 | time.Sleep(time.Millisecond * 70) 91 | assertOut([]int{2}) 92 | // should be done 93 | if status, err := node.Tick(); status != Success { 94 | t.Error("status was meant to be running but it was", status) 95 | } else if err != nil { 96 | t.Error("error was non-nil", err) 97 | } 98 | assertOut([]int{3}) 99 | } 100 | 101 | for x := 0; x < 3; x++ { 102 | run() 103 | } 104 | } 105 | 106 | func TestAsync_error(t *testing.T) { 107 | rErr := errors.New("some_error") 108 | node := NewNode( 109 | Async(func(children []Node) (Status, error) { 110 | return Failure, rErr 111 | }), 112 | nil, 113 | ) 114 | if status, err := node.Tick(); status != Running { 115 | t.Error("status was meant to be running but it was", status) 116 | } else if err != nil { 117 | t.Error("error was non-nil", err) 118 | } 119 | time.Sleep(time.Millisecond * 5) 120 | if status, err := node.Tick(); status != Failure { 121 | t.Error("status was meant to be failure but it was", status) 122 | } else if err != rErr { 123 | t.Error("unexpected error value:", err) 124 | } 125 | } 126 | 127 | func TestAsync_nil(t *testing.T) { 128 | if Async(nil) != nil { 129 | t.Fatal("expected nil tick") 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "context" 21 | "time" 22 | ) 23 | 24 | // Context provides support for tick(s) utilising context as a means of cancelation, with cancelation triggered by 25 | // either BT-driven logic or the normal means (parent cancelation, deadline / timeout). 26 | // 27 | // Note that it must be initialised by means of it's Init method (implements a tick) prior to use (Context.Tick tick). 28 | // Init may be ticked any number of times (each time triggering cancelation of any prior context). 29 | type Context struct { 30 | parent func() (context.Context, context.CancelFunc) 31 | ctx context.Context 32 | cancel context.CancelFunc 33 | } 34 | 35 | // WithCancel configures the receiver to initialise context like context.WithCancel(parent), returning the receiver 36 | func (c *Context) WithCancel(parent context.Context) *Context { 37 | c.parent = func() (context.Context, context.CancelFunc) { return context.WithCancel(parent) } 38 | return c 39 | } 40 | 41 | // WithDeadline configures the receiver to initialise context like context.WithDeadline(parent, deadline), returning 42 | // the receiver 43 | func (c *Context) WithDeadline(parent context.Context, deadline time.Time) *Context { 44 | c.parent = func() (context.Context, context.CancelFunc) { return context.WithDeadline(parent, deadline) } 45 | return c 46 | } 47 | 48 | // WithTimeout configures the receiver to initialise context like context.WithTimeout(parent, timeout), returning 49 | // the receiver 50 | func (c *Context) WithTimeout(parent context.Context, timeout time.Duration) *Context { 51 | c.parent = func() (context.Context, context.CancelFunc) { return context.WithTimeout(parent, timeout) } 52 | return c 53 | } 54 | 55 | // Init implements a tick that will cancel existing context, (re)initialise the context, then succeed, note that it 56 | // must not be called concurrently with any other method, and it must be ticked prior to any Context.Tick tick 57 | func (c *Context) Init([]Node) (Status, error) { 58 | if c.cancel != nil { 59 | c.cancel() 60 | } 61 | if c.parent != nil { 62 | c.ctx, c.cancel = c.parent() 63 | } else { 64 | c.ctx, c.cancel = context.WithCancel(context.Background()) 65 | } 66 | return Success, nil 67 | } 68 | 69 | // Tick returns a tick that will call fn with the receiver's context, returning nil if fn is nil (for consistency 70 | // with other implementations in this package), note that a Init node must have already been ticked on all possible 71 | // execution paths, or a panic may occur, due to fn being passed a nil context.Context 72 | func (c *Context) Tick(fn func(ctx context.Context, children []Node) (Status, error)) Tick { 73 | if fn != nil { 74 | return func(children []Node) (Status, error) { return fn(c.ctx, children) } 75 | } 76 | return nil 77 | } 78 | 79 | // Cancel implements a tick that will cancel the receiver's context (noop if it has none) then succeed 80 | func (c *Context) Cancel([]Node) (Status, error) { 81 | if c.cancel != nil { 82 | c.cancel() 83 | } 84 | return Success, nil 85 | } 86 | 87 | // Err implements a tick that will succeed if the receiver does not have a context or it has been canceled 88 | func (c *Context) Err([]Node) (Status, error) { 89 | if c.ctx == nil || c.ctx.Err() != nil { 90 | return Success, nil 91 | } 92 | return Failure, nil 93 | } 94 | 95 | // Done implements a tick that will block on the receiver's context being canceled (noop if it has none) then succeed 96 | func (c *Context) Done([]Node) (Status, error) { 97 | if c.ctx != nil { 98 | <-c.ctx.Done() 99 | } 100 | return Success, nil 101 | } 102 | -------------------------------------------------------------------------------- /value.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "errors" 21 | "reflect" 22 | "runtime" 23 | "sync" 24 | ) 25 | 26 | var ( 27 | runtimeCallers = runtime.Callers 28 | runtimeCallersFrames = runtime.CallersFrames 29 | runtimeFuncForPC = runtime.FuncForPC 30 | ) 31 | 32 | var ( 33 | valueCallMutex sync.Mutex 34 | valueDataMutex sync.RWMutex 35 | valueDataKey interface{} 36 | valueDataChan chan interface{} 37 | valueDataCaller [1]uintptr 38 | ) 39 | 40 | // WithValue will return the receiver wrapped with a key-value pair, using similar semantics to the context package. 41 | // 42 | // Values should only be used to attach information to BTs in a way that transits API boundaries, not for passing 43 | // optional parameters to functions. Some package-level synchronisation was necessary to facilitate this mechanism. As 44 | // such, this and the Node.Value method should be used with caution, preferably only outside normal operation. 45 | // 46 | // The same restrictions on the key apply as for context.WithValue. 47 | func (n Node) WithValue(key, value interface{}) Node { 48 | if n == nil { 49 | panic(errors.New(`behaviortree.Node.WithValue nil receiver`)) 50 | } 51 | if key == nil { 52 | panic(errors.New(`behaviortree.Node.WithValue nil key`)) 53 | } 54 | if !reflect.TypeOf(key).Comparable() { 55 | panic(errors.New(`behaviortree.Node.WithValue key is not comparable`)) 56 | } 57 | return func() (Tick, []Node) { 58 | n.valueHandle(func(k interface{}) (interface{}, bool) { 59 | if k == key { 60 | return value, true 61 | } 62 | return nil, false 63 | }) 64 | return n() 65 | } 66 | } 67 | 68 | // Value will return the value associated with this node for key, or nil if there is none. 69 | // 70 | // See also Node.WithValue, as well as the value mechanism provided by the context package. 71 | func (n Node) Value(key interface{}) interface{} { 72 | if n != nil { 73 | valueCallMutex.Lock() 74 | defer valueCallMutex.Unlock() 75 | return n.valueSync(key) 76 | } 77 | return nil 78 | } 79 | 80 | // valueSync is split out into it's own method and the Node.valuePrep call is used as a discriminator for relevant 81 | // values 82 | func (n Node) valueSync(key interface{}) (value interface{}) { 83 | if n.valuePrep(key) { 84 | select { 85 | case value = <-valueDataChan: 86 | default: 87 | } 88 | valueDataMutex.Lock() 89 | valueDataKey = nil 90 | valueDataChan = nil 91 | valueDataMutex.Unlock() 92 | } 93 | return 94 | } 95 | 96 | func (n Node) valuePrep(key interface{}) bool { 97 | valueDataMutex.Lock() 98 | if runtimeCallers(2, valueDataCaller[:]) < 1 { 99 | valueDataMutex.Unlock() 100 | return false 101 | } 102 | valueDataKey = key 103 | valueDataChan = make(chan interface{}, 1) 104 | valueDataMutex.Unlock() 105 | n() 106 | return true 107 | } 108 | 109 | func (n Node) valueHandle(fn func(key interface{}) (interface{}, bool)) { 110 | valueDataMutex.RLock() 111 | dataKey, dataChan, dataCaller := valueDataKey, valueDataChan, valueDataCaller 112 | valueDataMutex.RUnlock() 113 | 114 | // fast exit case 1: there is no pending value operation 115 | if dataChan == nil { 116 | return 117 | } 118 | 119 | // fast exit case 2: pending value operation is not relevant 120 | value, ok := fn(dataKey) 121 | if !ok { 122 | return 123 | } 124 | dataKey = nil 125 | 126 | // slow case, may require walking the entire call stack 127 | const depth = 2 << 7 128 | callers := make([]uintptr, depth) 129 | for skip := 4; skip > 0; skip += depth { 130 | callers = callers[:runtimeCallers(skip, callers[:])] 131 | for _, caller := range callers { 132 | if caller == dataCaller[0] { 133 | select { 134 | case dataChan <- value: 135 | default: 136 | } 137 | return 138 | } 139 | } 140 | if len(callers) != depth { 141 | return 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /ticker.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "sync" 23 | "time" 24 | ) 25 | 26 | type ( 27 | // Ticker models a node runner 28 | Ticker interface { 29 | // Done will close when the ticker is fully stopped. 30 | Done() <-chan struct{} 31 | 32 | // Err will return any error that occurs. 33 | Err() error 34 | 35 | // Stop shutdown the ticker asynchronously. 36 | Stop() 37 | } 38 | 39 | // tickerCore is the base ticker implementation 40 | tickerCore struct { 41 | ctx context.Context 42 | cancel context.CancelFunc 43 | node Node 44 | ticker *time.Ticker 45 | done chan struct{} 46 | stop chan struct{} 47 | once sync.Once 48 | mutex sync.Mutex 49 | err error 50 | } 51 | 52 | // tickerStopOnFailure is an implementation of a ticker that will run until the first error 53 | tickerStopOnFailure struct { 54 | Ticker 55 | } 56 | ) 57 | 58 | var ( 59 | // errExitOnFailure is a specific error used internally to exit tickers constructed with NewTickerStopOnFailure, 60 | // and won't be returned by the tickerStopOnFailure implementation 61 | errExitOnFailure = errors.New("errExitOnFailure") 62 | ) 63 | 64 | // NewTicker constructs a new Ticker, which simply uses time.Ticker to tick the provided node periodically, note 65 | // that a panic will occur if ctx is nil, duration is <= 0, or node is nil. 66 | // 67 | // The node will tick until the first error or Ticker.Stop is called, or context is canceled, after which any error 68 | // will be made available via Ticker.Err, before closure of the done channel, indicating that all resources have been 69 | // freed, and any error is available. 70 | func NewTicker(ctx context.Context, duration time.Duration, node Node) Ticker { 71 | if ctx == nil { 72 | panic(errors.New("behaviortree.NewTicker nil context")) 73 | } 74 | 75 | if duration <= 0 { 76 | panic(errors.New("behaviortree.NewTicker duration <= 0")) 77 | } 78 | 79 | if node == nil { 80 | panic(errors.New("behaviortree.NewTicker nil node")) 81 | } 82 | 83 | result := &tickerCore{ 84 | node: node, 85 | ticker: time.NewTicker(duration), 86 | done: make(chan struct{}), 87 | stop: make(chan struct{}), 88 | } 89 | 90 | result.ctx, result.cancel = context.WithCancel(ctx) 91 | 92 | go result.run() 93 | 94 | return result 95 | } 96 | 97 | // NewTickerStopOnFailure returns a new Ticker that will exit on the first Failure, but won't return a non-nil Err 98 | // UNLESS there was an actual error returned, it's built on top of the same core implementation provided by NewTicker, 99 | // and uses that function directly, note that it will panic if the node is nil, the panic cases for NewTicker also 100 | // apply. 101 | func NewTickerStopOnFailure(ctx context.Context, duration time.Duration, node Node) Ticker { 102 | if node == nil { 103 | panic(errors.New("behaviortree.NewTickerStopOnFailure nil node")) 104 | } 105 | 106 | return tickerStopOnFailure{ 107 | Ticker: NewTicker( 108 | ctx, 109 | duration, 110 | func() (Tick, []Node) { 111 | tick, children := node() 112 | if tick == nil { 113 | return nil, children 114 | } 115 | return func(children []Node) (Status, error) { 116 | status, err := tick(children) 117 | if err == nil && status == Failure { 118 | err = errExitOnFailure 119 | } 120 | return status, err 121 | }, children 122 | }, 123 | ), 124 | } 125 | } 126 | 127 | func (t *tickerCore) run() { 128 | var err error 129 | TickLoop: 130 | for err == nil { 131 | select { 132 | case <-t.ctx.Done(): 133 | err = t.ctx.Err() 134 | break TickLoop 135 | case <-t.stop: 136 | break TickLoop 137 | case <-t.ticker.C: 138 | _, err = t.node.Tick() 139 | } 140 | } 141 | t.mutex.Lock() 142 | t.err = err 143 | t.mutex.Unlock() 144 | t.Stop() 145 | t.cancel() 146 | close(t.done) 147 | } 148 | 149 | func (t *tickerCore) Done() <-chan struct{} { 150 | return t.done 151 | } 152 | 153 | func (t *tickerCore) Err() error { 154 | t.mutex.Lock() 155 | defer t.mutex.Unlock() 156 | return t.err 157 | } 158 | 159 | func (t *tickerCore) Stop() { 160 | t.once.Do(func() { 161 | t.ticker.Stop() 162 | close(t.stop) 163 | }) 164 | } 165 | 166 | func (t tickerStopOnFailure) Err() error { 167 | err := t.Ticker.Err() 168 | if err == errExitOnFailure { 169 | return nil 170 | } 171 | return err 172 | } 173 | -------------------------------------------------------------------------------- /manager.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "errors" 21 | "github.com/joeycumines/go-bigbuff" 22 | "sync" 23 | ) 24 | 25 | type ( 26 | // Manager models an aggregate Ticker, which should stop gracefully on the first failure 27 | Manager interface { 28 | Ticker 29 | 30 | // Add will register a new ticker under this manager 31 | Add(ticker Ticker) error 32 | } 33 | 34 | // manager is this package's implementation of the Manager interface 35 | manager struct { 36 | mu sync.RWMutex 37 | once sync.Once 38 | worker bigbuff.Worker 39 | done chan struct{} 40 | stop chan struct{} 41 | tickers chan managerTicker 42 | errs []error 43 | } 44 | 45 | managerTicker struct { 46 | Ticker Ticker 47 | Done func() 48 | } 49 | 50 | errManagerTicker []error 51 | 52 | errManagerStopped struct{ error } 53 | ) 54 | 55 | var ( 56 | // ErrManagerStopped is returned by the manager implementation in this package (see also NewManager) in the case 57 | // that Manager.Add is attempted after the manager has already started to stop. Use errors.Is to check this case. 58 | ErrManagerStopped error = errManagerStopped{error: errors.New(`behaviortree.Manager.Add already stopped`)} 59 | ) 60 | 61 | // NewManager will construct an implementation of the Manager interface, which is a stateful set of Ticker 62 | // implementations, aggregating the behavior such that the Done channel will close when ALL tickers registered with Add 63 | // are done, Err will return a combined error if there are any, and Stop will stop all registered tickers. 64 | // 65 | // Note that any error (of any registered tickers) will also trigger stopping, and stopping will prevent further 66 | // Add calls from succeeding. 67 | // 68 | // As of v1.8.0, any (combined) ticker error returned by the Manager can now support error chaining (i.e. the use of 69 | // errors.Is). Note that errors.Unwrap isn't supported, since there may be more than one. See also Manager.Err and 70 | // Manager.Add. 71 | func NewManager() Manager { 72 | result := &manager{ 73 | done: make(chan struct{}), 74 | stop: make(chan struct{}), 75 | tickers: make(chan managerTicker), 76 | } 77 | return result 78 | } 79 | 80 | func (m *manager) Done() <-chan struct{} { 81 | return m.done 82 | } 83 | 84 | func (m *manager) Err() error { 85 | m.mu.RLock() 86 | defer m.mu.RUnlock() 87 | if len(m.errs) != 0 { 88 | return errManagerTicker(m.errs) 89 | } 90 | return nil 91 | } 92 | 93 | func (m *manager) Stop() { 94 | m.once.Do(func() { 95 | close(m.stop) 96 | m.start()() 97 | }) 98 | } 99 | 100 | func (m *manager) Add(ticker Ticker) error { 101 | if ticker == nil { 102 | return errors.New("behaviortree.Manager.Add nil ticker") 103 | } 104 | done := m.start() 105 | select { 106 | case <-m.stop: 107 | default: 108 | select { 109 | case <-m.stop: 110 | case m.tickers <- managerTicker{ 111 | Ticker: ticker, 112 | Done: done, 113 | }: 114 | return nil 115 | } 116 | } 117 | done() 118 | if err := m.Err(); err != nil { 119 | return errManagerStopped{error: err} 120 | } 121 | return ErrManagerStopped 122 | } 123 | 124 | func (m *manager) start() (done func()) { return m.worker.Do(m.run) } 125 | 126 | func (m *manager) run(stop <-chan struct{}) { 127 | for { 128 | select { 129 | case <-stop: 130 | select { 131 | case <-m.stop: 132 | select { 133 | case <-m.done: 134 | default: 135 | close(m.done) 136 | } 137 | default: 138 | } 139 | return 140 | case t := <-m.tickers: 141 | go m.handle(t) 142 | } 143 | } 144 | } 145 | 146 | func (m *manager) handle(t managerTicker) { 147 | select { 148 | case <-t.Ticker.Done(): 149 | // note: this stop shouldn't be necessary, but has been retained for 150 | // consistency, with the previous implementation) 151 | t.Ticker.Stop() 152 | case <-m.stop: 153 | t.Ticker.Stop() 154 | <-t.Ticker.Done() 155 | } 156 | if err := t.Ticker.Err(); err != nil { 157 | m.mu.Lock() 158 | m.errs = append(m.errs, err) 159 | m.mu.Unlock() 160 | m.Stop() 161 | } 162 | t.Done() 163 | } 164 | 165 | func (e errManagerTicker) Error() string { 166 | var b []byte 167 | for i, err := range e { 168 | if i != 0 { 169 | b = append(b, ' ', '|', ' ') 170 | } 171 | b = append(b, err.Error()...) 172 | } 173 | return string(b) 174 | } 175 | 176 | func (e errManagerTicker) Is(target error) bool { 177 | for _, err := range e { 178 | if errors.Is(err, target) { 179 | return true 180 | } 181 | } 182 | return false 183 | } 184 | 185 | func (e errManagerStopped) Unwrap() error { return e.error } 186 | 187 | func (e errManagerStopped) Is(target error) bool { 188 | switch target.(type) { 189 | case errManagerStopped: 190 | return true 191 | default: 192 | return false 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | joeycumines@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /selector_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "github.com/go-test/deep" 21 | "strings" 22 | "testing" 23 | ) 24 | 25 | func TestSelector_simple(t *testing.T) { 26 | out := make(chan int) 27 | var ( 28 | status Status 29 | err error 30 | ) 31 | 32 | var tree Node = func() (Tick, []Node) { 33 | return Selector, []Node{ 34 | func() (Tick, []Node) { 35 | return func(children []Node) (Status, error) { 36 | out <- 1 37 | return Success, nil 38 | }, nil 39 | }, 40 | func() (Tick, []Node) { 41 | return func(children []Node) (Status, error) { 42 | out <- 99 43 | return Success, nil 44 | }, nil 45 | }, 46 | func() (Tick, []Node) { 47 | return func(children []Node) (Status, error) { 48 | out <- 98 49 | return Success, nil 50 | }, nil 51 | }, 52 | } 53 | } 54 | 55 | go func() { 56 | status, err = tree.Tick() 57 | out <- 2 58 | }() 59 | 60 | expected := []int{1, 2} 61 | actual := []int{ 62 | <-out, 63 | <-out, 64 | } 65 | 66 | if diff := deep.Equal(expected, actual); diff != nil { 67 | t.Fatalf("expected tick order != actual: %s", strings.Join(diff, "\n >")) 68 | } 69 | 70 | if status != Success { 71 | t.Error("expected status to be success but it was", status) 72 | } 73 | 74 | if err != nil { 75 | t.Error("expected nil error but it was", err) 76 | } 77 | } 78 | 79 | func TestSelector_none(t *testing.T) { 80 | var ( 81 | status Status 82 | err error 83 | ) 84 | 85 | var tree Node = func() (Tick, []Node) { 86 | return Selector, nil 87 | } 88 | 89 | status, err = tree.Tick() 90 | 91 | if status != Failure { 92 | t.Error("expected status to be success but it was", status) 93 | } 94 | 95 | if err != nil { 96 | t.Error("expected nil error but it was", err) 97 | } 98 | } 99 | 100 | func TestSelector_error(t *testing.T) { 101 | out := make(chan int) 102 | var ( 103 | status Status 104 | err error 105 | ) 106 | 107 | var tree Node = func() (Tick, []Node) { 108 | return Selector, []Node{ 109 | nil, 110 | func() (Tick, []Node) { 111 | return func(children []Node) (Status, error) { 112 | out <- 99 113 | return Success, nil 114 | }, nil 115 | }, 116 | } 117 | } 118 | 119 | go func() { 120 | status, err = tree.Tick() 121 | out <- 1 122 | }() 123 | 124 | expected := []int{1} 125 | actual := []int{ 126 | <-out, 127 | } 128 | 129 | if diff := deep.Equal(expected, actual); diff != nil { 130 | t.Fatalf("expected tick order != actual: %s", strings.Join(diff, "\n >")) 131 | } 132 | 133 | if status != Failure { 134 | t.Error("expected status to be failure but it was", status) 135 | } 136 | 137 | if err == nil { 138 | t.Error("expected non-nil error but it was", err) 139 | } 140 | } 141 | func TestSelector_failure(t *testing.T) { 142 | out := make(chan int) 143 | var ( 144 | status Status 145 | err error 146 | ) 147 | 148 | var tree Node = func() (Tick, []Node) { 149 | return Selector, []Node{ 150 | func() (Tick, []Node) { 151 | return func(children []Node) (Status, error) { 152 | out <- 1 153 | return Failure, nil 154 | }, nil 155 | }, 156 | func() (Tick, []Node) { 157 | return func(children []Node) (Status, error) { 158 | out <- 2 159 | return Success, nil 160 | }, nil 161 | }, 162 | func() (Tick, []Node) { 163 | return func(children []Node) (Status, error) { 164 | out <- 99 165 | return Success, nil 166 | }, nil 167 | }, 168 | } 169 | } 170 | 171 | go func() { 172 | status, err = tree.Tick() 173 | out <- 3 174 | }() 175 | 176 | expected := []int{1, 2, 3} 177 | actual := []int{ 178 | <-out, 179 | <-out, 180 | <-out, 181 | } 182 | 183 | if diff := deep.Equal(expected, actual); diff != nil { 184 | t.Fatalf("expected tick order != actual: %s", strings.Join(diff, "\n >")) 185 | } 186 | 187 | if status != Success { 188 | t.Error("expected status to be success but it was", status) 189 | } 190 | 191 | if err != nil { 192 | t.Error("expected nil error but it was", err) 193 | } 194 | } 195 | 196 | func TestSelector_running(t *testing.T) { 197 | out := make(chan int) 198 | var ( 199 | status Status 200 | err error 201 | ) 202 | 203 | var tree Node = func() (Tick, []Node) { 204 | return Selector, []Node{ 205 | func() (Tick, []Node) { 206 | return func(children []Node) (Status, error) { 207 | out <- 1 208 | return Failure, nil 209 | }, nil 210 | }, 211 | func() (Tick, []Node) { 212 | return func(children []Node) (Status, error) { 213 | out <- 2 214 | return Running, nil 215 | }, nil 216 | }, 217 | func() (Tick, []Node) { 218 | return func(children []Node) (Status, error) { 219 | out <- 99 220 | return Success, nil 221 | }, nil 222 | }, 223 | } 224 | } 225 | 226 | go func() { 227 | status, err = tree.Tick() 228 | out <- 3 229 | }() 230 | 231 | expected := []int{1, 2, 3} 232 | actual := []int{ 233 | <-out, 234 | <-out, 235 | <-out, 236 | } 237 | 238 | if diff := deep.Equal(expected, actual); diff != nil { 239 | t.Fatalf("expected tick order != actual: %s", strings.Join(diff, "\n >")) 240 | } 241 | 242 | if status != Running { 243 | t.Error("expected status to be running but it was", status) 244 | } 245 | 246 | if err != nil { 247 | t.Error("expected nil error but it was", err) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /behaviortree_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "fmt" 21 | "github.com/go-test/deep" 22 | "strings" 23 | "testing" 24 | ) 25 | 26 | func TestNode_Tick_nil(t *testing.T) { 27 | var ( 28 | status Status 29 | err error 30 | ) 31 | 32 | var tree Node 33 | 34 | //noinspection GoNilness 35 | status, err = tree.Tick() 36 | 37 | if status != Failure { 38 | t.Error("expected status to be failure but it was", status) 39 | } 40 | 41 | if err == nil { 42 | t.Error("expected non-nil error but it was", err) 43 | } 44 | } 45 | 46 | func TestNode_Tick_nilTick(t *testing.T) { 47 | var ( 48 | status Status 49 | err error 50 | ) 51 | 52 | var tree Node = func() (Tick, []Node) { 53 | return nil, nil 54 | } 55 | 56 | status, err = tree.Tick() 57 | 58 | if status != Failure { 59 | t.Error("expected status to be failure but it was", status) 60 | } 61 | 62 | if err == nil { 63 | t.Error("expected non-nil error but it was", err) 64 | } 65 | } 66 | 67 | func TestNewNode(t *testing.T) { 68 | out := make(chan int) 69 | var ( 70 | status Status 71 | err error 72 | ) 73 | 74 | tree := NewNode( 75 | Sequence, 76 | []Node{ 77 | NewNode( 78 | func(children []Node) (Status, error) { 79 | out <- 1 80 | return Success, nil 81 | }, 82 | nil, 83 | ), 84 | NewNode( 85 | func(children []Node) (Status, error) { 86 | out <- 2 87 | return Success, nil 88 | }, 89 | nil, 90 | ), 91 | NewNode( 92 | func(children []Node) (Status, error) { 93 | out <- 3 94 | return Success, nil 95 | }, 96 | nil, 97 | ), 98 | }, 99 | ) 100 | 101 | go func() { 102 | status, err = tree.Tick() 103 | out <- 4 104 | }() 105 | 106 | expected := []int{1, 2, 3, 4} 107 | actual := []int{ 108 | <-out, 109 | <-out, 110 | <-out, 111 | <-out, 112 | } 113 | 114 | if diff := deep.Equal(expected, actual); diff != nil { 115 | t.Fatalf("expected tick order != actual: %s", strings.Join(diff, "\n >")) 116 | } 117 | 118 | if status != Success { 119 | t.Error("expected status to be success but it was", status) 120 | } 121 | 122 | if err != nil { 123 | t.Error("expected nil error but it was", err) 124 | } 125 | } 126 | 127 | func TestStatus_String(t *testing.T) { 128 | testCases := []struct { 129 | Status Status 130 | String string 131 | }{ 132 | { 133 | Status: Success, 134 | String: `success`, 135 | }, 136 | { 137 | Status: Failure, 138 | String: `failure`, 139 | }, 140 | { 141 | Status: Running, 142 | String: `running`, 143 | }, 144 | { 145 | Status: 0, 146 | String: `unknown status (0)`, 147 | }, 148 | { 149 | Status: 234, 150 | String: `unknown status (234)`, 151 | }, 152 | } 153 | 154 | for i, testCase := range testCases { 155 | name := fmt.Sprintf("TestStatus_String_#%d", i) 156 | 157 | if actual := testCase.Status.String(); actual != testCase.String { 158 | t.Errorf("%s failed: expected stringer '%s' != actual '%s'", name, testCase.String, actual) 159 | } 160 | } 161 | } 162 | 163 | func TestStatus_Status(t *testing.T) { 164 | testCases := []struct { 165 | Status Status 166 | Expected Status 167 | }{ 168 | { 169 | Status: Success, 170 | Expected: Success, 171 | }, 172 | { 173 | Status: Failure, 174 | Expected: Failure, 175 | }, 176 | { 177 | Status: Running, 178 | Expected: Running, 179 | }, 180 | { 181 | Status: 0, 182 | Expected: Failure, 183 | }, 184 | { 185 | Status: 234, 186 | Expected: Failure, 187 | }, 188 | } 189 | 190 | for i, testCase := range testCases { 191 | name := fmt.Sprintf("TestStatus_Status_#%d", i) 192 | 193 | if actual := testCase.Status.Status(); actual != testCase.Expected { 194 | t.Errorf("%s failed: expected behaviortree.Status.Status '%s' != actual '%s'", name, testCase.Status, actual) 195 | } 196 | } 197 | } 198 | 199 | func genTestNewFrame() *Frame { return New(Sequence).Frame() } 200 | 201 | func TestNew_frame(t *testing.T) { 202 | var ( 203 | expected = newFrame(genTestNewFrame) 204 | actual = genTestNewFrame() 205 | ) 206 | if expected == nil || actual == nil || expected.PC == 0 || actual.PC == 0 || expected.PC != expected.Entry || actual.PC == actual.Entry { 207 | t.Fatal(expected, actual) 208 | } 209 | actual.PC = actual.Entry 210 | if *expected != *actual { 211 | t.Errorf("expected != actual\nEXPECTED: %#v\nACTUAL: %#v", expected, actual) 212 | } 213 | } 214 | 215 | func genTestNewNodeFrame() *Frame { return NewNode(Sequence, nil).Frame() } 216 | 217 | func TestNewNode_frame(t *testing.T) { 218 | var ( 219 | expected = newFrame(genTestNewNodeFrame) 220 | actual = genTestNewNodeFrame() 221 | ) 222 | if expected == nil || actual == nil || expected.PC == 0 || actual.PC == 0 || expected.PC != expected.Entry || actual.PC == actual.Entry { 223 | t.Fatal(expected, actual) 224 | } 225 | actual.PC = actual.Entry 226 | if *expected != *actual { 227 | t.Errorf("expected != actual\nEXPECTED: %#v\nACTUAL: %#v", expected, actual) 228 | } 229 | } 230 | 231 | func Test_factory_nilFrame(t *testing.T) { 232 | if v := New(nil).Value(vkFrame{}); v == nil { 233 | t.Error(v) 234 | } 235 | if v := New(nil).Value(1); v != nil { 236 | t.Error(v) 237 | } 238 | if v := NewNode(nil, nil).Value(vkFrame{}); v == nil { 239 | t.Error(v) 240 | } 241 | if v := NewNode(nil, nil).Value(1); v != nil { 242 | t.Error(v) 243 | } 244 | defer func() func() { 245 | old := runtimeCallers 246 | runtimeCallers = func(skip int, pc []uintptr) int { return 0 } 247 | return func() { 248 | runtimeCallers = old 249 | } 250 | }()() 251 | if v := New(nil).Value(vkFrame{}); v != nil { 252 | t.Error(v) 253 | } 254 | if v := New(nil).Value(1); v != nil { 255 | t.Error(v) 256 | } 257 | if v := NewNode(nil, nil).Value(vkFrame{}); v != nil { 258 | t.Error(v) 259 | } 260 | if v := NewNode(nil, nil).Value(1); v != nil { 261 | t.Error(v) 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /printer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "github.com/xlab/treeprint" 23 | "io" 24 | "strings" 25 | ) 26 | 27 | type ( 28 | // Printer models something providing behavior tree printing capabilities 29 | Printer interface { 30 | // Fprint writes a representation node to output 31 | Fprint(output io.Writer, node Node) error 32 | } 33 | 34 | // TreePrinter provides a generalised implementation of Printer used as the DefaultPrinter 35 | TreePrinter struct { 36 | // Inspector configures the meta and value for a node with a given tick 37 | Inspector func(node Node, tick Tick) (meta []interface{}, value interface{}) 38 | // Formatter initialises a new printer tree and returns it as a TreePrinterNode 39 | Formatter func() TreePrinterNode 40 | } 41 | 42 | // TreePrinterNode models a BT node for printing and is used by the TreePrinter implementation in this package 43 | TreePrinterNode interface { 44 | // Add should wire up a new node to the receiver then return it 45 | Add(meta []interface{}, value interface{}) TreePrinterNode 46 | // Bytes should encode the node and all children in preparation for use within TreePrinter 47 | Bytes() []byte 48 | } 49 | ) 50 | 51 | var ( 52 | // DefaultPrinter is used to implement Node.String 53 | DefaultPrinter Printer = TreePrinter{ 54 | Inspector: DefaultPrinterInspector, 55 | Formatter: DefaultPrinterFormatter, 56 | } 57 | ) 58 | 59 | // String implements fmt.Stringer using DefaultPrinter 60 | func (n Node) String() string { 61 | var b bytes.Buffer 62 | if err := DefaultPrinter.Fprint(&b, n); err != nil { 63 | return fmt.Sprintf(`behaviortree.DefaultPrinter error: %s`, err) 64 | } 65 | return b.String() 66 | } 67 | 68 | // DefaultPrinterFormatter is used by DefaultPrinter 69 | func DefaultPrinterFormatter() TreePrinterNode { return new(treePrinterNodeXlab) } 70 | 71 | // DefaultPrinterInspector is used by DefaultPrinter 72 | func DefaultPrinterInspector(node Node, tick Tick) ([]interface{}, interface{}) { 73 | var ( 74 | nodePtr uintptr 75 | nodeFileLine string 76 | nodeName string 77 | tickPtr uintptr 78 | tickFileLine string 79 | tickName string 80 | ) 81 | 82 | if v := node.Frame(); v != nil { 83 | nodePtr = v.PC 84 | nodeFileLine = shortFileLine(v.File, v.Line) 85 | nodeName = v.Function 86 | } else if node == nil { 87 | nodeName = `` 88 | } 89 | if nodeFileLine == `` { 90 | nodeFileLine = `-` 91 | } 92 | if nodeName == `` { 93 | nodeName = `-` 94 | } 95 | 96 | if v := tick.Frame(); v != nil { 97 | tickPtr = v.PC 98 | tickFileLine = shortFileLine(v.File, v.Line) 99 | tickName = v.Function 100 | } else if tick == nil { 101 | tickName = `` 102 | } 103 | if tickFileLine == `` { 104 | tickFileLine = `-` 105 | } 106 | if tickName == `` { 107 | tickName = `-` 108 | } 109 | 110 | return []interface{}{ 111 | fmt.Sprintf(`%#x`, nodePtr), 112 | nodeFileLine, 113 | fmt.Sprintf(`%#x`, tickPtr), 114 | tickFileLine, 115 | }, fmt.Sprintf(`%s | %s`, nodeName, tickName) 116 | } 117 | 118 | // Fprint implements Printer.Fprint 119 | func (p TreePrinter) Fprint(output io.Writer, node Node) error { 120 | tree := p.Formatter() 121 | p.build(tree, node) 122 | if _, err := io.Copy(output, bytes.NewReader(tree.Bytes())); err != nil { 123 | return err 124 | } 125 | return nil 126 | } 127 | 128 | func (p TreePrinter) build(tree TreePrinterNode, node Node) { 129 | if node != nil { 130 | tick, children := node() 131 | tree = tree.Add(p.Inspector(node, tick)) 132 | for _, child := range children { 133 | p.build(tree, child) 134 | } 135 | } 136 | } 137 | 138 | func shortFileLine(f string, l int) string { 139 | if i := strings.LastIndex(f, "/"); i >= 0 { 140 | f = f[i+1:] 141 | } 142 | return fmt.Sprintf(`%s:%d`, f, l) 143 | } 144 | 145 | type ( 146 | treePrinterNodeXlab struct { 147 | node treeprint.Tree 148 | sizes []int 149 | updates []func() 150 | } 151 | treePrinterNodeXlabMeta struct { 152 | *treePrinterNodeXlab 153 | interfaces []interface{} 154 | strings []string 155 | } 156 | ) 157 | 158 | func (n *treePrinterNodeXlab) Add(meta []interface{}, value interface{}) TreePrinterNode { 159 | if n.node == nil { 160 | r := new(treePrinterNodeXlab) 161 | m := &treePrinterNodeXlabMeta{treePrinterNodeXlab: r, interfaces: meta} 162 | m.updates = append(m.updates, m.update) 163 | n.node = treeprint.New() 164 | n.node.SetMetaValue(m) 165 | n.node.SetValue(value) 166 | return n 167 | } 168 | m := &treePrinterNodeXlabMeta{treePrinterNodeXlab: n, interfaces: meta} 169 | m.updates = append(m.updates, m.update) 170 | return &treePrinterNodeXlab{node: n.node.AddMetaBranch(m, value)} 171 | } 172 | func (n *treePrinterNodeXlab) Bytes() []byte { 173 | if n := n.node; n != nil { 174 | b := n.Bytes() 175 | if l := len(b); l != 0 && b[l-1] == '\n' { 176 | b = b[:l-1] 177 | } 178 | return b 179 | } 180 | return []byte(``) 181 | } 182 | func (m *treePrinterNodeXlabMeta) String() string { 183 | const space = ' ' 184 | for _, update := range m.updates { 185 | update() 186 | } 187 | m.updates = nil 188 | if m.interfaces != nil { 189 | panic(fmt.Errorf(`m.interfaces %v should be nil`, m.interfaces)) 190 | } 191 | if len(m.sizes) < len(m.strings) { 192 | panic(fmt.Errorf(`m.sizes %v mismatched m.strings %v`, m.sizes, m.strings)) 193 | } 194 | var b []byte 195 | for i, size := range m.sizes { 196 | if i != 0 { 197 | b = append(b, space) 198 | } 199 | if i < len(m.strings) { 200 | b = append(b, m.strings[i]...) 201 | size -= len(m.strings[i]) 202 | } 203 | b = append(b, bytes.Repeat([]byte{space}, size)...) 204 | } 205 | return string(b) 206 | } 207 | func (m *treePrinterNodeXlabMeta) update() { 208 | m.strings = make([]string, len(m.interfaces)) 209 | for i, v := range m.interfaces { 210 | m.strings[i] = fmt.Sprint(v) 211 | if i == len(m.sizes) { 212 | m.sizes = append(m.sizes, 0) 213 | } 214 | if v := len(m.strings[i]); v > m.sizes[i] { 215 | m.sizes[i] = v 216 | } 217 | } 218 | m.interfaces = nil 219 | } 220 | -------------------------------------------------------------------------------- /sequence_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "github.com/go-test/deep" 21 | "strings" 22 | "testing" 23 | ) 24 | 25 | func TestSequence_simple(t *testing.T) { 26 | out := make(chan int) 27 | var ( 28 | status Status 29 | err error 30 | ) 31 | 32 | var tree Node = func() (Tick, []Node) { 33 | return Sequence, []Node{ 34 | func() (Tick, []Node) { 35 | return func(children []Node) (Status, error) { 36 | out <- 1 37 | return Success, nil 38 | }, nil 39 | }, 40 | func() (Tick, []Node) { 41 | return func(children []Node) (Status, error) { 42 | out <- 2 43 | return Success, nil 44 | }, nil 45 | }, 46 | func() (Tick, []Node) { 47 | return func(children []Node) (Status, error) { 48 | out <- 3 49 | return Success, nil 50 | }, nil 51 | }, 52 | } 53 | } 54 | 55 | go func() { 56 | status, err = tree.Tick() 57 | out <- 4 58 | }() 59 | 60 | expected := []int{1, 2, 3, 4} 61 | actual := []int{ 62 | <-out, 63 | <-out, 64 | <-out, 65 | <-out, 66 | } 67 | 68 | if diff := deep.Equal(expected, actual); diff != nil { 69 | t.Fatalf("expected tick order != actual: %s", strings.Join(diff, "\n >")) 70 | } 71 | 72 | if status != Success { 73 | t.Error("expected status to be success but it was", status) 74 | } 75 | 76 | if err != nil { 77 | t.Error("expected nil error but it was", err) 78 | } 79 | } 80 | 81 | func TestSequence_none(t *testing.T) { 82 | var ( 83 | status Status 84 | err error 85 | ) 86 | 87 | var tree Node = func() (Tick, []Node) { 88 | return Sequence, nil 89 | } 90 | 91 | status, err = tree.Tick() 92 | 93 | if status != Success { 94 | t.Error("expected status to be success but it was", status) 95 | } 96 | 97 | if err != nil { 98 | t.Error("expected nil error but it was", err) 99 | } 100 | } 101 | 102 | func TestSequence_error(t *testing.T) { 103 | out := make(chan int) 104 | var ( 105 | status Status 106 | err error 107 | ) 108 | 109 | // errors on the nil 110 | var tree Node = func() (Tick, []Node) { 111 | return Sequence, []Node{ 112 | func() (Tick, []Node) { 113 | return func(children []Node) (Status, error) { 114 | out <- 1 115 | return Success, nil 116 | }, nil 117 | }, 118 | nil, 119 | func() (Tick, []Node) { 120 | return func(children []Node) (Status, error) { 121 | out <- 99 122 | return Success, nil 123 | }, nil 124 | }, 125 | } 126 | } 127 | 128 | go func() { 129 | status, err = tree.Tick() 130 | out <- 2 131 | }() 132 | 133 | expected := []int{1, 2} 134 | actual := []int{ 135 | <-out, 136 | <-out, 137 | } 138 | 139 | if diff := deep.Equal(expected, actual); diff != nil { 140 | t.Fatalf("expected tick order != actual: %s", strings.Join(diff, "\n >")) 141 | } 142 | 143 | if status != Failure { 144 | t.Error("expected status to be failure but it was", status) 145 | } 146 | 147 | if err == nil { 148 | t.Error("expected non-nil error but it was", err) 149 | } 150 | } 151 | 152 | func TestSequence_failure(t *testing.T) { 153 | out := make(chan int) 154 | var ( 155 | status Status 156 | err error 157 | ) 158 | 159 | var tree Node = func() (Tick, []Node) { 160 | return Sequence, []Node{ 161 | func() (Tick, []Node) { 162 | return func(children []Node) (Status, error) { 163 | out <- 1 164 | return Success, nil 165 | }, nil 166 | }, 167 | func() (Tick, []Node) { 168 | return func(children []Node) (Status, error) { 169 | out <- 2 170 | return Failure, nil 171 | }, nil 172 | }, 173 | func() (Tick, []Node) { 174 | return func(children []Node) (Status, error) { 175 | out <- 99 176 | return Success, nil 177 | }, nil 178 | }, 179 | } 180 | } 181 | 182 | go func() { 183 | status, err = tree.Tick() 184 | out <- 3 185 | }() 186 | 187 | expected := []int{1, 2, 3} 188 | actual := []int{ 189 | <-out, 190 | <-out, 191 | <-out, 192 | } 193 | 194 | if diff := deep.Equal(expected, actual); diff != nil { 195 | t.Fatalf("expected tick order != actual: %s", strings.Join(diff, "\n >")) 196 | } 197 | 198 | if status != Failure { 199 | t.Error("expected status to be failure but it was", status) 200 | } 201 | 202 | if err != nil { 203 | t.Error("expected nil error but it was", err) 204 | } 205 | } 206 | 207 | func TestSequence_running(t *testing.T) { 208 | out := make(chan int) 209 | var ( 210 | status Status 211 | err error 212 | ) 213 | 214 | var tree Node = func() (Tick, []Node) { 215 | return Sequence, []Node{ 216 | func() (Tick, []Node) { 217 | return func(children []Node) (Status, error) { 218 | out <- 1 219 | return Success, nil 220 | }, nil 221 | }, 222 | func() (Tick, []Node) { 223 | return func(children []Node) (Status, error) { 224 | out <- 2 225 | return Running, nil 226 | }, nil 227 | }, 228 | func() (Tick, []Node) { 229 | return func(children []Node) (Status, error) { 230 | out <- 99 231 | return Success, nil 232 | }, nil 233 | }, 234 | } 235 | } 236 | 237 | go func() { 238 | status, err = tree.Tick() 239 | out <- 3 240 | }() 241 | 242 | expected := []int{1, 2, 3} 243 | actual := []int{ 244 | <-out, 245 | <-out, 246 | <-out, 247 | } 248 | 249 | if diff := deep.Equal(expected, actual); diff != nil { 250 | t.Fatalf("expected tick order != actual: %s", strings.Join(diff, "\n >")) 251 | } 252 | 253 | if status != Running { 254 | t.Error("expected status to be running but it was", status) 255 | } 256 | 257 | if err != nil { 258 | t.Error("expected nil error but it was", err) 259 | } 260 | } 261 | 262 | func TestSequence_invalidStatus(t *testing.T) { 263 | var ( 264 | output []int 265 | children []Node 266 | ) 267 | for i := 0; i < 6; i++ { 268 | i := i 269 | children = append(children, New(func([]Node) (Status, error) { 270 | output = append(output, i) 271 | if i == 3 { 272 | return 0, nil 273 | } 274 | return Success, nil 275 | })) 276 | } 277 | if status, err := NewNode(Sequence, children).Tick(); err != nil || status != Failure { 278 | t.Error(status, err) 279 | } 280 | if diff := deep.Equal(output, []int{0, 1, 2, 3}); diff != nil { 281 | t.Errorf("%v\n%s", output, strings.Join(diff, "\n")) 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /any_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "testing" 23 | "time" 24 | ) 25 | 26 | func ExampleAny_sequencePartialSuccess() { 27 | fmt.Println(New( 28 | Any(Sequence), 29 | New(func(children []Node) (Status, error) { 30 | fmt.Println(1) 31 | return Success, nil 32 | }), 33 | New(func(children []Node) (Status, error) { 34 | fmt.Println(2) 35 | return Success, nil 36 | }), 37 | New(func(children []Node) (Status, error) { 38 | fmt.Println(3) 39 | return Success, nil 40 | }), 41 | New(func(children []Node) (Status, error) { 42 | fmt.Println(4) 43 | return Success, nil 44 | }), 45 | New(func(children []Node) (Status, error) { 46 | fmt.Println(5) 47 | return Failure, nil 48 | }), 49 | New(func(children []Node) (Status, error) { 50 | panic(`wont reach here`) 51 | }), 52 | ).Tick()) 53 | //output: 54 | //1 55 | //2 56 | //3 57 | //4 58 | //5 59 | //success 60 | } 61 | 62 | func ExampleAny_allPartialSuccess() { 63 | fmt.Println(New( 64 | Any(All), 65 | New(func(children []Node) (Status, error) { 66 | fmt.Println(1) 67 | return Success, nil 68 | }), 69 | New(func(children []Node) (Status, error) { 70 | fmt.Println(2) 71 | return Success, nil 72 | }), 73 | New(func(children []Node) (Status, error) { 74 | fmt.Println(3) 75 | return Success, nil 76 | }), 77 | New(func(children []Node) (Status, error) { 78 | fmt.Println(4) 79 | return Success, nil 80 | }), 81 | New(func(children []Node) (Status, error) { 82 | fmt.Println(5) 83 | return Failure, nil 84 | }), 85 | New(func(children []Node) (Status, error) { 86 | fmt.Println(6) 87 | return Success, nil 88 | }), 89 | ).Tick()) 90 | //output: 91 | //1 92 | //2 93 | //3 94 | //4 95 | //5 96 | //6 97 | //success 98 | } 99 | 100 | func ExampleAny_resetBehavior() { 101 | var ( 102 | status Status 103 | err error 104 | node = New( 105 | Any(Sequence), 106 | New(func(children []Node) (Status, error) { 107 | fmt.Println(1) 108 | return status, err 109 | }), 110 | New(func(children []Node) (Status, error) { 111 | fmt.Println(2) 112 | return Success, nil 113 | }), 114 | ) 115 | ) 116 | 117 | status = Success 118 | err = nil 119 | fmt.Println(node.Tick()) 120 | 121 | status = Failure 122 | err = nil 123 | fmt.Println(node.Tick()) 124 | 125 | status = Success 126 | err = errors.New(`some_error`) 127 | fmt.Println(node.Tick()) 128 | 129 | status = Success 130 | err = nil 131 | fmt.Println(node.Tick()) 132 | 133 | //output: 134 | //1 135 | //2 136 | //success 137 | //1 138 | //failure 139 | //1 140 | //failure some_error 141 | //1 142 | //2 143 | //success 144 | } 145 | 146 | func ExampleAny_running() { 147 | status := Running 148 | node := New( 149 | Any(All), 150 | New(func(children []Node) (Status, error) { 151 | fmt.Printf("child ticked: %s\n", status) 152 | return status, nil 153 | }), 154 | ) 155 | fmt.Println(node.Tick()) 156 | status = Failure 157 | fmt.Println(node.Tick()) 158 | status = Running 159 | fmt.Println(node.Tick()) 160 | status = Success 161 | fmt.Println(node.Tick()) 162 | //output: 163 | //child ticked: running 164 | //running 165 | //child ticked: failure 166 | //failure 167 | //child ticked: running 168 | //running 169 | //child ticked: success 170 | //success 171 | } 172 | 173 | func ExampleAny_forkPartialSuccess() { 174 | var ( 175 | c1 = make(chan struct{}) 176 | c2 = make(chan struct{}) 177 | c3 = make(chan struct{}) 178 | c4 = make(chan struct{}) 179 | c5 = make(chan struct{}) 180 | c6 = make(chan struct{}) 181 | status = Running 182 | ) 183 | go func() { 184 | time.Sleep(time.Millisecond * 100) 185 | fmt.Println(`unblocking the forked nodes`) 186 | close(c1) 187 | time.Sleep(time.Millisecond * 100) 188 | close(c2) 189 | time.Sleep(time.Millisecond * 100) 190 | close(c3) 191 | time.Sleep(time.Millisecond * 100) 192 | close(c4) 193 | time.Sleep(time.Millisecond * 100) 194 | close(c5) 195 | time.Sleep(time.Millisecond * 100) 196 | close(c6) 197 | }() 198 | node := New( 199 | Any(Fork()), 200 | New(func(children []Node) (Status, error) { 201 | fmt.Println(`ready`) 202 | <-c1 203 | fmt.Println(1) 204 | return Success, nil 205 | }), 206 | New(func(children []Node) (Status, error) { 207 | fmt.Println(`ready`) 208 | <-c2 209 | fmt.Println(2) 210 | return Success, nil 211 | }), 212 | New(func(children []Node) (Status, error) { 213 | fmt.Println(`ready`) 214 | <-c3 215 | fmt.Println(3) 216 | return status, nil 217 | }), 218 | New(func(children []Node) (Status, error) { 219 | fmt.Println(`ready`) 220 | <-c4 221 | fmt.Println(4) 222 | return Failure, nil 223 | }), 224 | New(func(children []Node) (Status, error) { 225 | fmt.Println(`ready`) 226 | <-c5 227 | fmt.Println(5) 228 | return Failure, nil 229 | }), 230 | New(func(children []Node) (Status, error) { 231 | fmt.Println(`ready`) 232 | <-c6 233 | fmt.Println(6) 234 | return Success, nil 235 | }), 236 | ) 237 | fmt.Println(node.Tick()) 238 | fmt.Println(`same running behavior as Fork`) 239 | fmt.Println(node.Tick()) 240 | fmt.Println(`but the exit status is overridden`) 241 | status = Failure 242 | fmt.Println(node.Tick()) 243 | //output: 244 | //ready 245 | //ready 246 | //ready 247 | //ready 248 | //ready 249 | //ready 250 | //unblocking the forked nodes 251 | //1 252 | //2 253 | //3 254 | //4 255 | //5 256 | //6 257 | //running 258 | //same running behavior as Fork 259 | //ready 260 | //3 261 | //running 262 | //but the exit status is overridden 263 | //ready 264 | //3 265 | //success 266 | } 267 | 268 | func TestAny_nilTick(t *testing.T) { 269 | if v := Any(nil); v != nil { 270 | t.Error(v) 271 | } 272 | } 273 | 274 | func TestAny_nilNode(t *testing.T) { 275 | if v, err := Any(Sequence)([]Node{nil}); err == nil || v != Failure { 276 | t.Error(v, err) 277 | } 278 | } 279 | 280 | func TestAny_nilChildTick(t *testing.T) { 281 | status, err := New(Any(Sequence), New(nil)).Tick() 282 | if status != Failure { 283 | t.Error(status) 284 | } 285 | if err == nil || err.Error() != `behaviortree.Node cannot tick a node with a nil tick` { 286 | t.Error(err) 287 | } 288 | } 289 | 290 | func TestAny_noChildren(t *testing.T) { 291 | if status, err := New(Any(Sequence)).Tick(); err != nil || status != Failure { 292 | t.Error(status, err) 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /fork_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "testing" 23 | "time" 24 | ) 25 | 26 | func TestFork_success(t *testing.T) { 27 | defer checkNumGoroutines(t)(false, 0) 28 | 29 | var ( 30 | out = make(chan string) 31 | prefix = `1.` 32 | node = New( 33 | Fork(), 34 | New(Async(func(children []Node) (Status, error) { 35 | out <- prefix + `1` 36 | return Success, nil 37 | })), 38 | New(Async(func(children []Node) (Status, error) { 39 | out <- prefix + `2` 40 | return Success, nil 41 | })), 42 | New(Async(func(children []Node) (Status, error) { 43 | out <- prefix + `3` 44 | return Success, nil 45 | })), 46 | ) 47 | seen = make(map[string]struct{}) 48 | tick = func() Status { 49 | select { 50 | case value := <-out: 51 | if _, ok := seen[value]; ok { 52 | t.Fatal(value) 53 | } 54 | seen[value] = struct{}{} 55 | default: 56 | } 57 | time.Sleep(time.Millisecond * 100) 58 | status, err := node.Tick() 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | return status 63 | } 64 | ) 65 | 66 | for x := 1; x <= 3; x++ { 67 | seen = make(map[string]struct{}) 68 | prefix = fmt.Sprintf("%d.", x) 69 | time.Sleep(time.Millisecond * 100) 70 | select { 71 | case value := <-out: 72 | t.Fatal(value) 73 | default: 74 | } 75 | if status, err := node.Tick(); err != nil || status != Running { 76 | t.Fatal(status, err) 77 | } 78 | time.Sleep(time.Millisecond * 100) 79 | if status := tick(); status != Running { 80 | t.Fatal(status, seen) 81 | } 82 | if status := tick(); status != Running { 83 | t.Fatal(status, seen) 84 | } 85 | if status := tick(); status != Success { 86 | t.Fatal(status, seen) 87 | } 88 | if len(seen) != 3 { 89 | t.Fatal(seen) 90 | } 91 | for y := 1; y <= 3; y++ { 92 | if _, ok := seen[fmt.Sprintf("%d.%d", x, y)]; !ok { 93 | t.Fatal(seen) 94 | } 95 | } 96 | } 97 | } 98 | 99 | func TestFork_failure(t *testing.T) { 100 | defer checkNumGoroutines(t)(false, 0) 101 | 102 | var ( 103 | out = make(chan string) 104 | prefix = `1.` 105 | node = New( 106 | Fork(), 107 | New(Async(func(children []Node) (Status, error) { 108 | out <- prefix + `1` 109 | return Success, nil 110 | })), 111 | New(Async(func(children []Node) (Status, error) { 112 | out <- prefix + `2` 113 | return Success, nil 114 | })), 115 | New(func(children []Node) (Status, error) { 116 | return 0, errors.New(`error_one`) 117 | }), 118 | New(Async(func(children []Node) (Status, error) { 119 | out <- prefix + `3` 120 | return Success, errors.New(`error_two`) 121 | })), 122 | ) 123 | seen = make(map[string]struct{}) 124 | tick = func() Status { 125 | select { 126 | case value := <-out: 127 | if _, ok := seen[value]; ok { 128 | t.Fatal(value) 129 | } 130 | seen[value] = struct{}{} 131 | default: 132 | } 133 | time.Sleep(time.Millisecond * 100) 134 | status, err := node.Tick() 135 | if status == Running { 136 | if err != nil { 137 | t.Fatal(err) 138 | } 139 | } else { 140 | if err == nil || err.Error() != `error_one | error_two` { 141 | t.Fatal(err) 142 | } 143 | } 144 | return status 145 | } 146 | ) 147 | 148 | for x := 0; x < 3; x++ { 149 | seen = make(map[string]struct{}) 150 | time.Sleep(time.Millisecond * 100) 151 | select { 152 | case value := <-out: 153 | t.Fatal(value) 154 | default: 155 | } 156 | if status, err := node.Tick(); err != nil || status != Running { 157 | t.Fatal(status, err) 158 | } 159 | time.Sleep(time.Millisecond * 100) 160 | if status := tick(); status != Running { 161 | t.Fatal(status, seen) 162 | } 163 | if status := tick(); status != Running { 164 | t.Fatal(status, seen) 165 | } 166 | if status := tick(); status != Failure { 167 | t.Fatal(status, seen) 168 | } 169 | } 170 | } 171 | 172 | func TestFork_noChildren(t *testing.T) { 173 | node := New(Fork()) 174 | if status, err := node.Tick(); status != Success || err != nil { 175 | t.Fatal(status, err) 176 | } 177 | if status, err := node.Tick(); status != Success || err != nil { 178 | t.Fatal(status, err) 179 | } 180 | if status, err := node.Tick(); status != Success || err != nil { 181 | t.Fatal(status, err) 182 | } 183 | } 184 | 185 | func TestFork_immediateSuccess(t *testing.T) { 186 | node := New( 187 | Fork(), 188 | New(func(children []Node) (Status, error) { 189 | return Success, nil 190 | }), 191 | New(func(children []Node) (Status, error) { 192 | return Success, nil 193 | }), 194 | New(func(children []Node) (Status, error) { 195 | return Success, nil 196 | }), 197 | ) 198 | if status, err := node.Tick(); status != Success || err != nil { 199 | t.Fatal(status, err) 200 | } 201 | if status, err := node.Tick(); status != Success || err != nil { 202 | t.Fatal(status, err) 203 | } 204 | if status, err := node.Tick(); status != Success || err != nil { 205 | t.Fatal(status, err) 206 | } 207 | } 208 | 209 | func TestFork_immediateFailure(t *testing.T) { 210 | node := New( 211 | Fork(), 212 | New(func(children []Node) (Status, error) { 213 | return Success, nil 214 | }), 215 | New(func(children []Node) (Status, error) { 216 | return 0, nil 217 | }), 218 | New(func(children []Node) (Status, error) { 219 | return Success, nil 220 | }), 221 | ) 222 | if status, err := node.Tick(); status != Failure || err != nil { 223 | t.Fatal(status, err) 224 | } 225 | if status, err := node.Tick(); status != Failure || err != nil { 226 | t.Fatal(status, err) 227 | } 228 | if status, err := node.Tick(); status != Failure || err != nil { 229 | t.Fatal(status, err) 230 | } 231 | } 232 | 233 | func TestFork_immediateError(t *testing.T) { 234 | node := New( 235 | Fork(), 236 | New(func(children []Node) (Status, error) { 237 | return Success, errors.New(`err`) 238 | }), 239 | New(func(children []Node) (Status, error) { 240 | return Success, errors.New(`err`) 241 | }), 242 | New(func(children []Node) (Status, error) { 243 | return Success, errors.New(`err`) 244 | }), 245 | ) 246 | if status, err := node.Tick(); status != Failure || err == nil || err.Error() != `err | err | err` { 247 | t.Fatal(status, err) 248 | } 249 | if status, err := node.Tick(); status != Failure || err == nil || err.Error() != `err | err | err` { 250 | t.Fatal(status, err) 251 | } 252 | if status, err := node.Tick(); status != Failure || err == nil || err.Error() != `err | err | err` { 253 | t.Fatal(status, err) 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /background_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "testing" 23 | "time" 24 | ) 25 | 26 | func ExampleBackground_success() { 27 | defer checkNumGoroutines(nil)(false, 0) 28 | node := func() Node { 29 | tick := Background(Fork) 30 | return func() (Tick, []Node) { 31 | return tick, []Node{ 32 | New(func(children []Node) (Status, error) { 33 | fmt.Println(`start fork`) 34 | return Success, nil 35 | }), 36 | New(Async(func(children []Node) (Status, error) { 37 | time.Sleep(time.Millisecond * 100) 38 | return Success, nil 39 | })), 40 | New(Async(func(children []Node) (Status, error) { 41 | time.Sleep(time.Millisecond * 200) 42 | return Success, nil 43 | })), 44 | New(Async(func(children []Node) (Status, error) { 45 | time.Sleep(time.Millisecond * 300) 46 | fmt.Println(`end fork`) 47 | return Success, nil 48 | })), 49 | } 50 | } 51 | }() 52 | fmt.Println(node.Tick()) 53 | time.Sleep(time.Millisecond * 50) 54 | fmt.Println(node.Tick()) 55 | time.Sleep(time.Millisecond * 150) 56 | fmt.Println(node.Tick()) 57 | time.Sleep(time.Millisecond * 200) 58 | fmt.Println(node.Tick()) // will receive the first tick's status 59 | time.Sleep(time.Millisecond * 50) 60 | fmt.Println(node.Tick()) 61 | time.Sleep(time.Millisecond * 100) 62 | fmt.Println(node.Tick()) 63 | fmt.Println(node.Tick()) 64 | fmt.Println(node.Tick()) 65 | time.Sleep(time.Millisecond * 450) 66 | fmt.Println(node.Tick()) 67 | fmt.Println(node.Tick()) 68 | //output: 69 | //start fork 70 | //running 71 | //start fork 72 | //running 73 | //start fork 74 | //running 75 | //end fork 76 | //end fork 77 | //success 78 | //success 79 | //end fork 80 | //success 81 | //start fork 82 | //running 83 | //start fork 84 | //running 85 | //end fork 86 | //end fork 87 | //success 88 | //success 89 | } 90 | 91 | func TestBackground(t *testing.T) { 92 | defer checkNumGoroutines(t)(false, 0) 93 | 94 | var ( 95 | status Status 96 | err error 97 | se = errors.New(`some_error`) 98 | ) 99 | node := New(Background(Fork), New(func(children []Node) (Status, error) { 100 | return status, err 101 | })) 102 | 103 | status, err = Success, nil 104 | if v, e := node.Tick(); v != Success || e != nil { 105 | t.Error(v, e) 106 | } 107 | 108 | status, err = Failure, nil 109 | if v, e := node.Tick(); v != Failure || e != nil { 110 | t.Error(v, e) 111 | } 112 | 113 | status, err = Success, se 114 | if v, e := node.Tick(); v != Failure || e != se { 115 | t.Error(v, e) 116 | } 117 | } 118 | 119 | func TestBackground_nilTick(t *testing.T) { 120 | if v := Background(nil); v != nil { 121 | t.Error(v) 122 | } 123 | } 124 | 125 | func TestBackground_withAny(t *testing.T) { 126 | defer checkNumGoroutines(t)(false, 0) 127 | 128 | var ( 129 | tick = Background(func() Tick { 130 | return Any(All) 131 | }) 132 | ) 133 | 134 | one := make(chan Status) 135 | go func() { 136 | one <- Running 137 | }() 138 | if status, err := tick([]Node{ 139 | New(func(children []Node) (status Status, e error) { 140 | return Failure, nil 141 | }), 142 | New(All, New(func(children []Node) (Status, error) { 143 | return <-one, nil 144 | })), 145 | New(func(children []Node) (status Status, e error) { 146 | return Failure, nil 147 | }), 148 | }); err != nil || status != Running { 149 | t.Error(status, err) 150 | } 151 | time.Sleep(time.Millisecond * 100) 152 | select { 153 | case <-one: 154 | t.Fatal(`expected consumed`) 155 | default: 156 | } 157 | 158 | two := make(chan Status) 159 | go func() { 160 | one <- Running 161 | two <- Running 162 | }() 163 | if status, err := tick([]Node{ 164 | New(func(children []Node) (status Status, e error) { 165 | return Failure, nil 166 | }), 167 | New(All, New(func(children []Node) (Status, error) { 168 | return <-two, nil 169 | })), 170 | New(func(children []Node) (status Status, e error) { 171 | return Failure, nil 172 | }), 173 | }); err != nil || status != Running { 174 | t.Error(status, err) 175 | } 176 | time.Sleep(time.Millisecond * 100) 177 | select { 178 | case <-one: 179 | t.Fatal(`expected consumed`) 180 | case <-two: 181 | t.Fatal(`expected consumed`) 182 | default: 183 | } 184 | 185 | three := make(chan Status) 186 | go func() { 187 | one <- Running 188 | two <- Running 189 | three <- Running 190 | }() 191 | if status, err := tick([]Node{ 192 | New(func(children []Node) (status Status, e error) { 193 | return Failure, nil 194 | }), 195 | New(All, New(func(children []Node) (Status, error) { 196 | return <-three, nil 197 | })), 198 | New(func(children []Node) (status Status, e error) { 199 | return Failure, nil 200 | }), 201 | }); err != nil || status != Running { 202 | t.Error(status, err) 203 | } 204 | time.Sleep(time.Millisecond * 100) 205 | select { 206 | case <-one: 207 | t.Fatal(`expected consumed`) 208 | case <-two: 209 | t.Fatal(`expected consumed`) 210 | case <-three: 211 | t.Fatal(`expected consumed`) 212 | default: 213 | } 214 | 215 | go func() { 216 | one <- Running 217 | two <- Running 218 | three <- Failure 219 | }() 220 | if status, err := tick([]Node{func() (tick Tick, nodes []Node) { 221 | panic(`should never reach here`) 222 | }}); err != nil || status != Failure { 223 | t.Error(status, err) 224 | } 225 | close(three) 226 | time.Sleep(time.Millisecond * 100) 227 | 228 | go func() { 229 | one <- Running 230 | two <- Running 231 | }() 232 | if status, err := tick([]Node{New(Selector)}); err != nil || status != Failure { 233 | t.Error(status, err) 234 | } 235 | time.Sleep(time.Millisecond * 100) 236 | select { 237 | case <-one: 238 | t.Fatal(`expected consumed`) 239 | case <-two: 240 | t.Fatal(`expected consumed`) 241 | default: 242 | } 243 | 244 | go func() { 245 | one <- Success 246 | }() 247 | if status, err := tick([]Node{func() (tick Tick, nodes []Node) { 248 | panic(`should never reach here`) 249 | }}); err != nil || status != Success { 250 | t.Error(status, err) 251 | } 252 | close(one) 253 | time.Sleep(time.Millisecond * 100) 254 | 255 | go func() { 256 | two <- Success 257 | }() 258 | if status, err := tick([]Node{func() (tick Tick, nodes []Node) { 259 | panic(`should never reach here`) 260 | }}); err != nil || status != Success { 261 | t.Error(status, err) 262 | } 263 | close(two) 264 | time.Sleep(time.Millisecond * 100) 265 | 266 | if status, err := tick([]Node{New(Sequence)}); err != nil || status != Success { 267 | t.Error(status, err) 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /value_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "fmt" 21 | "runtime" 22 | "testing" 23 | "time" 24 | ) 25 | 26 | func TestNode_Value_race(t *testing.T) { 27 | defer checkNumGoroutines(t)(false, waitNumGoroutinesDefault*3) 28 | done := make(chan struct{}) 29 | defer close(done) 30 | type k1 struct{} 31 | type k2 struct{} 32 | nodeOther := nn(Sequence).WithValue(k1{}, 5) 33 | for i := 0; i < 3000; i++ { 34 | node := nodeOther 35 | nodeOther = func() (Tick, []Node) { return node() } 36 | go func() { 37 | ticker := time.NewTicker(time.Millisecond * 10) 38 | defer ticker.Stop() 39 | for { 40 | node() 41 | select { 42 | case <-done: 43 | return 44 | case <-ticker.C: 45 | } 46 | } 47 | }() 48 | } 49 | go func() { 50 | ticker := time.NewTicker(time.Millisecond * 10) 51 | defer ticker.Stop() 52 | node := nn(Sequence).WithValue(k2{}, 3) 53 | for { 54 | node() 55 | select { 56 | case <-done: 57 | return 58 | case <-ticker.C: 59 | } 60 | } 61 | }() 62 | node := func() Node { 63 | node := nn(Sequence).WithValue(k1{}, 6) 64 | return func() (Tick, []Node) { 65 | time.Sleep(time.Millisecond * 100) 66 | return node() 67 | } 68 | }() 69 | n := time.Now() 70 | if v := node.Value(k1{}); v != 6 { 71 | t.Error(v) 72 | } 73 | if v := node.Value(k2{}); v != nil { 74 | t.Error(v) 75 | } 76 | t.Log(time.Since(n)) 77 | } 78 | 79 | func nn(tick Tick, children ...Node) Node { return func() (Tick, []Node) { return tick, children } } 80 | 81 | // noinspection GoNilness 82 | func TestNode_Value_simple(t *testing.T) { 83 | type k1 struct{} 84 | type k2 struct{} 85 | if v := (Node)(nil).Value(k1{}); v != nil { 86 | t.Error(v) 87 | } 88 | if v := (Node)(nil).Value(k2{}); v != nil { 89 | t.Error(v) 90 | } 91 | n1 := nn(Sequence) 92 | if v := n1.Value(k1{}); v != nil { 93 | t.Error(v) 94 | } 95 | if v := n1.Value(k2{}); v != nil { 96 | t.Error(v) 97 | } 98 | n2 := n1.WithValue(k1{}, `v1`) 99 | if v := n2.Value(k1{}); v != `v1` { 100 | t.Error(v) 101 | } 102 | if frame, _ := runtime.CallersFrames(valueDataCaller[:]).Next(); frame.Function != `github.com/joeycumines/go-behaviortree.Node.valueSync` { 103 | t.Error(frame) 104 | } 105 | if v := n2.Value(k2{}); v != nil { 106 | t.Error(v) 107 | } 108 | n3 := n2.WithValue(k2{}, `v2`) 109 | if v := n3.Value(k1{}); v != `v1` { 110 | t.Error(v) 111 | } 112 | if v := n3.Value(k2{}); v != `v2` { 113 | t.Error(v) 114 | } 115 | n4 := n3.WithValue(k1{}, `v3`) 116 | if v := n4.Value(k1{}); v != `v3` { 117 | t.Error(v) 118 | } 119 | if v := n4.Value(k2{}); v != `v2` { 120 | t.Error(v) 121 | } 122 | if v := n3.Value(k1{}); v != `v1` { 123 | t.Error(v) 124 | } 125 | if v := n3.Value(k2{}); v != `v2` { 126 | t.Error(v) 127 | } 128 | } 129 | 130 | func TestNode_Value_noCaller(t *testing.T) { 131 | done := make(chan struct{}) 132 | defer func() func() { 133 | old := runtimeCallers 134 | runtimeCallers = func(skip int, pc []uintptr) int { 135 | close(done) 136 | return 0 137 | } 138 | return func() { 139 | runtimeCallers = old 140 | } 141 | }()() 142 | if v := nn(Sequence).Value(nil); v != nil { 143 | t.Error(v) 144 | } 145 | select { 146 | case <-done: 147 | default: 148 | t.Error(`expected done`) 149 | } 150 | valueDataMutex.Lock() 151 | //lint:ignore SA2001 ensuring the lock can be acquired 152 | valueDataMutex.Unlock() 153 | } 154 | 155 | func TestNode_Value_nested(t *testing.T) { 156 | type k1 struct{} 157 | node := nn(Sequence).WithValue(k1{}, 5) 158 | if v := node.Value(k1{}); v != 5 { 159 | t.Fatal(v) 160 | } 161 | for i := 0; i < 3000; i++ { 162 | old := node 163 | node = func() (Tick, []Node) { return old() } 164 | if v := node.Value(k1{}); v != 5 { 165 | t.Fatal(i, v) 166 | } 167 | } 168 | } 169 | 170 | func TestNode_WithValue_panicNilReceiver(t *testing.T) { 171 | defer func() { 172 | if r := fmt.Sprint(recover()); r != `behaviortree.Node.WithValue nil receiver` { 173 | t.Error(r) 174 | } 175 | }() 176 | Node(nil).WithValue(1, 2) 177 | t.Error(`expected panic`) 178 | } 179 | 180 | func TestNode_WithValue_panicNilKey(t *testing.T) { 181 | defer func() { 182 | if r := fmt.Sprint(recover()); r != `behaviortree.Node.WithValue nil key` { 183 | t.Error(r) 184 | } 185 | }() 186 | Node(func() (Tick, []Node) { return nil, nil }).WithValue(nil, 2) 187 | t.Error(`expected panic`) 188 | } 189 | 190 | func TestNode_WithValue_panicNotComparible(t *testing.T) { 191 | defer func() { 192 | if r := fmt.Sprint(recover()); r != `behaviortree.Node.WithValue key is not comparable` { 193 | t.Error(r) 194 | } 195 | }() 196 | Node(func() (Tick, []Node) { return nil, nil }).WithValue([]int(nil), 2) 197 | t.Error(`expected panic`) 198 | } 199 | 200 | var Result interface{} 201 | 202 | func benchTick(node Node) (status Status, err error) { 203 | for { 204 | status, err = node.Tick() 205 | if err != nil { 206 | return 207 | } 208 | if status == Failure { 209 | return 210 | } 211 | } 212 | } 213 | 214 | func Benchmark_newExampleCounter_withValue(b *testing.B) { 215 | var ( 216 | status Status 217 | err error 218 | ) 219 | for i := 0; i < b.N; i++ { 220 | status, err = benchTick(newExampleCounter()) 221 | if err != nil { 222 | b.Fatal(err) 223 | } 224 | } 225 | Result = status 226 | } 227 | 228 | func Benchmark_newExampleCounter_sansValue(b *testing.B) { 229 | b.StopTimer() 230 | defer func() func() { 231 | old := factory 232 | factory = func(tick Tick, children []Node) (node Node) { 233 | return func() (Tick, []Node) { 234 | return tick, children 235 | } 236 | } 237 | return func() { 238 | factory = old 239 | } 240 | }()() 241 | b.StartTimer() 242 | var ( 243 | status Status 244 | err error 245 | ) 246 | for i := 0; i < b.N; i++ { 247 | status, err = benchTick(newExampleCounter()) 248 | if err != nil { 249 | b.Fatal(err) 250 | } 251 | } 252 | Result = status 253 | } 254 | 255 | func Benchmark_newExampleCounter_withValueBackgroundStringer(b *testing.B) { 256 | { 257 | b.StopTimer() 258 | node := newExampleCounter() 259 | done := make(chan struct{}) 260 | defer close(done) 261 | go func() { 262 | ticker := time.NewTicker(time.Millisecond) 263 | defer ticker.Stop() 264 | for { 265 | _ = node.String() 266 | select { 267 | case <-done: 268 | return 269 | case <-ticker.C: 270 | } 271 | } 272 | }() 273 | time.Sleep(time.Millisecond * 50) 274 | b.StartTimer() 275 | } 276 | var ( 277 | status Status 278 | err error 279 | ) 280 | for i := 0; i < b.N; i++ { 281 | status, err = benchTick(newExampleCounter()) 282 | if err != nil { 283 | b.Fatal(err) 284 | } 285 | } 286 | Result = status 287 | } 288 | -------------------------------------------------------------------------------- /ticker_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "fmt" 23 | "reflect" 24 | "sync" 25 | "testing" 26 | "time" 27 | ) 28 | 29 | func TestNewTicker_panic1(t *testing.T) { 30 | defer func() { 31 | r := recover() 32 | if s := fmt.Sprint(r); r == nil || s != "behaviortree.NewTicker nil context" { 33 | t.Fatal("unexpected panic", s) 34 | } 35 | }() 36 | //lint:ignore SA1012 testing nil context 37 | NewTicker(nil, 1, func() (Tick, []Node) { 38 | return nil, nil 39 | }) 40 | t.Error("expected a panic") 41 | } 42 | 43 | func TestNewTicker_panic2(t *testing.T) { 44 | defer func() { 45 | r := recover() 46 | if s := fmt.Sprint(r); r == nil || s != "behaviortree.NewTicker duration <= 0" { 47 | t.Fatal("unexpected panic", s) 48 | } 49 | }() 50 | NewTicker(context.Background(), 0, func() (Tick, []Node) { 51 | return nil, nil 52 | }) 53 | t.Error("expected a panic") 54 | } 55 | 56 | func TestNewTicker_panic3(t *testing.T) { 57 | defer func() { 58 | r := recover() 59 | if s := fmt.Sprint(r); r == nil || s != "behaviortree.NewTicker nil node" { 60 | t.Fatal("unexpected panic", s) 61 | } 62 | }() 63 | NewTicker(context.Background(), 1, nil) 64 | t.Error("expected a panic") 65 | } 66 | 67 | func TestNewTicker_run(t *testing.T) { 68 | defer checkNumGoroutines(t)(false, 0) 69 | 70 | var ( 71 | mutex sync.Mutex 72 | counter int 73 | ) 74 | 75 | node := func() (Tick, []Node) { 76 | return func(children []Node) (Status, error) { 77 | mutex.Lock() 78 | defer mutex.Unlock() 79 | counter++ 80 | time.Sleep(time.Millisecond) 81 | return Success, nil 82 | }, nil 83 | } 84 | 85 | type stopComplain int 86 | 87 | c := NewTicker( 88 | context.WithValue(context.Background(), stopComplain(1), 2), 89 | time.Millisecond*5, 90 | node, 91 | ) 92 | 93 | time.Sleep(time.Millisecond * 50) 94 | 95 | v := c.(*tickerCore) 96 | 97 | if v.node == nil || reflect.ValueOf(node).Pointer() != reflect.ValueOf(v.node).Pointer() { 98 | //noinspection GoPrintFunctions 99 | t.Error("unexpected node", v.node) 100 | } 101 | 102 | if v.ctx == nil || v.ctx.Value(stopComplain(1)) != 2 { 103 | t.Error("unexpected context", v.ctx) 104 | } 105 | 106 | if v.cancel == nil { 107 | t.Error("nil cancel") 108 | } 109 | 110 | if v.err != nil { 111 | t.Error("unexpected error", v.err) 112 | } 113 | 114 | if v.stop == nil || v.done == nil { 115 | t.Error("nil chans") 116 | } 117 | 118 | if v.ticker == nil { 119 | t.Error("nil ticker") 120 | } 121 | 122 | func() { 123 | mutex.Lock() 124 | defer mutex.Unlock() 125 | if counter < 0 { 126 | t.Error("bad counter", counter) 127 | } 128 | }() 129 | 130 | if err := c.Err(); err != nil { 131 | t.Error("unexpected error call", err) 132 | } 133 | 134 | if d := c.Done(); d == nil || d != v.done { 135 | t.Error("unexpected done call", d) 136 | } 137 | 138 | func() { 139 | start := time.Now() 140 | 141 | c.Stop() 142 | 143 | <-c.Done() 144 | 145 | diff := time.Since(start) 146 | 147 | if diff > time.Millisecond*20 { 148 | t.Fatal("unexpected diff", diff) 149 | } 150 | 151 | if err := c.Err(); err != nil { 152 | t.Fatal(err) 153 | } 154 | }() 155 | } 156 | 157 | func TestNewTicker_runError(t *testing.T) { 158 | defer checkNumGoroutines(t)(false, 0) 159 | 160 | expected := errors.New("some_error") 161 | 162 | node := func() (Tick, []Node) { 163 | return func(children []Node) (Status, error) { 164 | return 0, expected 165 | }, nil 166 | } 167 | 168 | startedAt := time.Now() 169 | defer func() { 170 | diff := time.Since(startedAt) 171 | if diff > time.Millisecond*20 { 172 | t.Error("unexpected diff", diff) 173 | } 174 | }() 175 | 176 | c := NewTicker( 177 | context.Background(), 178 | time.Millisecond, 179 | node, 180 | ) 181 | 182 | <-c.Done() 183 | 184 | if err := c.Err(); err != expected { 185 | t.Error("unexpected error", err) 186 | } 187 | } 188 | 189 | func TestNewTicker_runCancel(t *testing.T) { 190 | defer checkNumGoroutines(t)(false, 0) 191 | 192 | node := func() (Tick, []Node) { 193 | return func(children []Node) (Status, error) { 194 | time.Sleep(time.Millisecond) 195 | return Success, nil 196 | }, nil 197 | } 198 | 199 | since := func() func() time.Duration { 200 | startedAt := time.Now() 201 | return func() time.Duration { return time.Since(startedAt) } 202 | }() 203 | 204 | defer func() { 205 | if v := since(); v > time.Millisecond*700 { 206 | t.Error(v) 207 | } 208 | }() 209 | 210 | ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*200) 211 | defer cancel() 212 | 213 | c := NewTicker( 214 | ctx, 215 | time.Millisecond, 216 | node, 217 | ) 218 | 219 | time.Sleep(time.Millisecond * 50) 220 | 221 | select { 222 | case <-c.Done(): 223 | t.Error() 224 | default: 225 | } 226 | 227 | <-c.Done() 228 | 229 | if v := since(); v < time.Millisecond*180 { 230 | t.Error(v) 231 | } 232 | 233 | if err := c.Err(); err == nil || err.Error() != "context deadline exceeded" { 234 | t.Error("unexpected error", err) 235 | } 236 | } 237 | 238 | func TestNewTickerStopOnFailure_success(t *testing.T) { 239 | defer checkNumGoroutines(t)(false, 0) 240 | var ( 241 | mutex sync.Mutex 242 | count int 243 | ticker = NewTickerStopOnFailure( 244 | context.Background(), 245 | time.Millisecond*50, 246 | func() (Tick, []Node) { 247 | return func(children []Node) (Status, error) { 248 | mutex.Lock() 249 | defer mutex.Unlock() 250 | if len(children) != 5 { 251 | t.Error("bad children", len(children)) 252 | } 253 | count++ 254 | if count == 5 { 255 | return Failure, nil 256 | } 257 | return Success, nil 258 | }, make([]Node, 5) 259 | }, 260 | ) 261 | ) 262 | defer ticker.Stop() 263 | timer := time.NewTimer(time.Millisecond * 350) 264 | defer timer.Stop() 265 | startedAt := time.Now() 266 | select { 267 | case <-timer.C: 268 | t.Fatal("expected done") 269 | case <-ticker.Done(): 270 | } 271 | duration := time.Since(startedAt) 272 | if duration < time.Millisecond*170 { 273 | t.Error(duration.String()) 274 | } 275 | mutex.Lock() 276 | defer mutex.Unlock() 277 | if err := ticker.Err(); err != nil { 278 | t.Error(err) 279 | } 280 | } 281 | 282 | func TestNewTickerStopOnFailure_error(t *testing.T) { 283 | defer checkNumGoroutines(t)(false, 0) 284 | ticker := NewTickerStopOnFailure( 285 | context.Background(), 286 | time.Millisecond*50, 287 | func() (Tick, []Node) { 288 | return func(children []Node) (Status, error) { 289 | return Failure, errors.New("some_error") 290 | }, make([]Node, 5) 291 | }, 292 | ) 293 | defer ticker.Stop() 294 | <-ticker.Done() 295 | if ticker.Err() == nil { 296 | t.Fatal("expected an error") 297 | } 298 | } 299 | 300 | func TestNewTickerStopOnFailure_nilNode(t *testing.T) { 301 | defer checkNumGoroutines(t)(false, 0) 302 | defer func() { 303 | if r := fmt.Sprint(recover()); r != "behaviortree.NewTickerStopOnFailure nil node" { 304 | t.Error(r) 305 | } 306 | }() 307 | NewTickerStopOnFailure(context.Background(), 0, nil) 308 | } 309 | 310 | func TestNewTickerStopOnFailure_nilTick(t *testing.T) { 311 | defer checkNumGoroutines(t)(false, 0) 312 | ticker := NewTickerStopOnFailure( 313 | context.Background(), 314 | time.Millisecond*10, 315 | func() (tick Tick, nodes []Node) { 316 | return 317 | }, 318 | ) 319 | <-ticker.Done() 320 | if err := ticker.Err(); err == nil || err.Error() != "behaviortree.Node cannot tick a node with a nil tick" { 321 | t.Error(err) 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /switch_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "fmt" 23 | "testing" 24 | "time" 25 | ) 26 | 27 | func TestSwitch(t *testing.T) { 28 | type ( 29 | ChildTick struct { 30 | Index int 31 | Status Status 32 | Err error 33 | } 34 | Invocation struct { 35 | Name string 36 | Children int 37 | ChildTicks []ChildTick 38 | Status Status 39 | Err error 40 | } 41 | ) 42 | for _, tc := range [...]struct { 43 | Name string 44 | Tick Tick 45 | Invocations []Invocation 46 | }{ 47 | { 48 | Name: `stateless`, 49 | Tick: Switch, 50 | Invocations: []Invocation{ 51 | { 52 | Name: `no children`, 53 | Status: Success, 54 | }, 55 | { 56 | Name: `default case success`, 57 | Children: 1, 58 | ChildTicks: []ChildTick{ 59 | {0, Success, nil}, 60 | }, 61 | Status: Success, 62 | }, 63 | { 64 | Name: `default case invalid status and error`, 65 | Children: 1, 66 | ChildTicks: []ChildTick{ 67 | {0, 123, errors.New(`some_error`)}, 68 | }, 69 | Status: 123, 70 | Err: errors.New(`some_error`), 71 | }, 72 | { 73 | Name: `single case success`, 74 | Children: 2, 75 | ChildTicks: []ChildTick{ 76 | {0, Success, nil}, 77 | {1, Success, nil}, 78 | }, 79 | Status: Success, 80 | }, 81 | { 82 | Name: `single case failure`, 83 | Children: 2, 84 | ChildTicks: []ChildTick{ 85 | {0, Success, nil}, 86 | {1, Failure, nil}, 87 | }, 88 | Status: Failure, 89 | }, 90 | { 91 | Name: `single case success no match`, 92 | Children: 2, 93 | ChildTicks: []ChildTick{ 94 | {0, Failure, nil}, 95 | }, 96 | Status: Success, 97 | }, 98 | { 99 | Name: `single case invalid status and error`, 100 | Children: 2, 101 | ChildTicks: []ChildTick{ 102 | {0, Success, nil}, 103 | {1, 123, errors.New(`some_error`)}, 104 | }, 105 | Status: 123, 106 | Err: errors.New(`some_error`), 107 | }, 108 | { 109 | Name: `single case condition invalid status and error`, 110 | Children: 2, 111 | ChildTicks: []ChildTick{ 112 | {0, 123, errors.New(`some_error`)}, 113 | }, 114 | Status: Failure, 115 | Err: errors.New(`some_error`), 116 | }, 117 | { 118 | Name: `single case condition running`, 119 | Children: 2, 120 | ChildTicks: []ChildTick{ 121 | {0, Running, nil}, 122 | }, 123 | Status: Running, 124 | }, 125 | { 126 | Name: `multi case`, 127 | Children: 9, 128 | ChildTicks: []ChildTick{ 129 | {0, Failure, nil}, 130 | {2, Failure, nil}, 131 | {4, Success, nil}, 132 | {5, Success, nil}, 133 | }, 134 | Status: Success, 135 | }, 136 | { 137 | Name: `multi case default failure`, 138 | Children: 7, 139 | ChildTicks: []ChildTick{ 140 | {0, Failure, nil}, 141 | {2, Failure, nil}, 142 | {4, Failure, nil}, 143 | {6, Failure, nil}, 144 | }, 145 | Status: Failure, 146 | }, 147 | }, 148 | }, 149 | } { 150 | t.Run(tc.Name, func(t *testing.T) { 151 | for _, invocation := range tc.Invocations { 152 | t.Run(invocation.Name, func(t *testing.T) { 153 | var children []Node 154 | for i := 0; i < invocation.Children; i++ { 155 | i := i 156 | children = append(children, New(func([]Node) (status Status, err error) { 157 | if len(invocation.ChildTicks) == 0 { 158 | t.Errorf(`child %d ticked but none expected`, i) 159 | return Success, nil 160 | } 161 | if invocation.ChildTicks[0].Index != i { 162 | t.Errorf(`child %d ticked but expected %d`, i, invocation.ChildTicks[0].Index) 163 | return Success, nil 164 | } 165 | status, err = invocation.ChildTicks[0].Status, invocation.ChildTicks[0].Err 166 | invocation.ChildTicks = invocation.ChildTicks[1:] 167 | return 168 | })) 169 | } 170 | status, err := tc.Tick(children) 171 | if (err == nil) != (invocation.Err == nil) || (err != nil && err.Error() != invocation.Err.Error()) { 172 | t.Error(err) 173 | } 174 | if status != invocation.Status { 175 | t.Error(status) 176 | } 177 | if len(invocation.ChildTicks) != 0 { 178 | t.Errorf(`expected %d more child ticks`, len(invocation.ChildTicks)) 179 | } 180 | }) 181 | } 182 | }) 183 | } 184 | } 185 | 186 | func ExampleSwitch() { 187 | var ( 188 | sanityChecks []func() 189 | newNode = func(name string, statuses ...Status) Node { 190 | sanityChecks = append(sanityChecks, func() { 191 | if len(statuses) != 0 { 192 | panic(fmt.Errorf(`node %s has %d unconsumed statuses`, name, len(statuses))) 193 | } 194 | }) 195 | return New(func([]Node) (status Status, _ error) { 196 | if len(statuses) == 0 { 197 | panic(fmt.Errorf(`node %s has no unconsumed statuses`, name)) 198 | } 199 | status = statuses[0] 200 | statuses = statuses[1:] 201 | fmt.Printf("Tick %s: %s\n", name, status) 202 | return 203 | }) 204 | } 205 | ticker = NewTickerStopOnFailure( 206 | context.Background(), 207 | time.Millisecond, 208 | New( 209 | Memorize(Sequence), 210 | newNode(`START`, Success, Success, Success, Success, Failure), 211 | New( 212 | Memorize(Selector), 213 | New( 214 | Memorize(Sequence), 215 | New( 216 | Memorize(Switch), 217 | 218 | newNode(`case-1-condition`, Failure, Failure, Running, Running, Running, Failure, Failure), 219 | newNode(`case-1-statement`), 220 | 221 | newNode(`case-2-condition`, Failure, Failure, Running, Running, Success, Success), 222 | newNode(`case-2-statement`, Running, Running, Running, Failure, Running, Success), 223 | 224 | newNode(`case-3-condition`, Failure, Failure), 225 | newNode(`case-3-statement`), 226 | 227 | newNode(`default-statement`, Failure, Success), 228 | ), 229 | newNode(`SUCCESS`, Success, Success), 230 | ), 231 | newNode(`FAILURE`, Success, Success), 232 | ), 233 | ), 234 | ) 235 | ) 236 | <-ticker.Done() 237 | if err := ticker.Err(); err != nil { 238 | panic(err) 239 | } 240 | for _, sanityCheck := range sanityChecks { 241 | sanityCheck() 242 | } 243 | // output: 244 | // Tick START: success 245 | // Tick case-1-condition: failure 246 | // Tick case-2-condition: failure 247 | // Tick case-3-condition: failure 248 | // Tick default-statement: failure 249 | // Tick FAILURE: success 250 | // Tick START: success 251 | // Tick case-1-condition: failure 252 | // Tick case-2-condition: failure 253 | // Tick case-3-condition: failure 254 | // Tick default-statement: success 255 | // Tick SUCCESS: success 256 | // Tick START: success 257 | // Tick case-1-condition: running 258 | // Tick case-1-condition: running 259 | // Tick case-1-condition: running 260 | // Tick case-1-condition: failure 261 | // Tick case-2-condition: running 262 | // Tick case-2-condition: running 263 | // Tick case-2-condition: success 264 | // Tick case-2-statement: running 265 | // Tick case-2-statement: running 266 | // Tick case-2-statement: running 267 | // Tick case-2-statement: failure 268 | // Tick FAILURE: success 269 | // Tick START: success 270 | // Tick case-1-condition: failure 271 | // Tick case-2-condition: success 272 | // Tick case-2-statement: running 273 | // Tick case-2-statement: success 274 | // Tick SUCCESS: success 275 | // Tick START: failure 276 | } 277 | -------------------------------------------------------------------------------- /printer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "bytes" 21 | "errors" 22 | "fmt" 23 | "github.com/go-test/deep" 24 | "io" 25 | "regexp" 26 | "runtime" 27 | "strings" 28 | "testing" 29 | ) 30 | 31 | func replacePointers(b string) string { 32 | var ( 33 | m = make(map[string]struct{}) 34 | r []string 35 | n int 36 | ) 37 | for _, v := range regexp.MustCompile(`(?:[[:^alnum:]]|^)(0x[[:alnum:]]{1,16})(?:[[:^alnum:]]|$)`).FindAllStringSubmatch(b, -1) { 38 | if v := v[1]; v != `0x0` { 39 | if _, ok := m[v]; !ok { 40 | n++ 41 | m[v] = struct{}{} 42 | r = append(r, v, fmt.Sprintf(`%#x`, n)) 43 | } 44 | } 45 | } 46 | return strings.NewReplacer(r...).Replace(b) 47 | } 48 | 49 | func TestNode_String(t *testing.T) { 50 | for _, testCase := range []struct { 51 | Name string 52 | Node Node 53 | Value string 54 | }{ 55 | { 56 | Name: `nil node`, 57 | Node: nil, 58 | Value: ``, 59 | }, 60 | { 61 | Name: `single sequence`, 62 | Node: New(Sequence), 63 | Value: "[0x1 printer_test.go:62 0x2 sequence.go:21] github.com/joeycumines/go-behaviortree.TestNode_String | github.com/joeycumines/go-behaviortree.Sequence", 64 | }, 65 | { 66 | Name: `single closure`, 67 | Node: New(func(children []Node) (Status, error) { panic(`TestNode_String`) }), 68 | Value: "[0x1 printer_test.go:67 0x2 printer_test.go:67] github.com/joeycumines/go-behaviortree.TestNode_String | github.com/joeycumines/go-behaviortree.TestNode_String.func1", 69 | }, 70 | { 71 | Name: `nil tick`, 72 | Node: New(nil), 73 | Value: "[0x1 printer_test.go:72 0x0 -] github.com/joeycumines/go-behaviortree.TestNode_String | ", 74 | }, 75 | { 76 | Name: `example counter`, 77 | Node: newExampleCounter(), 78 | Value: "[0x1 example_test.go:47 0x2 selector.go:21] github.com/joeycumines/go-behaviortree.newExampleCounter | github.com/joeycumines/go-behaviortree.Selector\n├── [0x3 example_test.go:49 0x4 sequence.go:21] github.com/joeycumines/go-behaviortree.newExampleCounter | github.com/joeycumines/go-behaviortree.Sequence\n│ ├── [0x5 example_test.go:51 0x6 example_test.go:52] github.com/joeycumines/go-behaviortree.newExampleCounter | github.com/joeycumines/go-behaviortree.newExampleCounter.func3\n│ ├── [0x7 example_test.go:40 0x8 example_test.go:41] github.com/joeycumines/go-behaviortree.newExampleCounter | github.com/joeycumines/go-behaviortree.newExampleCounter.func2\n│ └── [0x9 example_test.go:32 0xa example_test.go:33] github.com/joeycumines/go-behaviortree.newExampleCounter.func1 | github.com/joeycumines/go-behaviortree.newExampleCounter.func1.1\n└── [0xb example_test.go:62 0x4 sequence.go:21] github.com/joeycumines/go-behaviortree.newExampleCounter | github.com/joeycumines/go-behaviortree.Sequence\n ├── [0xc example_test.go:64 0xd example_test.go:65] github.com/joeycumines/go-behaviortree.newExampleCounter | github.com/joeycumines/go-behaviortree.newExampleCounter.func4\n ├── [0x7 example_test.go:40 0x8 example_test.go:41] github.com/joeycumines/go-behaviortree.newExampleCounter | github.com/joeycumines/go-behaviortree.newExampleCounter.func2\n └── [0x9 example_test.go:32 0xa example_test.go:33] github.com/joeycumines/go-behaviortree.newExampleCounter.func1 | github.com/joeycumines/go-behaviortree.newExampleCounter.func1.1", 79 | }, 80 | } { 81 | t.Run(testCase.Name, func(t *testing.T) { 82 | value := testCase.Node.String() 83 | //t.Logf("\n---\n%s\n---", value) 84 | value = replacePointers(value) 85 | if value != testCase.Value { 86 | t.Errorf("unexpected value: %q\n> %s", value, strings.ReplaceAll(value, "\n", "\n> ")) 87 | } 88 | }) 89 | } 90 | } 91 | 92 | type mockPrinter struct { 93 | fprint func(output io.Writer, node Node) error 94 | } 95 | 96 | func (m *mockPrinter) Fprint(output io.Writer, node Node) error { return m.fprint(output, node) } 97 | 98 | func TestNode_String_error(t *testing.T) { 99 | defer func() func() { 100 | old := DefaultPrinter 101 | DefaultPrinter = &mockPrinter{fprint: func(output io.Writer, node Node) error { 102 | return errors.New(`some_error`) 103 | }} 104 | return func() { 105 | DefaultPrinter = old 106 | } 107 | }()() 108 | if v := Node(nil).String(); v != `behaviortree.DefaultPrinter error: some_error` { 109 | t.Error(v) 110 | } 111 | } 112 | 113 | func TestTreePrinter_Fprint_copyError(t *testing.T) { 114 | r, w := io.Pipe() 115 | _ = r.Close() 116 | if err := (TreePrinter{Formatter: DefaultPrinterFormatter, Inspector: DefaultPrinterInspector}).Fprint(w, Node(nil)); err != io.ErrClosedPipe { 117 | t.Error(err) 118 | } 119 | } 120 | 121 | func Test_treePrinterNodeXlabMeta_String_panicLen(t *testing.T) { 122 | defer func() { 123 | if r := fmt.Sprint(recover()); r != `m.sizes [4] mismatched m.strings [one two]` { 124 | t.Error(r) 125 | } 126 | }() 127 | m := &treePrinterNodeXlabMeta{ 128 | treePrinterNodeXlab: &treePrinterNodeXlab{ 129 | sizes: []int{4}, 130 | }, 131 | strings: []string{`one`, `two`}, 132 | } 133 | _ = m.String() 134 | t.Error(`expected panic`) 135 | } 136 | 137 | func Test_treePrinterNodeXlabMeta_String_panicInterfaces(t *testing.T) { 138 | defer func() { 139 | if r := fmt.Sprint(recover()); r != `m.interfaces [] should be nil` { 140 | t.Error(r) 141 | } 142 | }() 143 | m := &treePrinterNodeXlabMeta{ 144 | treePrinterNodeXlab: &treePrinterNodeXlab{ 145 | sizes: []int{4}, 146 | }, 147 | strings: []string{`one`, `two`}, 148 | interfaces: make([]interface{}, 0), 149 | } 150 | _ = m.String() 151 | t.Error(`expected panic`) 152 | } 153 | 154 | func dummyNode() (Tick, []Node) { 155 | return nil, nil 156 | } 157 | 158 | func TestDefaultPrinterInspector_nil(t *testing.T) { 159 | var actual [2]interface{} 160 | actual[0], actual[1] = DefaultPrinterInspector(nil, nil) 161 | if diff := deep.Equal( 162 | actual, 163 | [2]interface{}{ 164 | []interface{}{ 165 | `0x0`, 166 | `-`, 167 | `0x0`, 168 | `-`, 169 | }, 170 | ` | `, 171 | }, 172 | ); diff != nil { 173 | t.Errorf("unexpected diff:\n%s", strings.Join(diff, "\n")) 174 | } 175 | var node Node = dummyNode 176 | actual[0], actual[1] = DefaultPrinterInspector(node, nil) 177 | if diff := deep.Equal( 178 | actual, 179 | [2]interface{}{ 180 | []interface{}{ 181 | fmt.Sprintf(`%p`, node), 182 | `printer_test.go:155`, 183 | `0x0`, 184 | `-`, 185 | }, 186 | `github.com/joeycumines/go-behaviortree.dummyNode | `, 187 | }, 188 | ); diff != nil { 189 | t.Errorf("unexpected diff:\n%s", strings.Join(diff, "\n")) 190 | } 191 | tick := Selector 192 | actual[0], actual[1] = DefaultPrinterInspector(nil, tick) 193 | if diff := deep.Equal( 194 | actual, 195 | [2]interface{}{ 196 | []interface{}{ 197 | `0x0`, 198 | `-`, 199 | fmt.Sprintf(`%p`, tick), 200 | `selector.go:21`, 201 | }, 202 | ` | github.com/joeycumines/go-behaviortree.Selector`, 203 | }, 204 | ); diff != nil { 205 | t.Errorf("unexpected diff:\n%s", strings.Join(diff, "\n")) 206 | } 207 | } 208 | 209 | func TestDefaultPrinterInspector_noName(t *testing.T) { 210 | defer func() func() { 211 | old := runtimeFuncForPC 212 | runtimeFuncForPC = func(pc uintptr) *runtime.Func { return nil } 213 | return func() { 214 | runtimeFuncForPC = old 215 | } 216 | }()() 217 | var actual [2]interface{} 218 | actual[0], actual[1] = DefaultPrinterInspector(dummyNode, Sequence) 219 | if diff := deep.Equal( 220 | actual, 221 | [2]interface{}{ 222 | []interface{}{ 223 | `0x0`, 224 | `-`, 225 | `0x0`, 226 | `-`, 227 | }, 228 | `- | -`, 229 | }, 230 | ); diff != nil { 231 | t.Errorf("unexpected diff:\n%s", strings.Join(diff, "\n")) 232 | } 233 | } 234 | 235 | func TestTreePrinter_Fprint_emptyMeta(t *testing.T) { 236 | p := TreePrinter{ 237 | Inspector: func(node Node, tick Tick) (meta []interface{}, value interface{}) { 238 | return []interface{}{``, ``, ``}, `` 239 | }, 240 | Formatter: DefaultPrinterFormatter, 241 | } 242 | b := new(bytes.Buffer) 243 | _ = p.Fprint(b, nn(nil)) 244 | if v := b.String(); v != `[ ] ` { 245 | t.Error(v) 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /manager_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "runtime" 23 | "sync" 24 | "sync/atomic" 25 | "testing" 26 | "time" 27 | ) 28 | 29 | func TestManager_Stop_raceCloseDone(t *testing.T) { 30 | defer checkNumGoroutines(t)(false, 0) 31 | m := NewManager().(*manager) 32 | close(m.done) 33 | m.Stop() 34 | } 35 | 36 | func TestManager_Stop_noTickers(t *testing.T) { 37 | defer checkNumGoroutines(t)(false, 0) 38 | m := NewManager() 39 | if err := m.Err(); err != nil { 40 | t.Error(err) 41 | } 42 | select { 43 | case <-m.Done(): 44 | t.Error() 45 | default: 46 | } 47 | m.Stop() 48 | if err := m.Err(); err != nil { 49 | t.Error(err) 50 | } 51 | <-m.Done() 52 | if err := m.Err(); err != nil { 53 | t.Error(err) 54 | } 55 | } 56 | 57 | func TestManager_Add_whileStopping(t *testing.T) { 58 | defer checkNumGoroutines(t)(false, 0) 59 | m := NewManager() 60 | for i := 0; i < 10; i++ { 61 | if err := m.Add(NewManager()); err != nil { 62 | t.Fatal(err) 63 | } 64 | } 65 | var ( 66 | wg sync.WaitGroup 67 | count int64 68 | done int32 69 | stopped int32 70 | ) 71 | wg.Add(8) 72 | defer func() { 73 | atomic.AddInt64(&count, -atomic.LoadInt64(&count)) 74 | m.Stop() 75 | atomic.StoreInt32(&stopped, 1) 76 | time.Sleep(time.Millisecond * 50) 77 | atomic.StoreInt32(&done, 1) 78 | wg.Wait() 79 | <-m.Done() 80 | if err := m.Err(); err != nil { 81 | t.Error(err) 82 | } 83 | count := atomic.LoadInt64(&count) 84 | t.Log(count) 85 | if count < 15 { 86 | t.Error(count) 87 | } 88 | }() 89 | for i := 0; i < 8; i++ { 90 | go func() { 91 | defer wg.Done() 92 | for atomic.LoadInt32(&done) == 0 { 93 | var ( 94 | stoppedBefore = atomic.LoadInt32(&stopped) != 0 95 | err = m.Add(NewManager()) 96 | stoppedAfter = atomic.LoadInt32(&stopped) != 0 97 | ) 98 | if err != nil && err != ErrManagerStopped { 99 | t.Error(err) 100 | } 101 | if stoppedBefore && !stoppedAfter { 102 | t.Error() 103 | } 104 | if stoppedBefore && err == nil { 105 | t.Error() 106 | } 107 | atomic.AddInt64(&count, 1) 108 | time.Sleep(time.Millisecond) 109 | } 110 | }() 111 | } 112 | time.Sleep(time.Millisecond * 20) 113 | } 114 | 115 | func TestManager_Add_secondStopCase(t *testing.T) { 116 | defer checkNumGoroutines(t)(false, 0) 117 | out := make(chan error) 118 | defer close(out) 119 | done := make(chan struct{}) 120 | stop := make(chan struct{}) 121 | go func() { out <- (&manager{stop: stop, done: done}).Add(mockTicker{}) }() 122 | time.Sleep(time.Millisecond * 100) 123 | select { 124 | case err := <-out: 125 | t.Fatal(err) 126 | default: 127 | } 128 | close(stop) 129 | if err := <-out; err != ErrManagerStopped { 130 | t.Error(err) 131 | } 132 | <-done 133 | } 134 | 135 | func TestManager_Stop_cleanupGoroutines(t *testing.T) { 136 | check := checkNumGoroutines(t) 137 | defer check(false, 0) 138 | 139 | m := NewManager() 140 | 141 | { 142 | // add a ticker then stop it, then verify that all resources (goroutines) are cleaned up 143 | check(false, 0) 144 | done := make(chan struct{}) 145 | if err := m.Add(mockTicker{ 146 | done: func() <-chan struct{} { return done }, 147 | err: func() error { return nil }, 148 | stop: func() {}, 149 | }); err != nil { 150 | t.Fatal(err) 151 | } 152 | check(true, 0) 153 | close(done) 154 | check(false, 0) 155 | if err := m.Err(); err != nil { 156 | t.Error(err) 157 | } 158 | select { 159 | case <-m.Done(): 160 | t.Fatal() 161 | default: 162 | } 163 | } 164 | 165 | { 166 | // add two tickers (one multiple times), then stop one, then the other 167 | var ( 168 | m1 = NewManager() 169 | m2 = NewManager() 170 | ) 171 | check(false, 0) 172 | for i := 0; i < 30; i++ { 173 | if err := m.Add(m1); err != nil { 174 | t.Fatal(err) 175 | } 176 | } 177 | if err := m.Add(m2); err != nil { 178 | t.Fatal(err) 179 | } 180 | check(true, 0) 181 | gr := runtime.NumGoroutine() 182 | m1.Stop() 183 | if diff := waitNumGoroutines(0, func(n int) bool { return (n - gr + 1) <= 0 }) - gr + 1; diff > 0 { 184 | t.Errorf("too many goroutines: +%d", diff) 185 | } 186 | m2.Stop() 187 | } 188 | } 189 | 190 | func TestNewManager(t *testing.T) { 191 | defer checkNumGoroutines(t)(false, 0) 192 | 193 | m := NewManager().(*manager) 194 | 195 | select { 196 | case <-m.Done(): 197 | t.Error("unexpected done") 198 | default: 199 | } 200 | 201 | if err := m.Err(); err != nil { 202 | t.Fatal(err) 203 | } 204 | 205 | var ( 206 | mutex sync.Mutex 207 | stops1 int 208 | done1 = make(chan struct{}) 209 | err1 error 210 | stops2 int 211 | done2 = make(chan struct{}) 212 | err2 error 213 | ) 214 | 215 | if err := m.Add( 216 | mockTicker{ 217 | done: func() <-chan struct{} { 218 | return done1 219 | }, 220 | err: func() error { 221 | mutex.Lock() 222 | defer mutex.Unlock() 223 | return err1 224 | }, 225 | stop: func() { 226 | mutex.Lock() 227 | defer mutex.Unlock() 228 | stops1++ 229 | }, 230 | }, 231 | ); err != nil { 232 | t.Fatal(err) 233 | } 234 | 235 | if err := m.Add( 236 | mockTicker{ 237 | done: func() <-chan struct{} { 238 | return done2 239 | }, 240 | err: func() error { 241 | mutex.Lock() 242 | defer mutex.Unlock() 243 | return err2 244 | }, 245 | stop: func() { 246 | mutex.Lock() 247 | defer mutex.Unlock() 248 | stops2++ 249 | }, 250 | }, 251 | ); err != nil { 252 | t.Fatal(err) 253 | } 254 | 255 | if d := m.Done(); d != m.done || d == nil { 256 | t.Error(d) 257 | } 258 | if err := m.Err(); err != nil { 259 | t.Error(err) 260 | } 261 | select { 262 | case <-m.stop: 263 | t.Error() 264 | default: 265 | } 266 | select { 267 | case <-m.done: 268 | t.Error() 269 | default: 270 | } 271 | 272 | mutex.Lock() 273 | err2 = errors.New("some_error") 274 | close(done2) 275 | mutex.Unlock() 276 | 277 | time.Sleep(time.Millisecond * 100) 278 | 279 | mutex.Lock() 280 | if stops2 != 1 { 281 | t.Error(stops2) 282 | } 283 | if stops1 != 1 { 284 | t.Error(stops1) 285 | } 286 | err1 = errors.New("other_error") 287 | mutex.Unlock() 288 | 289 | select { 290 | case <-m.Done(): 291 | t.Error("unexpected done") 292 | default: 293 | } 294 | 295 | close(done1) 296 | 297 | <-m.Done() 298 | 299 | checkErrTicker := func(err error) { 300 | t.Helper() 301 | if err == nil || err.Error() != "some_error | other_error" { 302 | t.Error(err) 303 | } 304 | if !errors.Is(err, err1) { 305 | t.Error(err) 306 | } 307 | if !errors.Is(err, err2) { 308 | t.Error(err) 309 | } 310 | if errors.Is(err, errors.New(`another_error`)) { 311 | t.Error(err) 312 | } 313 | { 314 | err := err 315 | if v, ok := err.(errManagerStopped); ok { 316 | err = v.Unwrap() 317 | } 318 | if v, ok := err.(errManagerTicker); !ok || len(v) != 2 { 319 | t.Error(err) 320 | } 321 | } 322 | } 323 | checkErrStopped := func(err error) { 324 | t.Helper() 325 | if !errors.Is(err.(errManagerStopped), ErrManagerStopped.(errManagerStopped)) || 326 | !(errManagerStopped{}).Is(err) || 327 | !err.(interface{ Is(error) bool }).Is(errManagerStopped{}) { 328 | t.Error(err) 329 | } 330 | } 331 | 332 | checkErrTicker(m.Err()) 333 | { 334 | err := m.Add(mockTicker{}) 335 | checkErrTicker(err) 336 | checkErrStopped(err) 337 | } 338 | 339 | // does nothing 340 | m.Stop() 341 | 342 | checkErrTicker(m.Err()) 343 | { 344 | err := m.Add(mockTicker{}) 345 | checkErrTicker(err) 346 | checkErrStopped(err) 347 | } 348 | 349 | m.errs = nil 350 | if err := m.Add(mockTicker{}); err != ErrManagerStopped { 351 | t.Error("expected error") 352 | } 353 | if err := m.Add(nil); err == nil { 354 | t.Error("expected error") 355 | } 356 | } 357 | 358 | type mockTicker struct { 359 | done func() <-chan struct{} 360 | err func() error 361 | stop func() 362 | } 363 | 364 | func (m mockTicker) Done() <-chan struct{} { 365 | if m.done != nil { 366 | return m.done() 367 | } 368 | panic("implement me") 369 | } 370 | 371 | func (m mockTicker) Err() error { 372 | if m.err != nil { 373 | return m.err() 374 | } 375 | panic("implement me") 376 | } 377 | 378 | func (m mockTicker) Stop() { 379 | if m.stop != nil { 380 | m.stop() 381 | return 382 | } 383 | panic("implement me") 384 | } 385 | 386 | const ( 387 | waitNumGoroutinesDefault = time.Millisecond * 200 388 | waitNumGoroutinesNumerator = 1 389 | waitNumGoroutinesDenominator = 1 390 | waitNumGoroutinesMin = time.Millisecond * 50 391 | ) 392 | 393 | func waitNumGoroutines(wait time.Duration, fn func(n int) bool) (n int) { 394 | if wait == 0 { 395 | wait = waitNumGoroutinesDefault 396 | } 397 | wait *= waitNumGoroutinesNumerator 398 | wait /= waitNumGoroutinesDenominator 399 | if wait < waitNumGoroutinesMin { 400 | wait = waitNumGoroutinesMin 401 | } 402 | count := int(wait / waitNumGoroutinesMin) 403 | wait /= time.Duration(count) 404 | n = runtime.NumGoroutine() 405 | for i := 0; i < count && !fn(n); i++ { 406 | time.Sleep(wait) 407 | runtime.GC() 408 | n = runtime.NumGoroutine() 409 | } 410 | return 411 | } 412 | 413 | // checkNumGoroutines is used to indirectly test goroutine state / cleanup, the reliance on timing isn't great, but I 414 | // wasn't able to come up with a better solution 415 | func checkNumGoroutines(t *testing.T) func(increase bool, wait time.Duration) { 416 | if t != nil { 417 | t.Helper() 418 | } 419 | var ( 420 | errorf = func(format string, values ...interface{}) { 421 | if err := fmt.Errorf(format, values...); t != nil { 422 | t.Error(err) 423 | } else { 424 | panic(err) 425 | } 426 | } 427 | start = runtime.NumGoroutine() 428 | ) 429 | return func(increase bool, wait time.Duration) { 430 | if t != nil { 431 | t.Helper() 432 | } 433 | var fn func(n int) bool 434 | if increase { 435 | fn = func(n int) bool { return start < n } 436 | } else { 437 | fn = func(n int) bool { return start >= n } 438 | } 439 | if now := waitNumGoroutines(wait, fn); increase { 440 | if start >= now { 441 | errorf("too few goroutines: -%d", start-now+1) 442 | } 443 | } else if start < now { 444 | errorf("too many goroutines: +%d", now-start) 445 | } 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Joseph Cumines 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 Joseph Cumines 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package behaviortree 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "sync" 23 | "time" 24 | ) 25 | 26 | func newExampleCounter() Node { 27 | var ( 28 | // counter is the shared state used by this example 29 | counter = 0 30 | // printCounter returns a node that will print the counter prefixed with the given name then succeed 31 | printCounter = func(name string) Node { 32 | return New( 33 | func(children []Node) (Status, error) { 34 | fmt.Printf("%s: %d\n", name, counter) 35 | return Success, nil 36 | }, 37 | ) 38 | } 39 | // incrementCounter is a node that will increment counter then succeed 40 | incrementCounter = New( 41 | func(children []Node) (Status, error) { 42 | counter++ 43 | return Success, nil 44 | }, 45 | ) 46 | ) 47 | return New( 48 | Selector, // runs each child sequentially until one succeeds (success) or all fail (failure) 49 | New( 50 | Sequence, // runs each child in order until one fails (failure) or they all succeed (success) 51 | New( 52 | func(children []Node) (Status, error) { // succeeds while counter is less than 10 53 | if counter < 10 { 54 | return Success, nil 55 | } 56 | return Failure, nil 57 | }, 58 | ), 59 | incrementCounter, 60 | printCounter("< 10"), 61 | ), 62 | New( 63 | Sequence, 64 | New( 65 | func(children []Node) (Status, error) { // succeeds while counter is less than 20 66 | if counter < 20 { 67 | return Success, nil 68 | } 69 | return Failure, nil 70 | }, 71 | ), 72 | incrementCounter, 73 | printCounter("< 20"), 74 | ), 75 | ) // if both children failed (counter is >= 20) the root node will also fail 76 | } 77 | 78 | // ExampleNewTickerStopOnFailure_counter demonstrates the use of NewTickerStopOnFailure to implement more complex "run 79 | // to completion" behavior using the simple modular building blocks provided by this package 80 | func ExampleNewTickerStopOnFailure_counter() { 81 | // ticker is what actually runs this example and will tick the behavior tree defined by a given node at a given 82 | // rate and will stop after the first failed tick or error or context cancel 83 | ticker := NewTickerStopOnFailure( 84 | context.Background(), 85 | time.Millisecond, 86 | newExampleCounter(), 87 | ) 88 | // waits until ticker stops, which will be on the first failure of it's root node 89 | <-ticker.Done() 90 | // every Tick may return an error which would automatically cause a failure and propagation of the error 91 | if err := ticker.Err(); err != nil { 92 | panic(err) 93 | } 94 | // Output: 95 | // < 10: 1 96 | // < 10: 2 97 | // < 10: 3 98 | // < 10: 4 99 | // < 10: 5 100 | // < 10: 6 101 | // < 10: 7 102 | // < 10: 8 103 | // < 10: 9 104 | // < 10: 10 105 | // < 20: 11 106 | // < 20: 12 107 | // < 20: 13 108 | // < 20: 14 109 | // < 20: 15 110 | // < 20: 16 111 | // < 20: 17 112 | // < 20: 18 113 | // < 20: 19 114 | // < 20: 20 115 | } 116 | 117 | // ExampleMemorize_cancellationWithContextCancel demonstrates how support for reactive logic that uses context can 118 | // be implemented 119 | func ExampleMemorize_cancellationWithContextCancel() { 120 | type Str string 121 | var ( 122 | ctx context.Context 123 | cancel context.CancelFunc 124 | debug = func(label string, tick Tick) Tick { 125 | return func(children []Node) (status Status, err error) { 126 | status, err = tick(children) 127 | fmt.Printf("%s returned (%v, %v)\n", label, status, err) 128 | return 129 | } 130 | } 131 | recorded = func(statuses ...Status) Tick { 132 | return func([]Node) (status Status, err error) { 133 | status = statuses[0] 134 | statuses = statuses[1:] 135 | return 136 | } 137 | } 138 | counter int 139 | ticker = NewTickerStopOnFailure( 140 | context.Background(), 141 | time.Millisecond, 142 | New( 143 | All, 144 | New( 145 | Memorize(debug(`memorized`, All)), 146 | New(func([]Node) (Status, error) { 147 | counter++ 148 | ctx, cancel = context.WithCancel(context.WithValue(context.Background(), Str(`n`), counter)) 149 | return Success, nil 150 | }), // prepare the context 151 | New( 152 | debug(`sequence`, Sequence), 153 | New(debug(`guard`, recorded( 154 | Success, 155 | Success, 156 | Success, 157 | Success, 158 | Failure, 159 | ))), 160 | New(func([]Node) (Status, error) { 161 | fmt.Printf("[start action] context #%d's err=%v\n", ctx.Value(Str(`n`)), ctx.Err()) 162 | return Success, nil 163 | }), 164 | New(debug(`action`, recorded( 165 | Running, 166 | Running, 167 | Success, 168 | Running, 169 | ))), 170 | ), 171 | New(func([]Node) (Status, error) { 172 | cancel() 173 | return Success, nil 174 | }), // cancel the context 175 | ), 176 | New(func([]Node) (Status, error) { 177 | fmt.Printf("[end memorized] context #%d's err=%v\n", ctx.Value(Str(`n`)), ctx.Err()) 178 | return Success, nil 179 | }), 180 | ), 181 | ) 182 | ) 183 | 184 | <-ticker.Done() 185 | if err := ticker.Err(); err != nil { 186 | panic(err) 187 | } 188 | //output: 189 | //guard returned (success, ) 190 | //[start action] context #1's err= 191 | //action returned (running, ) 192 | //sequence returned (running, ) 193 | //memorized returned (running, ) 194 | //guard returned (success, ) 195 | //[start action] context #1's err= 196 | //action returned (running, ) 197 | //sequence returned (running, ) 198 | //memorized returned (running, ) 199 | //guard returned (success, ) 200 | //[start action] context #1's err= 201 | //action returned (success, ) 202 | //sequence returned (success, ) 203 | //memorized returned (success, ) 204 | //[end memorized] context #1's err=context canceled 205 | //guard returned (success, ) 206 | //[start action] context #2's err= 207 | //action returned (running, ) 208 | //sequence returned (running, ) 209 | //memorized returned (running, ) 210 | //guard returned (failure, ) 211 | //sequence returned (failure, ) 212 | //memorized returned (failure, ) 213 | //[end memorized] context #2's err=context canceled 214 | } 215 | 216 | // ExampleBackground_asyncJobQueue implements a basic example of backgrounding of long-running tasks that may be 217 | // performed concurrently, see ExampleNewTickerStopOnFailure_counter for an explanation of the ticker 218 | func ExampleBackground_asyncJobQueue() { 219 | type ( 220 | Job struct { 221 | Name string 222 | Duration time.Duration 223 | Done chan struct{} 224 | } 225 | ) 226 | var ( 227 | // doWorker performs the actual "work" for a Job 228 | doWorker = func(job Job) { 229 | fmt.Printf("[worker] job \"%s\" STARTED\n", job.Name) 230 | time.Sleep(job.Duration) 231 | fmt.Printf("[worker] job \"%s\" FINISHED\n", job.Name) 232 | close(job.Done) 233 | } 234 | // queue be sent jobs, which will be received within the ticker 235 | queue = make(chan Job, 50) 236 | // doClient sends and waits for a job 237 | doClient = func(name string, duration time.Duration) { 238 | job := Job{name, duration, make(chan struct{})} 239 | ts := time.Now() 240 | fmt.Printf("[client] job \"%s\" STARTED\n", job.Name) 241 | queue <- job 242 | <-job.Done 243 | fmt.Printf("[client] job \"%s\" FINISHED\n", job.Name) 244 | t := time.Since(ts) 245 | d := t - job.Duration 246 | if d < 0 { 247 | d *= -1 248 | } 249 | if d > time.Millisecond*50 { 250 | panic(fmt.Errorf(`job "%s" expected %s actual %s`, job.Name, job.Duration.String(), t.String())) 251 | } 252 | } 253 | // running keeps track of the number of running jobs 254 | running = func() func(delta int64) int64 { 255 | var ( 256 | value int64 257 | mutex sync.Mutex 258 | ) 259 | return func(delta int64) int64 { 260 | mutex.Lock() 261 | defer mutex.Unlock() 262 | value += delta 263 | return value 264 | } 265 | }() 266 | // done will be closed when it's time to exit the ticker 267 | done = make(chan struct{}) 268 | ticker = NewTickerStopOnFailure( 269 | context.Background(), 270 | time.Millisecond, 271 | New( 272 | Sequence, 273 | New(func(children []Node) (Status, error) { 274 | select { 275 | case <-done: 276 | return Failure, nil 277 | default: 278 | return Success, nil 279 | } 280 | }), 281 | func() Node { 282 | // the tick is initialised once, and is stateful (though the tick it's wrapping isn't) 283 | tick := Background(func() Tick { return Selector }) 284 | return func() (Tick, []Node) { 285 | // this block will be refreshed each time that a new job is started 286 | var ( 287 | job Job 288 | ) 289 | return tick, []Node{ 290 | New( 291 | Sequence, 292 | Sync([]Node{ 293 | New(func(children []Node) (Status, error) { 294 | select { 295 | case job = <-queue: 296 | running(1) 297 | return Success, nil 298 | default: 299 | return Failure, nil 300 | } 301 | }), 302 | New(Async(func(children []Node) (Status, error) { 303 | defer running(-1) 304 | doWorker(job) 305 | return Success, nil 306 | })), 307 | })..., 308 | ), 309 | // no job available - success 310 | New(func(children []Node) (Status, error) { 311 | return Success, nil 312 | }), 313 | } 314 | } 315 | }(), 316 | ), 317 | ) 318 | wg sync.WaitGroup 319 | ) 320 | wg.Add(1) 321 | run := func(name string, duration time.Duration) { 322 | wg.Add(1) 323 | defer wg.Done() 324 | doClient(name, duration) 325 | } 326 | 327 | fmt.Printf("running jobs: %d\n", running(0)) 328 | 329 | go run(`1. 120ms`, time.Millisecond*120) 330 | time.Sleep(time.Millisecond * 25) 331 | go run(`2. 70ms`, time.Millisecond*70) 332 | time.Sleep(time.Millisecond * 25) 333 | fmt.Printf("running jobs: %d\n", running(0)) 334 | 335 | doClient(`3. 150ms`, time.Millisecond*150) 336 | time.Sleep(time.Millisecond * 50) 337 | fmt.Printf("running jobs: %d\n", running(0)) 338 | 339 | time.Sleep(time.Millisecond * 50) 340 | wg.Done() 341 | wg.Wait() 342 | close(done) 343 | <-ticker.Done() 344 | if err := ticker.Err(); err != nil { 345 | panic(err) 346 | } 347 | //output: 348 | //running jobs: 0 349 | //[client] job "1. 120ms" STARTED 350 | //[worker] job "1. 120ms" STARTED 351 | //[client] job "2. 70ms" STARTED 352 | //[worker] job "2. 70ms" STARTED 353 | //running jobs: 2 354 | //[client] job "3. 150ms" STARTED 355 | //[worker] job "3. 150ms" STARTED 356 | //[worker] job "2. 70ms" FINISHED 357 | //[client] job "2. 70ms" FINISHED 358 | //[worker] job "1. 120ms" FINISHED 359 | //[client] job "1. 120ms" FINISHED 360 | //[worker] job "3. 150ms" FINISHED 361 | //[client] job "3. 150ms" FINISHED 362 | //running jobs: 0 363 | } 364 | 365 | // ExampleContext demonstrates how the Context implementation may be used to integrate with the context package 366 | func ExampleContext() { 367 | ctx, cancel := context.WithCancel(context.Background()) 368 | defer cancel() 369 | 370 | var ( 371 | btCtx = new(Context).WithTimeout(ctx, time.Millisecond*100) 372 | debug = func(args ...interface{}) Tick { 373 | return func([]Node) (Status, error) { 374 | fmt.Println(args...) 375 | return Success, nil 376 | } 377 | } 378 | counter int 379 | counterEqual = func(v int) Tick { 380 | return func([]Node) (Status, error) { 381 | if counter == v { 382 | return Success, nil 383 | } 384 | return Failure, nil 385 | } 386 | } 387 | counterInc Tick = func([]Node) (Status, error) { 388 | counter++ 389 | //fmt.Printf("counter = %d\n", counter) 390 | return Success, nil 391 | } 392 | ticker = NewTicker(ctx, time.Millisecond, New( 393 | Sequence, 394 | New( 395 | Selector, 396 | New(Not(btCtx.Err)), 397 | New( 398 | Sequence, 399 | New(debug(`(re)initialising btCtx...`)), 400 | New(btCtx.Init), 401 | New(Not(btCtx.Err)), 402 | ), 403 | ), 404 | New( 405 | Selector, 406 | New( 407 | Sequence, 408 | New(counterEqual(0)), 409 | New(debug(`blocking on context-enabled tick...`)), 410 | New( 411 | btCtx.Tick(func(ctx context.Context, children []Node) (Status, error) { 412 | fmt.Printf("NOTE children (%d) passed through\n", len(children)) 413 | <-ctx.Done() 414 | return Success, nil 415 | }), 416 | New(Sequence), 417 | New(Sequence), 418 | ), 419 | New(counterInc), 420 | ), 421 | New( 422 | Sequence, 423 | New(counterEqual(1)), 424 | New(debug(`blocking on done...`)), 425 | New(btCtx.Done), 426 | New(counterInc), 427 | ), 428 | New( 429 | Sequence, 430 | New(counterEqual(2)), 431 | New(debug(`canceling local then rechecking the above...`)), 432 | New(btCtx.Cancel), 433 | New(btCtx.Err), 434 | New(btCtx.Tick(func(ctx context.Context, children []Node) (Status, error) { 435 | <-ctx.Done() 436 | return Success, nil 437 | })), 438 | New(btCtx.Done), 439 | New(counterInc), 440 | ), 441 | New( 442 | Sequence, 443 | New(counterEqual(3)), 444 | New(debug(`canceling parent then rechecking the above...`)), 445 | New(func([]Node) (Status, error) { 446 | cancel() 447 | return Success, nil 448 | }), 449 | New(btCtx.Err), 450 | New(btCtx.Tick(func(ctx context.Context, children []Node) (Status, error) { 451 | <-ctx.Done() 452 | return Success, nil 453 | })), 454 | New(btCtx.Done), 455 | New(debug(`exiting...`)), 456 | ), 457 | ), 458 | )) 459 | ) 460 | 461 | <-ticker.Done() 462 | 463 | //output: 464 | //(re)initialising btCtx... 465 | //blocking on context-enabled tick... 466 | //NOTE children (2) passed through 467 | //(re)initialising btCtx... 468 | //blocking on done... 469 | //(re)initialising btCtx... 470 | //canceling local then rechecking the above... 471 | //(re)initialising btCtx... 472 | //canceling parent then rechecking the above... 473 | //exiting... 474 | } 475 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/joeycumines/go-behaviortree)](https://goreportcard.com/report/joeycumines/go-behaviortree) 2 | 3 | # go-behaviortree 4 | 5 | Package behaviortree provides a simple and powerful Go implementation of behavior trees without fluff. 6 | 7 | Go doc: [https://godoc.org/github.com/joeycumines/go-behaviortree](https://godoc.org/github.com/joeycumines/go-behaviortree) 8 | 9 | Wikipedia: [Behavior tree - AI, robotics, and control](https://en.wikipedia.org/wiki/Behavior_tree_(artificial_intelligence,_robotics_and_control)) 10 | 11 | ```go 12 | type ( 13 | // Node represents an node in a tree, that can be ticked 14 | Node func() (Tick, []Node) 15 | 16 | // Tick represents the logic for a node, which may or may not be stateful 17 | Tick func(children []Node) (Status, error) 18 | 19 | // Status is a type with three valid values, Running, Success, and Failure, the three possible states for BTs 20 | Status int 21 | ) 22 | 23 | // Tick runs the node's tick function with it's children 24 | func (n Node) Tick() (Status, error) 25 | ``` 26 | 27 | ## Features 28 | 29 | - Core behavior tree implementation (the types above + `Sequence` and `Selector`) 30 | - Tools to aide implementation of "reactive" behavior trees (`Memorize`, `Async`, `Sync`) 31 | - Implementations to run and manage behavior trees (`NewManager`, `NewTicker`) 32 | - Collection of `Tick` implementations / wrappers (targeting various use cases) 33 | - Context-like mechanism to attach metadata to `Node` values that can transit API boundaries / encapsulation 34 | - Basic tree debugging capabilities via implementation of `fmt.Stringer` (see also `DefaultPrinter`, `Node.Frame`) 35 | - Experimental support for the PA-BT planning algorithm via [github.com/joeycumines/go-pabt](https://github.com/joeycumines/go-pabt) 36 | 37 | ## Design 38 | 39 | This library provides an implementation of the generalised behavior tree pattern. The three types above along with the 40 | stateless `Tick` functions `Selector` and `Sequence` (line for line from Wikipedia) make up the entirety of the core 41 | functionality. Additional features have been derived by means of iterating against specific, real-world problem cases. 42 | 43 | It is recommended that anyone interested in understanding behavior trees read from the start of chapter 1 of 44 | [Colledanchise, Michele & Ogren, Petter. (2018). Behavior Trees in Robotics and AI: An Introduction. 10.1201/9780429489105.](https://www.researchgate.net/publication/319463746_Behavior_Trees_in_Robotics_and_AI_An_Introduction) 45 | until at least (the end of) `1.3.2 - Control Flow Nodes with Memory`. Further context should (hopefully) go a long way 46 | in aiding newcomers to gain a firm grasp of the relevant behavior and associated patterns, in order to start building 47 | automations using behavior trees. 48 | 49 | N.B. Though it does appear to be an excellent resource, _Behavior Trees in Robotics and AI: An Introduction._ was only 50 | brought to my attention well after the release of `v1.3.0`. In particular, concepts and terms are unlikely to be 51 | entirely consistent. 52 | 53 | ### Reactivity 54 | 55 | Executing a behavior tree sequentially (i.e. without the use of nodes that return `Running`) can be an effective way 56 | to decompose complicated switching logic. Though effective, this design suffers from limitations common to traditional 57 | finite state machines, which tends to cripple the modularity of any interruptable operations, as each tick must manage 58 | it's own exit condition(s). Golang's `context` doesn't really help in that case, either, being a communication 59 | mechanism, rather than a control mechanism. As an alternative, implementations may use pre-conditions (preceding 60 | child(ren) in a sequence), guarding an asynchronous tick. Context support may be desirable, and may be implemented as 61 | `Tick` implementations(s). This package provides a `Context` implementation to address several common use cases, such 62 | as operations with timeouts. Context support is peripheral in that it's only relevant to a subset of implementations, 63 | and was deliberately omitted from the core `Tick` type. 64 | 65 | ### Modularity 66 | 67 | This library **only** concerns itself with the task of actually running behavior trees. It is deliberately designed 68 | to make it straightforward to plug in external implementations. External implementations _may_ be integrated as a 69 | fully-fledged recursively generated tree of `Node`. Tree-aware debug tracing could be implemented using a similar 70 | mechanism. At the end of the day though, implementations just need to `Tick`. 71 | 72 | ## Implementation 73 | 74 | This section attempts to provide better insight into the _intent_ of certain implementation details. Part of this 75 | involves offering implementation **guidelines**, based on _imagined_ use cases (beyond those explored as part of each 76 | implementation). It ain't gospel. 77 | 78 | ### Executing BTs 79 | 80 | #### Use explicit exit conditions 81 | 82 | - Return `error` to handle failures that should terminate tree execution 83 | - BTs that "run until completion" may be implemented using `NewTickerStopOnFailure` (internally it just returns 84 | an error then strips any occurrence of that that error from the ticker's result) 85 | - Panics may be used as normal, and are suitable for cases such unrecoverable errors that _shouldn't_ happen 86 | 87 | ### Shared state 88 | 89 | #### `Tick` implementations should be grouped in a way that makes sense, in the context of any shared state 90 | 91 | - It may be convenient to expose a group of `Tick` prototypes with shared state as methods of a struct 92 | - Global state should be avoided for all the regular reasons, but especially since it defeats a lot of the point 93 | of having modular, composable behavior 94 | 95 | #### Encapsulate `Tick` implementations, rather than `Node` 96 | 97 | - Children may be may be modified, but only until the (outer) `Tick` returns it's next non-running status 98 | - This mechanism _theoretically_ facilitates dynamically generated trees while simultaneously supporting more complex 99 | / concurrency-heavy implementations made up of reusable building blocks 100 | 101 | ## Roadmap 102 | 103 | I am actively maintaining this project, and will be for the foreseeable future. It has been "feature complete" for 104 | some time though, so additional functionality will assessed on a case-by-case basis. 105 | 106 | ## Example Usage 107 | 108 | The examples below are straight from `example_test.go`. 109 | 110 | ```go 111 | // ExampleNewTickerStopOnFailure_counter demonstrates the use of NewTickerStopOnFailure to implement more complex "run 112 | // to completion" behavior using the simple modular building blocks provided by this package 113 | func ExampleNewTickerStopOnFailure_counter() { 114 | var ( 115 | // counter is the shared state used by this example 116 | counter = 0 117 | // printCounter returns a node that will print the counter prefixed with the given name then succeed 118 | printCounter = func(name string) Node { 119 | return New( 120 | func(children []Node) (Status, error) { 121 | fmt.Printf("%s: %d\n", name, counter) 122 | return Success, nil 123 | }, 124 | ) 125 | } 126 | // incrementCounter is a node that will increment counter then succeed 127 | incrementCounter = New( 128 | func(children []Node) (Status, error) { 129 | counter++ 130 | return Success, nil 131 | }, 132 | ) 133 | // ticker is what actually runs this example and will tick the behavior tree defined by a given node at a given 134 | // rate and will stop after the first failed tick or error or context cancel 135 | ticker = NewTickerStopOnFailure( 136 | context.Background(), 137 | time.Millisecond, 138 | New( 139 | Selector, // runs each child sequentially until one succeeds (success) or all fail (failure) 140 | New( 141 | Sequence, // runs each child in order until one fails (failure) or they all succeed (success) 142 | New( 143 | func(children []Node) (Status, error) { // succeeds while counter is less than 10 144 | if counter < 10 { 145 | return Success, nil 146 | } 147 | return Failure, nil 148 | }, 149 | ), 150 | incrementCounter, 151 | printCounter("< 10"), 152 | ), 153 | New( 154 | Sequence, 155 | New( 156 | func(children []Node) (Status, error) { // succeeds while counter is less than 20 157 | if counter < 20 { 158 | return Success, nil 159 | } 160 | return Failure, nil 161 | }, 162 | ), 163 | incrementCounter, 164 | printCounter("< 20"), 165 | ), 166 | ), // if both children failed (counter is >= 20) the root node will also fail 167 | ) 168 | ) 169 | // waits until ticker stops, which will be on the first failure of it's root node 170 | <-ticker.Done() 171 | // every Tick may return an error which would automatically cause a failure and propagation of the error 172 | if err := ticker.Err(); err != nil { 173 | panic(err) 174 | } 175 | // Output: 176 | // < 10: 1 177 | // < 10: 2 178 | // < 10: 3 179 | // < 10: 4 180 | // < 10: 5 181 | // < 10: 6 182 | // < 10: 7 183 | // < 10: 8 184 | // < 10: 9 185 | // < 10: 10 186 | // < 20: 11 187 | // < 20: 12 188 | // < 20: 13 189 | // < 20: 14 190 | // < 20: 15 191 | // < 20: 16 192 | // < 20: 17 193 | // < 20: 18 194 | // < 20: 19 195 | // < 20: 20 196 | } 197 | 198 | // ExampleMemorize_cancellationWithContextCancel demonstrates how support for reactive logic that uses context can 199 | // be implemented 200 | func ExampleMemorize_cancellationWithContextCancel() { 201 | var ( 202 | ctx context.Context 203 | cancel context.CancelFunc 204 | debug = func(label string, tick Tick) Tick { 205 | return func(children []Node) (status Status, err error) { 206 | status, err = tick(children) 207 | fmt.Printf("%s returned (%v, %v)\n", label, status, err) 208 | return 209 | } 210 | } 211 | recorded = func(statuses ...Status) Tick { 212 | return func([]Node) (status Status, err error) { 213 | status = statuses[0] 214 | statuses = statuses[1:] 215 | return 216 | } 217 | } 218 | counter int 219 | ticker = NewTickerStopOnFailure( 220 | context.Background(), 221 | time.Millisecond, 222 | New( 223 | All, 224 | New( 225 | Memorize(debug(`memorized`, All)), 226 | New(func([]Node) (Status, error) { 227 | counter++ 228 | ctx, cancel = context.WithCancel(context.WithValue(context.Background(), `n`, counter)) 229 | return Success, nil 230 | }), // prepare the context 231 | New( 232 | debug(`sequence`, Sequence), 233 | New(debug(`guard`, recorded( 234 | Success, 235 | Success, 236 | Success, 237 | Success, 238 | Failure, 239 | ))), 240 | New(func([]Node) (Status, error) { 241 | fmt.Printf("[start action] context #%d's err=%v\n", ctx.Value(`n`), ctx.Err()) 242 | return Success, nil 243 | }), 244 | New(debug(`action`, recorded( 245 | Running, 246 | Running, 247 | Success, 248 | Running, 249 | ))), 250 | ), 251 | New(func([]Node) (Status, error) { 252 | cancel() 253 | return Success, nil 254 | }), // cancel the context 255 | ), 256 | New(func([]Node) (Status, error) { 257 | fmt.Printf("[end memorized] context #%d's err=%v\n", ctx.Value(`n`), ctx.Err()) 258 | return Success, nil 259 | }), 260 | ), 261 | ) 262 | ) 263 | 264 | <-ticker.Done() 265 | if err := ticker.Err(); err != nil { 266 | panic(err) 267 | } 268 | //output: 269 | //guard returned (success, ) 270 | //[start action] context #1's err= 271 | //action returned (running, ) 272 | //sequence returned (running, ) 273 | //memorized returned (running, ) 274 | //guard returned (success, ) 275 | //[start action] context #1's err= 276 | //action returned (running, ) 277 | //sequence returned (running, ) 278 | //memorized returned (running, ) 279 | //guard returned (success, ) 280 | //[start action] context #1's err= 281 | //action returned (success, ) 282 | //sequence returned (success, ) 283 | //memorized returned (success, ) 284 | //[end memorized] context #1's err=context canceled 285 | //guard returned (success, ) 286 | //[start action] context #2's err= 287 | //action returned (running, ) 288 | //sequence returned (running, ) 289 | //memorized returned (running, ) 290 | //guard returned (failure, ) 291 | //sequence returned (failure, ) 292 | //memorized returned (failure, ) 293 | //[end memorized] context #2's err=context canceled 294 | } 295 | 296 | // ExampleBackground_asyncJobQueue implements a basic example of backgrounding of long-running tasks that may be 297 | // performed concurrently, see ExampleNewTickerStopOnFailure_counter for an explanation of the ticker 298 | func ExampleBackground_asyncJobQueue() { 299 | type ( 300 | Job struct { 301 | Name string 302 | Duration time.Duration 303 | Done chan struct{} 304 | } 305 | ) 306 | var ( 307 | // doWorker performs the actual "work" for a Job 308 | doWorker = func(job Job) { 309 | fmt.Printf("[worker] job \"%s\" STARTED\n", job.Name) 310 | time.Sleep(job.Duration) 311 | fmt.Printf("[worker] job \"%s\" FINISHED\n", job.Name) 312 | close(job.Done) 313 | } 314 | // queue be sent jobs, which will be received within the ticker 315 | queue = make(chan Job, 50) 316 | // doClient sends and waits for a job 317 | doClient = func(name string, duration time.Duration) { 318 | job := Job{name, duration, make(chan struct{})} 319 | ts := time.Now() 320 | fmt.Printf("[client] job \"%s\" STARTED\n", job.Name) 321 | queue <- job 322 | <-job.Done 323 | fmt.Printf("[client] job \"%s\" FINISHED\n", job.Name) 324 | t := time.Now().Sub(ts) 325 | d := t - job.Duration 326 | if d < 0 { 327 | d *= -1 328 | } 329 | if d > time.Millisecond*50 { 330 | panic(fmt.Errorf(`job "%s" expected %s actual %s`, job.Name, job.Duration.String(), t.String())) 331 | } 332 | } 333 | // running keeps track of the number of running jobs 334 | running = func() func(delta int64) int64 { 335 | var ( 336 | value int64 337 | mutex sync.Mutex 338 | ) 339 | return func(delta int64) int64 { 340 | mutex.Lock() 341 | defer mutex.Unlock() 342 | value += delta 343 | return value 344 | } 345 | }() 346 | // done will be closed when it's time to exit the ticker 347 | done = make(chan struct{}) 348 | ticker = NewTickerStopOnFailure( 349 | context.Background(), 350 | time.Millisecond, 351 | New( 352 | Sequence, 353 | New(func(children []Node) (Status, error) { 354 | select { 355 | case <-done: 356 | return Failure, nil 357 | default: 358 | return Success, nil 359 | } 360 | }), 361 | func() Node { 362 | // the tick is initialised once, and is stateful (though the tick it's wrapping isn't) 363 | tick := Background(func() Tick { return Selector }) 364 | return func() (Tick, []Node) { 365 | // this block will be refreshed each time that a new job is started 366 | var ( 367 | job Job 368 | ) 369 | return tick, []Node{ 370 | New( 371 | Sequence, 372 | Sync([]Node{ 373 | New(func(children []Node) (Status, error) { 374 | select { 375 | case job = <-queue: 376 | running(1) 377 | return Success, nil 378 | default: 379 | return Failure, nil 380 | } 381 | }), 382 | New(Async(func(children []Node) (Status, error) { 383 | defer running(-1) 384 | doWorker(job) 385 | return Success, nil 386 | })), 387 | })..., 388 | ), 389 | // no job available - success 390 | New(func(children []Node) (Status, error) { 391 | return Success, nil 392 | }), 393 | } 394 | } 395 | }(), 396 | ), 397 | ) 398 | wg sync.WaitGroup 399 | ) 400 | wg.Add(1) 401 | run := func(name string, duration time.Duration) { 402 | wg.Add(1) 403 | defer wg.Done() 404 | doClient(name, duration) 405 | } 406 | 407 | fmt.Printf("running jobs: %d\n", running(0)) 408 | 409 | go run(`1. 120ms`, time.Millisecond*120) 410 | time.Sleep(time.Millisecond * 25) 411 | go run(`2. 70ms`, time.Millisecond*70) 412 | time.Sleep(time.Millisecond * 25) 413 | fmt.Printf("running jobs: %d\n", running(0)) 414 | 415 | doClient(`3. 150ms`, time.Millisecond*150) 416 | time.Sleep(time.Millisecond * 50) 417 | fmt.Printf("running jobs: %d\n", running(0)) 418 | 419 | time.Sleep(time.Millisecond * 50) 420 | wg.Done() 421 | wg.Wait() 422 | close(done) 423 | <-ticker.Done() 424 | if err := ticker.Err(); err != nil { 425 | panic(err) 426 | } 427 | //output: 428 | //running jobs: 0 429 | //[client] job "1. 120ms" STARTED 430 | //[worker] job "1. 120ms" STARTED 431 | //[client] job "2. 70ms" STARTED 432 | //[worker] job "2. 70ms" STARTED 433 | //running jobs: 2 434 | //[client] job "3. 150ms" STARTED 435 | //[worker] job "3. 150ms" STARTED 436 | //[worker] job "2. 70ms" FINISHED 437 | //[client] job "2. 70ms" FINISHED 438 | //[worker] job "1. 120ms" FINISHED 439 | //[client] job "1. 120ms" FINISHED 440 | //[worker] job "3. 150ms" FINISHED 441 | //[client] job "3. 150ms" FINISHED 442 | //running jobs: 0 443 | } 444 | 445 | 446 | // ExampleContext demonstrates how the Context implementation may be used to integrate with the context package 447 | func ExampleContext() { 448 | ctx, cancel := context.WithCancel(context.Background()) 449 | defer cancel() 450 | 451 | var ( 452 | btCtx = new(Context).WithTimeout(ctx, time.Millisecond*100) 453 | debug = func(args ...interface{}) Tick { 454 | return func([]Node) (Status, error) { 455 | fmt.Println(args...) 456 | return Success, nil 457 | } 458 | } 459 | counter int 460 | counterEqual = func(v int) Tick { 461 | return func([]Node) (Status, error) { 462 | if counter == v { 463 | return Success, nil 464 | } 465 | return Failure, nil 466 | } 467 | } 468 | counterInc Tick = func([]Node) (Status, error) { 469 | counter++ 470 | //fmt.Printf("counter = %d\n", counter) 471 | return Success, nil 472 | } 473 | ticker = NewTicker(ctx, time.Millisecond, New( 474 | Sequence, 475 | New( 476 | Selector, 477 | New(Not(btCtx.Err)), 478 | New( 479 | Sequence, 480 | New(debug(`(re)initialising btCtx...`)), 481 | New(btCtx.Init), 482 | New(Not(btCtx.Err)), 483 | ), 484 | ), 485 | New( 486 | Selector, 487 | New( 488 | Sequence, 489 | New(counterEqual(0)), 490 | New(debug(`blocking on context-enabled tick...`)), 491 | New( 492 | btCtx.Tick(func(ctx context.Context, children []Node) (Status, error) { 493 | fmt.Printf("NOTE children (%d) passed through\n", len(children)) 494 | <-ctx.Done() 495 | return Success, nil 496 | }), 497 | New(Sequence), 498 | New(Sequence), 499 | ), 500 | New(counterInc), 501 | ), 502 | New( 503 | Sequence, 504 | New(counterEqual(1)), 505 | New(debug(`blocking on done...`)), 506 | New(btCtx.Done), 507 | New(counterInc), 508 | ), 509 | New( 510 | Sequence, 511 | New(counterEqual(2)), 512 | New(debug(`canceling local then rechecking the above...`)), 513 | New(btCtx.Cancel), 514 | New(btCtx.Err), 515 | New(btCtx.Tick(func(ctx context.Context, children []Node) (Status, error) { 516 | <-ctx.Done() 517 | return Success, nil 518 | })), 519 | New(btCtx.Done), 520 | New(counterInc), 521 | ), 522 | New( 523 | Sequence, 524 | New(counterEqual(3)), 525 | New(debug(`canceling parent then rechecking the above...`)), 526 | New(func([]Node) (Status, error) { 527 | cancel() 528 | return Success, nil 529 | }), 530 | New(btCtx.Err), 531 | New(btCtx.Tick(func(ctx context.Context, children []Node) (Status, error) { 532 | <-ctx.Done() 533 | return Success, nil 534 | })), 535 | New(btCtx.Done), 536 | New(debug(`exiting...`)), 537 | ), 538 | ), 539 | )) 540 | ) 541 | 542 | <-ticker.Done() 543 | 544 | //output: 545 | //(re)initialising btCtx... 546 | //blocking on context-enabled tick... 547 | //NOTE children (2) passed through 548 | //(re)initialising btCtx... 549 | //blocking on done... 550 | //(re)initialising btCtx... 551 | //canceling local then rechecking the above... 552 | //(re)initialising btCtx... 553 | //canceling parent then rechecking the above... 554 | //exiting... 555 | } 556 | ``` 557 | --------------------------------------------------------------------------------