From 636517791ed24d78ed9ce89e8c026781452bf4ce Mon Sep 17 00:00:00 2001 From: Joseph Cumines Date: Sat, 24 Apr 2021 16:07:39 +1000 Subject: [PATCH] feat(Switch): tick implementation like a switch statement Adds a stateless Switch function that may be used directly as a Tick. Since all cases are defined as children, tick wrappers are typically compatible, notably including Memorize, which works as one might expect (see the example). --- switch.go | 47 +++++++++ switch_test.go | 276 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 323 insertions(+) create mode 100644 switch.go create mode 100644 switch_test.go diff --git a/switch.go b/switch.go new file mode 100644 index 0000000..83f9acb --- /dev/null +++ b/switch.go @@ -0,0 +1,47 @@ +/* + Copyright 2021 Joseph Cumines + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package behaviortree + +// Switch is a tick implementation that provides switch-like functionality, where each switch case is comprised of a +// condition and statement, formed by a pair of (contiguous) children. If there are an odd number of children, then the +// final child will be treated as a statement with an always-true condition (used as the default case). The first error +// or first running status will be returned (if any). Otherwise, the result will be either that of the statement +// corresponding to the first successful condition, or success. +// +// This implementation is compatible with both Memorize and Sync. +func Switch(children []Node) (Status, error) { + for i := 0; i < len(children); i += 2 { + if i == len(children)-1 { + // statement (default case) + return children[i].Tick() + } + // condition (normal case) + status, err := children[i].Tick() + if err != nil { + return Failure, err + } + if status == Running { + return Running, nil + } + if status == Success { + // statement (normal case) + return children[i+1].Tick() + } + } + // no matching condition and no default statement + return Success, nil +} diff --git a/switch_test.go b/switch_test.go new file mode 100644 index 0000000..77d0455 --- /dev/null +++ b/switch_test.go @@ -0,0 +1,276 @@ +/* + Copyright 2021 Joseph Cumines + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package behaviortree + +import ( + "context" + "errors" + "fmt" + "testing" + "time" +) + +func TestSwitch(t *testing.T) { + type ( + ChildTick struct { + Index int + Status Status + Err error + } + Invocation struct { + Name string + Children int + ChildTicks []ChildTick + Status Status + Err error + } + ) + for _, tc := range [...]struct { + Name string + Tick Tick + Invocations []Invocation + }{ + { + Name: `stateless`, + Tick: Switch, + Invocations: []Invocation{ + { + Name: `no children`, + Status: Success, + }, + { + Name: `default case success`, + Children: 1, + ChildTicks: []ChildTick{ + {0, Success, nil}, + }, + Status: Success, + }, + { + Name: `default case invalid status and error`, + Children: 1, + ChildTicks: []ChildTick{ + {0, 123, errors.New(`some_error`)}, + }, + Status: 123, + Err: errors.New(`some_error`), + }, + { + Name: `single case success`, + Children: 2, + ChildTicks: []ChildTick{ + {0, Success, nil}, + {1, Success, nil}, + }, + Status: Success, + }, + { + Name: `single case failure`, + Children: 2, + ChildTicks: []ChildTick{ + {0, Success, nil}, + {1, Failure, nil}, + }, + Status: Failure, + }, + { + Name: `single case success no match`, + Children: 2, + ChildTicks: []ChildTick{ + {0, Failure, nil}, + }, + Status: Success, + }, + { + Name: `single case invalid status and error`, + Children: 2, + ChildTicks: []ChildTick{ + {0, Success, nil}, + {1, 123, errors.New(`some_error`)}, + }, + Status: 123, + Err: errors.New(`some_error`), + }, + { + Name: `single case condition invalid status and error`, + Children: 2, + ChildTicks: []ChildTick{ + {0, 123, errors.New(`some_error`)}, + }, + Status: Failure, + Err: errors.New(`some_error`), + }, + { + Name: `single case condition running`, + Children: 2, + ChildTicks: []ChildTick{ + {0, Running, nil}, + }, + Status: Running, + }, + { + Name: `multi case`, + Children: 9, + ChildTicks: []ChildTick{ + {0, Failure, nil}, + {2, Failure, nil}, + {4, Success, nil}, + {5, Success, nil}, + }, + Status: Success, + }, + { + Name: `multi case default failure`, + Children: 7, + ChildTicks: []ChildTick{ + {0, Failure, nil}, + {2, Failure, nil}, + {4, Failure, nil}, + {6, Failure, nil}, + }, + Status: Failure, + }, + }, + }, + } { + t.Run(tc.Name, func(t *testing.T) { + for _, invocation := range tc.Invocations { + t.Run(invocation.Name, func(t *testing.T) { + var children []Node + for i := 0; i < invocation.Children; i++ { + i := i + children = append(children, New(func([]Node) (status Status, err error) { + if len(invocation.ChildTicks) == 0 { + t.Errorf(`child %d ticked but none expected`, i) + return Success, nil + } + if invocation.ChildTicks[0].Index != i { + t.Errorf(`child %d ticked but expected %d`, i, invocation.ChildTicks[0].Index) + return Success, nil + } + status, err = invocation.ChildTicks[0].Status, invocation.ChildTicks[0].Err + invocation.ChildTicks = invocation.ChildTicks[1:] + return + })) + } + status, err := tc.Tick(children) + if (err == nil) != (invocation.Err == nil) || (err != nil && err.Error() != invocation.Err.Error()) { + t.Error(err) + } + if status != invocation.Status { + t.Error(status) + } + if len(invocation.ChildTicks) != 0 { + t.Errorf(`expected %d more child ticks`, len(invocation.ChildTicks)) + } + }) + } + }) + } +} + +func ExampleSwitch() { + var ( + sanityChecks []func() + newNode = func(name string, statuses ...Status) Node { + sanityChecks = append(sanityChecks, func() { + if len(statuses) != 0 { + panic(fmt.Errorf(`node %s has %d unconsumed statuses`, name, len(statuses))) + } + }) + return New(func([]Node) (status Status, _ error) { + if len(statuses) == 0 { + panic(fmt.Errorf(`node %s has no unconsumed statuses`, name)) + } + status = statuses[0] + statuses = statuses[1:] + fmt.Printf("Tick %s: %s\n", name, status) + return + }) + } + ticker = NewTickerStopOnFailure( + context.Background(), + time.Millisecond, + New( + Memorize(Sequence), + newNode(`START`, Success, Success, Success, Success, Failure), + New( + Memorize(Selector), + New( + Memorize(Sequence), + New( + Memorize(Switch), + + newNode(`case-1-condition`, Failure, Failure, Running, Running, Running, Failure, Failure), + newNode(`case-1-statement`), + + newNode(`case-2-condition`, Failure, Failure, Running, Running, Success, Success), + newNode(`case-2-statement`, Running, Running, Running, Failure, Running, Success), + + newNode(`case-3-condition`, Failure, Failure), + newNode(`case-3-statement`), + + newNode(`default-statement`, Failure, Success), + ), + newNode(`SUCCESS`, Success, Success), + ), + newNode(`FAILURE`, Success, Success), + ), + ), + ) + ) + <-ticker.Done() + if err := ticker.Err(); err != nil { + panic(err) + } + for _, sanityCheck := range sanityChecks { + sanityCheck() + } + // output: + // Tick START: success + // Tick case-1-condition: failure + // Tick case-2-condition: failure + // Tick case-3-condition: failure + // Tick default-statement: failure + // Tick FAILURE: success + // Tick START: success + // Tick case-1-condition: failure + // Tick case-2-condition: failure + // Tick case-3-condition: failure + // Tick default-statement: success + // Tick SUCCESS: success + // Tick START: success + // Tick case-1-condition: running + // Tick case-1-condition: running + // Tick case-1-condition: running + // Tick case-1-condition: failure + // Tick case-2-condition: running + // Tick case-2-condition: running + // Tick case-2-condition: success + // Tick case-2-statement: running + // Tick case-2-statement: running + // Tick case-2-statement: running + // Tick case-2-statement: failure + // Tick FAILURE: success + // Tick START: success + // Tick case-1-condition: failure + // Tick case-2-condition: success + // Tick case-2-statement: running + // Tick case-2-statement: success + // Tick SUCCESS: success + // Tick START: failure +}