behaviortree

package module
v1.8.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 31, 2022 License: Apache-2.0 Imports: 13 Imported by: 0

README

Go Report Card

go-behaviortree

Package behaviortree provides a simple and powerful Go implementation of behavior trees without fluff.

Go doc: https://godoc.org/github.com/joeycumines/go-behaviortree

Wikipedia: Behavior tree - AI, robotics, and control

type (
	// Node represents an node in a tree, that can be ticked
	Node func() (Tick, []Node)

	// Tick represents the logic for a node, which may or may not be stateful
	Tick func(children []Node) (Status, error)

	// Status is a type with three valid values, Running, Success, and Failure, the three possible states for BTs
	Status int
)

// Tick runs the node's tick function with it's children
func (n Node) Tick() (Status, error)

Features

  • Core behavior tree implementation (the types above + Sequence and Selector)
  • Tools to aide implementation of "reactive" behavior trees (Memorize, Async, Sync)
  • Implementations to run and manage behavior trees (NewManager, NewTicker)
  • Collection of Tick implementations / wrappers (targeting various use cases)
  • Context-like mechanism to attach metadata to Node values that can transit API boundaries / encapsulation
  • Basic tree debugging capabilities via implementation of fmt.Stringer (see also DefaultPrinter, Node.Frame)
  • Experimental support for the PA-BT planning algorithm via github.com/joeycumines/go-pabt

Design

This library provides an implementation of the generalised behavior tree pattern. The three types above along with the stateless Tick functions Selector and Sequence (line for line from Wikipedia) make up the entirety of the core functionality. Additional features have been derived by means of iterating against specific, real-world problem cases.

It is recommended that anyone interested in understanding behavior trees read from the start of chapter 1 of Colledanchise, Michele & Ogren, Petter. (2018). Behavior Trees in Robotics and AI: An Introduction. 10.1201/9780429489105. until at least (the end of) 1.3.2 - Control Flow Nodes with Memory. Further context should (hopefully) go a long way in aiding newcomers to gain a firm grasp of the relevant behavior and associated patterns, in order to start building automations using behavior trees.

N.B. Though it does appear to be an excellent resource, Behavior Trees in Robotics and AI: An Introduction. was only brought to my attention well after the release of v1.3.0. In particular, concepts and terms are unlikely to be entirely consistent.

Reactivity

Executing a behavior tree sequentially (i.e. without the use of nodes that return Running) can be an effective way to decompose complicated switching logic. Though effective, this design suffers from limitations common to traditional finite state machines, which tends to cripple the modularity of any interruptable operations, as each tick must manage it's own exit condition(s). Golang's context doesn't really help in that case, either, being a communication mechanism, rather than a control mechanism. As an alternative, implementations may use pre-conditions (preceding child(ren) in a sequence), guarding an asynchronous tick. Context support may be desirable, and may be implemented as Tick implementations(s). This package provides a Context implementation to address several common use cases, such as operations with timeouts. Context support is peripheral in that it's only relevant to a subset of implementations, and was deliberately omitted from the core Tick type.

Modularity

This library only concerns itself with the task of actually running behavior trees. It is deliberately designed to make it straightforward to plug in external implementations. External implementations may be integrated as a fully-fledged recursively generated tree of Node. Tree-aware debug tracing could be implemented using a similar mechanism. At the end of the day though, implementations just need to Tick.

Implementation

This section attempts to provide better insight into the intent of certain implementation details. Part of this involves offering implementation guidelines, based on imagined use cases (beyond those explored as part of each implementation). It ain't gospel.

Executing BTs
Use explicit exit conditions
  • Return error to handle failures that should terminate tree execution
  • BTs that "run until completion" may be implemented using NewTickerStopOnFailure (internally it just returns an error then strips any occurrence of that that error from the ticker's result)
  • Panics may be used as normal, and are suitable for cases such unrecoverable errors that shouldn't happen
Shared state
Tick implementations should be grouped in a way that makes sense, in the context of any shared state
  • It may be convenient to expose a group of Tick prototypes with shared state as methods of a struct
  • Global state should be avoided for all the regular reasons, but especially since it defeats a lot of the point of having modular, composable behavior
Encapsulate Tick implementations, rather than Node
  • Children may be may be modified, but only until the (outer) Tick returns it's next non-running status
  • This mechanism theoretically facilitates dynamically generated trees while simultaneously supporting more complex / concurrency-heavy implementations made up of reusable building blocks

Roadmap

I am actively maintaining this project, and will be for the foreseeable future. It has been "feature complete" for some time though, so additional functionality will assessed on a case-by-case basis.

Example Usage

The examples below are straight from example_test.go.

// ExampleNewTickerStopOnFailure_counter demonstrates the use of NewTickerStopOnFailure to implement more complex "run
// to completion" behavior using the simple modular building blocks provided by this package
func ExampleNewTickerStopOnFailure_counter() {
	var (
		// counter is the shared state used by this example
		counter = 0
		// printCounter returns a node that will print the counter prefixed with the given name then succeed
		printCounter = func(name string) Node {
			return New(
				func(children []Node) (Status, error) {
					fmt.Printf("%s: %d\n", name, counter)
					return Success, nil
				},
			)
		}
		// incrementCounter is a node that will increment counter then succeed
		incrementCounter = New(
			func(children []Node) (Status, error) {
				counter++
				return Success, nil
			},
		)
		// ticker is what actually runs this example and will tick the behavior tree defined by a given node at a given
		// rate and will stop after the first failed tick or error or context cancel
		ticker = NewTickerStopOnFailure(
			context.Background(),
			time.Millisecond,
			New(
				Selector, // runs each child sequentially until one succeeds (success) or all fail (failure)
				New(
					Sequence, // runs each child in order until one fails (failure) or they all succeed (success)
					New(
						func(children []Node) (Status, error) { // succeeds while counter is less than 10
							if counter < 10 {
								return Success, nil
							}
							return Failure, nil
						},
					),
					incrementCounter,
					printCounter("< 10"),
				),
				New(
					Sequence,
					New(
						func(children []Node) (Status, error) { // succeeds while counter is less than 20
							if counter < 20 {
								return Success, nil
							}
							return Failure, nil
						},
					),
					incrementCounter,
					printCounter("< 20"),
				),
			), // if both children failed (counter is >= 20) the root node will also fail
		)
	)
	// waits until ticker stops, which will be on the first failure of it's root node
	<-ticker.Done()
	// every Tick may return an error which would automatically cause a failure and propagation of the error
	if err := ticker.Err(); err != nil {
		panic(err)
	}
	// Output:
	// < 10: 1
	// < 10: 2
	// < 10: 3
	// < 10: 4
	// < 10: 5
	// < 10: 6
	// < 10: 7
	// < 10: 8
	// < 10: 9
	// < 10: 10
	// < 20: 11
	// < 20: 12
	// < 20: 13
	// < 20: 14
	// < 20: 15
	// < 20: 16
	// < 20: 17
	// < 20: 18
	// < 20: 19
	// < 20: 20
}

// ExampleMemorize_cancellationWithContextCancel demonstrates how support for reactive logic that uses context can
// be implemented
func ExampleMemorize_cancellationWithContextCancel() {
	var (
		ctx    context.Context
		cancel context.CancelFunc
		debug  = func(label string, tick Tick) Tick {
			return func(children []Node) (status Status, err error) {
				status, err = tick(children)
				fmt.Printf("%s returned (%v, %v)\n", label, status, err)
				return
			}
		}
		recorded = func(statuses ...Status) Tick {
			return func([]Node) (status Status, err error) {
				status = statuses[0]
				statuses = statuses[1:]
				return
			}
		}
		counter int
		ticker  = NewTickerStopOnFailure(
			context.Background(),
			time.Millisecond,
			New(
				All,
				New(
					Memorize(debug(`memorized`, All)),
					New(func([]Node) (Status, error) {
						counter++
						ctx, cancel = context.WithCancel(context.WithValue(context.Background(), `n`, counter))
						return Success, nil
					}), // prepare the context
					New(
						debug(`sequence`, Sequence),
						New(debug(`guard`, recorded(
							Success,
							Success,
							Success,
							Success,
							Failure,
						))),
						New(func([]Node) (Status, error) {
							fmt.Printf("[start action] context #%d's err=%v\n", ctx.Value(`n`), ctx.Err())
							return Success, nil
						}),
						New(debug(`action`, recorded(
							Running,
							Running,
							Success,
							Running,
						))),
					),
					New(func([]Node) (Status, error) {
						cancel()
						return Success, nil
					}), // cancel the context
				),
				New(func([]Node) (Status, error) {
					fmt.Printf("[end memorized] context #%d's err=%v\n", ctx.Value(`n`), ctx.Err())
					return Success, nil
				}),
			),
		)
	)

	<-ticker.Done()
	if err := ticker.Err(); err != nil {
		panic(err)
	}
	//output:
	//guard returned (success, <nil>)
	//[start action] context #1's err=<nil>
	//action returned (running, <nil>)
	//sequence returned (running, <nil>)
	//memorized returned (running, <nil>)
	//guard returned (success, <nil>)
	//[start action] context #1's err=<nil>
	//action returned (running, <nil>)
	//sequence returned (running, <nil>)
	//memorized returned (running, <nil>)
	//guard returned (success, <nil>)
	//[start action] context #1's err=<nil>
	//action returned (success, <nil>)
	//sequence returned (success, <nil>)
	//memorized returned (success, <nil>)
	//[end memorized] context #1's err=context canceled
	//guard returned (success, <nil>)
	//[start action] context #2's err=<nil>
	//action returned (running, <nil>)
	//sequence returned (running, <nil>)
	//memorized returned (running, <nil>)
	//guard returned (failure, <nil>)
	//sequence returned (failure, <nil>)
	//memorized returned (failure, <nil>)
	//[end memorized] context #2's err=context canceled
}

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

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

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

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

	time.Sleep(time.Millisecond * 50)
	wg.Done()
	wg.Wait()
	close(done)
	<-ticker.Done()
	if err := ticker.Err(); err != nil {
		panic(err)
	}
	//output:
	//running jobs: 0
	//[client] job "1. 120ms" STARTED
	//[worker] job "1. 120ms" STARTED
	//[client] job "2. 70ms" STARTED
	//[worker] job "2. 70ms" STARTED
	//running jobs: 2
	//[client] job "3. 150ms" STARTED
	//[worker] job "3. 150ms" STARTED
	//[worker] job "2. 70ms" FINISHED
	//[client] job "2. 70ms" FINISHED
	//[worker] job "1. 120ms" FINISHED
	//[client] job "1. 120ms" FINISHED
	//[worker] job "3. 150ms" FINISHED
	//[client] job "3. 150ms" FINISHED
	//running jobs: 0
}


// ExampleContext demonstrates how the Context implementation may be used to integrate with the context package
func ExampleContext() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	var (
		btCtx = new(Context).WithTimeout(ctx, time.Millisecond*100)
		debug = func(args ...interface{}) Tick {
			return func([]Node) (Status, error) {
				fmt.Println(args...)
				return Success, nil
			}
		}
		counter      int
		counterEqual = func(v int) Tick {
			return func([]Node) (Status, error) {
				if counter == v {
					return Success, nil
				}
				return Failure, nil
			}
		}
		counterInc Tick = func([]Node) (Status, error) {
			counter++
			//fmt.Printf("counter = %d\n", counter)
			return Success, nil
		}
		ticker = NewTicker(ctx, time.Millisecond, New(
			Sequence,
			New(
				Selector,
				New(Not(btCtx.Err)),
				New(
					Sequence,
					New(debug(`(re)initialising btCtx...`)),
					New(btCtx.Init),
					New(Not(btCtx.Err)),
				),
			),
			New(
				Selector,
				New(
					Sequence,
					New(counterEqual(0)),
					New(debug(`blocking on context-enabled tick...`)),
					New(
						btCtx.Tick(func(ctx context.Context, children []Node) (Status, error) {
							fmt.Printf("NOTE children (%d) passed through\n", len(children))
							<-ctx.Done()
							return Success, nil
						}),
						New(Sequence),
						New(Sequence),
					),
					New(counterInc),
				),
				New(
					Sequence,
					New(counterEqual(1)),
					New(debug(`blocking on done...`)),
					New(btCtx.Done),
					New(counterInc),
				),
				New(
					Sequence,
					New(counterEqual(2)),
					New(debug(`canceling local then rechecking the above...`)),
					New(btCtx.Cancel),
					New(btCtx.Err),
					New(btCtx.Tick(func(ctx context.Context, children []Node) (Status, error) {
						<-ctx.Done()
						return Success, nil
					})),
					New(btCtx.Done),
					New(counterInc),
				),
				New(
					Sequence,
					New(counterEqual(3)),
					New(debug(`canceling parent then rechecking the above...`)),
					New(func([]Node) (Status, error) {
						cancel()
						return Success, nil
					}),
					New(btCtx.Err),
					New(btCtx.Tick(func(ctx context.Context, children []Node) (Status, error) {
						<-ctx.Done()
						return Success, nil
					})),
					New(btCtx.Done),
					New(debug(`exiting...`)),
				),
			),
		))
	)

	<-ticker.Done()

	//output:
	//(re)initialising btCtx...
	//blocking on context-enabled tick...
	//NOTE children (2) passed through
	//(re)initialising btCtx...
	//blocking on done...
	//(re)initialising btCtx...
	//canceling local then rechecking the above...
	//(re)initialising btCtx...
	//canceling parent then rechecking the above...
	//exiting...
}

Documentation

Overview

Package behaviortree provides a simple and powerful Go implementation of behavior trees without fluff.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrManagerStopped is returned by the manager implementation in this package (see also NewManager) in the case
	// that Manager.Add is attempted after the manager has already started to stop. Use errors.Is to check this case.
	ErrManagerStopped error = errManagerStopped{/* contains filtered or unexported fields */}
)

Functions

func DefaultPrinterInspector added in v1.8.1

func DefaultPrinterInspector(node Node, tick Tick) ([]interface{}, interface{})

DefaultPrinterInspector is used by DefaultPrinter

Types

type Context added in v1.8.1

type Context struct {
	// contains filtered or unexported fields
}

Context provides support for tick(s) utilising context as a means of cancelation, with cancelation triggered by either BT-driven logic or the normal means (parent cancelation, deadline / timeout).

Note that it must be initialised by means of it's Init method (implements a tick) prior to use (Context.Tick tick). Init may be ticked any number of times (each time triggering cancelation of any prior context).

Example

ExampleContext demonstrates how the Context implementation may be used to integrate with the context package

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

var (
	btCtx = new(Context).WithTimeout(ctx, time.Millisecond*100)
	debug = func(args ...interface{}) Tick {
		return func([]Node) (Status, error) {
			fmt.Println(args...)
			return Success, nil
		}
	}
	counter      int
	counterEqual = func(v int) Tick {
		return func([]Node) (Status, error) {
			if counter == v {
				return Success, nil
			}
			return Failure, nil
		}
	}
	counterInc Tick = func([]Node) (Status, error) {
		counter++
		//fmt.Printf("counter = %d\n", counter)
		return Success, nil
	}
	ticker = NewTicker(ctx, time.Millisecond, New(
		Sequence,
		New(
			Selector,
			New(Not(btCtx.Err)),
			New(
				Sequence,
				New(debug(`(re)initialising btCtx...`)),
				New(btCtx.Init),
				New(Not(btCtx.Err)),
			),
		),
		New(
			Selector,
			New(
				Sequence,
				New(counterEqual(0)),
				New(debug(`blocking on context-enabled tick...`)),
				New(
					btCtx.Tick(func(ctx context.Context, children []Node) (Status, error) {
						fmt.Printf("NOTE children (%d) passed through\n", len(children))
						<-ctx.Done()
						return Success, nil
					}),
					New(Sequence),
					New(Sequence),
				),
				New(counterInc),
			),
			New(
				Sequence,
				New(counterEqual(1)),
				New(debug(`blocking on done...`)),
				New(btCtx.Done),
				New(counterInc),
			),
			New(
				Sequence,
				New(counterEqual(2)),
				New(debug(`canceling local then rechecking the above...`)),
				New(btCtx.Cancel),
				New(btCtx.Err),
				New(btCtx.Tick(func(ctx context.Context, children []Node) (Status, error) {
					<-ctx.Done()
					return Success, nil
				})),
				New(btCtx.Done),
				New(counterInc),
			),
			New(
				Sequence,
				New(counterEqual(3)),
				New(debug(`canceling parent then rechecking the above...`)),
				New(func([]Node) (Status, error) {
					cancel()
					return Success, nil
				}),
				New(btCtx.Err),
				New(btCtx.Tick(func(ctx context.Context, children []Node) (Status, error) {
					<-ctx.Done()
					return Success, nil
				})),
				New(btCtx.Done),
				New(debug(`exiting...`)),
			),
		),
	))
)

<-ticker.Done()
Output:

(re)initialising btCtx...
blocking on context-enabled tick...
NOTE children (2) passed through
(re)initialising btCtx...
blocking on done...
(re)initialising btCtx...
canceling local then rechecking the above...
(re)initialising btCtx...
canceling parent then rechecking the above...
exiting...

func (*Context) Cancel added in v1.8.1

func (c *Context) Cancel([]Node) (Status, error)

Cancel implements a tick that will cancel the receiver's context (noop if it has none) then succeed

func (*Context) Done added in v1.8.1

func (c *Context) Done([]Node) (Status, error)

Done implements a tick that will block on the receiver's context being canceled (noop if it has none) then succeed

func (*Context) Err added in v1.8.1

func (c *Context) Err([]Node) (Status, error)

Err implements a tick that will succeed if the receiver does not have a context or it has been canceled

func (*Context) Init added in v1.8.1

func (c *Context) Init([]Node) (Status, error)

Init implements a tick that will cancel existing context, (re)initialise the context, then succeed, note that it must not be called concurrently with any other method, and it must be ticked prior to any Context.Tick tick

func (*Context) Tick added in v1.8.1

func (c *Context) Tick(fn func(ctx context.Context, children []Node) (Status, error)) Tick

Tick returns a tick that will call fn with the receiver's context, returning nil if fn is nil (for consistency with other implementations in this package), note that a Init node must have already been ticked on all possible execution paths, or a panic may occur, due to fn being passed a nil context.Context

func (*Context) WithCancel added in v1.8.1

func (c *Context) WithCancel(parent context.Context) *Context

WithCancel configures the receiver to initialise context like context.WithCancel(parent), returning the receiver

func (*Context) WithDeadline added in v1.8.1

func (c *Context) WithDeadline(parent context.Context, deadline time.Time) *Context

WithDeadline configures the receiver to initialise context like context.WithDeadline(parent, deadline), returning the receiver

func (*Context) WithTimeout added in v1.8.1

func (c *Context) WithTimeout(parent context.Context, timeout time.Duration) *Context

WithTimeout configures the receiver to initialise context like context.WithTimeout(parent, timeout), returning the receiver

type Frame added in v1.8.1

type Frame struct {
	// PC is the program counter for the location in this frame.
	// For a frame that calls another frame, this will be the
	// program counter of a call instruction. Because of inlining,
	// multiple frames may have the same PC value, but different
	// symbolic information.
	PC uintptr
	// Function is the package path-qualified function name of
	// this call frame. If non-empty, this string uniquely
	// identifies a single function in the program.
	// This may be the empty string if not known.
	Function string
	// File and Line are the file name and line number of the
	// location in this frame. For non-leaf frames, this will be
	// the location of a call. These may be the empty string and
	// zero, respectively, if not known.
	File string
	Line int
	// Entry point program counter for the function; may be zero
	// if not known.
	Entry uintptr
}

Frame is a partial copy of runtime.Frame.

This packages captures details about the caller of it's New and NewNode functions, embedding them into the nodes themselves, for tree printing / tracing purposes.

type Manager

type Manager interface {
	Ticker

	// Add will register a new ticker under this manager
	Add(ticker Ticker) error
}

Manager models an aggregate Ticker, which should stop gracefully on the first failure

func NewManager

func NewManager() Manager

NewManager will construct an implementation of the Manager interface, which is a stateful set of Ticker implementations, aggregating the behavior such that the Done channel will close when ALL tickers registered with Add are done, Err will return a combined error if there are any, and Stop will stop all registered tickers.

Note that any error (of any registered tickers) will also trigger stopping, and stopping will prevent further Add calls from succeeding.

As of v1.8.0, any (combined) ticker error returned by the Manager can now support error chaining (i.e. the use of errors.Is). Note that errors.Unwrap isn't supported, since there may be more than one. See also Manager.Err and Manager.Add.

type Node

type Node func() (Tick, []Node)

Node represents an node in a tree, that can be ticked

func New

func New(tick Tick, children ...Node) Node

New constructs a new behavior tree and is equivalent to NewNode with vararg support for less indentation

func NewNode

func NewNode(tick Tick, children []Node) Node

NewNode constructs a new node out of a tick and children

func Sync

func Sync(nodes []Node) []Node

Sync will wrap a set of nodes in such a way that their real ticks will only be triggered when either the node being ticked was previously running, or no other nodes are running, synchronising calling their Node and Tick calls.

NOTE the Memorize function provides similar functionality, and should be preferred, where both are suitable.

func (Node) Frame added in v1.8.1

func (n Node) Frame() *Frame

Frame will return the call frame for the caller of New/NewNode, an approximation based on the receiver, or nil.

This method uses the Value mechanism and is subject to the same warnings / performance limitations.

func (Node) String added in v1.8.1

func (n Node) String() string

String implements fmt.Stringer using DefaultPrinter

func (Node) Tick

func (n Node) Tick() (Status, error)

Tick runs the node's tick function with it's children

func (Node) Value added in v1.8.1

func (n Node) Value(key interface{}) interface{}

Value will return the value associated with this node for key, or nil if there is none.

See also Node.WithValue, as well as the value mechanism provided by the context package.

func (Node) WithValue added in v1.8.1

func (n Node) WithValue(key, value interface{}) Node

WithValue will return the receiver wrapped with a key-value pair, using similar semantics to the context package.

Values should only be used to attach information to BTs in a way that transits API boundaries, not for passing optional parameters to functions. Some package-level synchronisation was necessary to facilitate this mechanism. As such, this and the Node.Value method should be used with caution, preferably only outside normal operation.

The same restrictions on the key apply as for context.WithValue.

type Printer added in v1.8.1

type Printer interface {
	// Fprint writes a representation node to output
	Fprint(output io.Writer, node Node) error
}

Printer models something providing behavior tree printing capabilities

var (
	// DefaultPrinter is used to implement Node.String
	DefaultPrinter Printer = TreePrinter{
		Inspector: DefaultPrinterInspector,
		Formatter: DefaultPrinterFormatter,
	}
)

type Status

type Status int

Status is a type with three valid values, Running, Success, and Failure, the three possible states for BTs

const (

	// Running indicates that the Tick for a given Node is currently running
	Running Status
	// Success indicates that the Tick for a given Node completed successfully
	Success
	// Failure indicates that the Tick for a given Node failed to complete successfully
	Failure
)

func All added in v1.2.0

func All(children []Node) (Status, error)

All implements a tick which will tick all children sequentially until the first running status or error is encountered (propagated), and will return success only if all children were ticked and returned success (returns success if there were no children, like sequence).

func Selector

func Selector(children []Node) (Status, error)

Selector is a tick implementation that ticks each child sequentially, until the the first error (returning the error), the first non-failure status (returning the status), or all children are ticked (returning failure)

func Sequence

func Sequence(children []Node) (Status, error)

Sequence is a tick implementation that ticks each child sequentially, until the the first error (returning the error), the first non-success status (returning the status), or all children are ticked (returning success)

func Switch added in v1.8.1

func Switch(children []Node) (Status, error)

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.

Example
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

func (Status) Status

func (s Status) Status() Status

Status returns the status value, but defaults to failure on out of bounds

func (Status) String

func (s Status) String() string

String returns a string representation of the status

type Tick

type Tick func(children []Node) (Status, error)

Tick represents the logic for a node, which may or may not be stateful

func Any added in v1.2.0

func Any(tick Tick) Tick

Any wraps a tick such that non-error non-running statuses will be overridden with a success if at least one child succeeded - which is achieved by encapsulation of children, before passing them into the wrapped tick. Nil will be returned if tick is nil, and nil children will be passed through as such.

Example (AllPartialSuccess)
fmt.Println(New(
	Any(All),
	New(func(children []Node) (Status, error) {
		fmt.Println(1)
		return Success, nil
	}),
	New(func(children []Node) (Status, error) {
		fmt.Println(2)
		return Success, nil
	}),
	New(func(children []Node) (Status, error) {
		fmt.Println(3)
		return Success, nil
	}),
	New(func(children []Node) (Status, error) {
		fmt.Println(4)
		return Success, nil
	}),
	New(func(children []Node) (Status, error) {
		fmt.Println(5)
		return Failure, nil
	}),
	New(func(children []Node) (Status, error) {
		fmt.Println(6)
		return Success, nil
	}),
).Tick())
Output:

1
2
3
4
5
6
success <nil>
Example (ForkPartialSuccess)
var (
	c1     = make(chan struct{})
	c2     = make(chan struct{})
	c3     = make(chan struct{})
	c4     = make(chan struct{})
	c5     = make(chan struct{})
	c6     = make(chan struct{})
	status = Running
)
go func() {
	time.Sleep(time.Millisecond * 100)
	fmt.Println(`unblocking the forked nodes`)
	close(c1)
	time.Sleep(time.Millisecond * 100)
	close(c2)
	time.Sleep(time.Millisecond * 100)
	close(c3)
	time.Sleep(time.Millisecond * 100)
	close(c4)
	time.Sleep(time.Millisecond * 100)
	close(c5)
	time.Sleep(time.Millisecond * 100)
	close(c6)
}()
node := New(
	Any(Fork()),
	New(func(children []Node) (Status, error) {
		fmt.Println(`ready`)
		<-c1
		fmt.Println(1)
		return Success, nil
	}),
	New(func(children []Node) (Status, error) {
		fmt.Println(`ready`)
		<-c2
		fmt.Println(2)
		return Success, nil
	}),
	New(func(children []Node) (Status, error) {
		fmt.Println(`ready`)
		<-c3
		fmt.Println(3)
		return status, nil
	}),
	New(func(children []Node) (Status, error) {
		fmt.Println(`ready`)
		<-c4
		fmt.Println(4)
		return Failure, nil
	}),
	New(func(children []Node) (Status, error) {
		fmt.Println(`ready`)
		<-c5
		fmt.Println(5)
		return Failure, nil
	}),
	New(func(children []Node) (Status, error) {
		fmt.Println(`ready`)
		<-c6
		fmt.Println(6)
		return Success, nil
	}),
)
fmt.Println(node.Tick())
fmt.Println(`same running behavior as Fork`)
fmt.Println(node.Tick())
fmt.Println(`but the exit status is overridden`)
status = Failure
fmt.Println(node.Tick())
Output:

ready
ready
ready
ready
ready
ready
unblocking the forked nodes
1
2
3
4
5
6
running <nil>
same running behavior as Fork
ready
3
running <nil>
but the exit status is overridden
ready
3
success <nil>
Example (ResetBehavior)
var (
	status Status
	err    error
	node   = New(
		Any(Sequence),
		New(func(children []Node) (Status, error) {
			fmt.Println(1)
			return status, err
		}),
		New(func(children []Node) (Status, error) {
			fmt.Println(2)
			return Success, nil
		}),
	)
)

status = Success
err = nil
fmt.Println(node.Tick())

status = Failure
err = nil
fmt.Println(node.Tick())

status = Success
err = errors.New(`some_error`)
fmt.Println(node.Tick())

status = Success
err = nil
fmt.Println(node.Tick())
Output:

1
2
success <nil>
1
failure <nil>
1
failure some_error
1
2
success <nil>
Example (Running)
status := Running
node := New(
	Any(All),
	New(func(children []Node) (Status, error) {
		fmt.Printf("child ticked: %s\n", status)
		return status, nil
	}),
)
fmt.Println(node.Tick())
status = Failure
fmt.Println(node.Tick())
status = Running
fmt.Println(node.Tick())
status = Success
fmt.Println(node.Tick())
Output:

child ticked: running
running <nil>
child ticked: failure
failure <nil>
child ticked: running
running <nil>
child ticked: success
success <nil>
Example (SequencePartialSuccess)
fmt.Println(New(
	Any(Sequence),
	New(func(children []Node) (Status, error) {
		fmt.Println(1)
		return Success, nil
	}),
	New(func(children []Node) (Status, error) {
		fmt.Println(2)
		return Success, nil
	}),
	New(func(children []Node) (Status, error) {
		fmt.Println(3)
		return Success, nil
	}),
	New(func(children []Node) (Status, error) {
		fmt.Println(4)
		return Success, nil
	}),
	New(func(children []Node) (Status, error) {
		fmt.Println(5)
		return Failure, nil
	}),
	New(func(children []Node) (Status, error) {
		panic(`wont reach here`)
	}),
).Tick())
Output:

1
2
3
4
5
success <nil>

func Async

func Async(tick Tick) Tick

Async wraps a tick so that it runs asynchronously, note nil ticks will return nil

func Background added in v1.2.0

func Background(tick func() Tick) Tick

Background pushes running nodes into the background, allowing multiple concurrent ticks (potentially running independent children, depending on the behavior of the node). It accepts a tick via closure, in order to support stateful ticks. On tick, backgrounded nodes are ticked from oldest to newest, until the first non-running status is returned, which will trigger removal from the backgrounded node list, and propagating status and any error, without modification. All other normal operation will result in a new node being generated and ticked, backgrounding it on running, otherwise discarding the node and propagating it's return values immediately. Passing a nil value will cause nil to be returned. WARNING there is no upper bound to the number of backgrounded nodes (the caller must manage that externally).

Example (AsyncJobQueue)

ExampleBackground_asyncJobQueue implements a basic example of backgrounding of long-running tasks that may be performed concurrently, see ExampleNewTickerStopOnFailure_counter for an explanation of the ticker

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

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

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

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

time.Sleep(time.Millisecond * 50)
wg.Done()
wg.Wait()
close(done)
<-ticker.Done()
if err := ticker.Err(); err != nil {
	panic(err)
}
Output:

running jobs: 0
[client] job "1. 120ms" STARTED
[worker] job "1. 120ms" STARTED
[client] job "2. 70ms" STARTED
[worker] job "2. 70ms" STARTED
running jobs: 2
[client] job "3. 150ms" STARTED
[worker] job "3. 150ms" STARTED
[worker] job "2. 70ms" FINISHED
[client] job "2. 70ms" FINISHED
[worker] job "1. 120ms" FINISHED
[client] job "1. 120ms" FINISHED
[worker] job "3. 150ms" FINISHED
[client] job "3. 150ms" FINISHED
running jobs: 0
Example (Success)
defer checkNumGoroutines(nil)(false, 0)
node := func() Node {
	tick := Background(Fork)
	return func() (Tick, []Node) {
		return tick, []Node{
			New(func(children []Node) (Status, error) {
				fmt.Println(`start fork`)
				return Success, nil
			}),
			New(Async(func(children []Node) (Status, error) {
				time.Sleep(time.Millisecond * 100)
				return Success, nil
			})),
			New(Async(func(children []Node) (Status, error) {
				time.Sleep(time.Millisecond * 200)
				return Success, nil
			})),
			New(Async(func(children []Node) (Status, error) {
				time.Sleep(time.Millisecond * 300)
				fmt.Println(`end fork`)
				return Success, nil
			})),
		}
	}
}()
fmt.Println(node.Tick())
time.Sleep(time.Millisecond * 50)
fmt.Println(node.Tick())
time.Sleep(time.Millisecond * 150)
fmt.Println(node.Tick())
time.Sleep(time.Millisecond * 200)
fmt.Println(node.Tick()) // will receive the first tick's status
time.Sleep(time.Millisecond * 50)
fmt.Println(node.Tick())
time.Sleep(time.Millisecond * 100)
fmt.Println(node.Tick())
fmt.Println(node.Tick())
fmt.Println(node.Tick())
time.Sleep(time.Millisecond * 450)
fmt.Println(node.Tick())
fmt.Println(node.Tick())
Output:

start fork
running <nil>
start fork
running <nil>
start fork
running <nil>
end fork
end fork
success <nil>
success <nil>
end fork
success <nil>
start fork
running <nil>
start fork
running <nil>
end fork
end fork
success <nil>
success <nil>

func Fork added in v1.1.0

func Fork() Tick

Fork generates a stateful Tick which will tick all children at once, returning after all children return a result, returning running if any children did so, and ticking only those which returned running in subsequent calls, until all children have returned a non-running status, combining any errors, and returning success if there were no failures or errors (otherwise failure), repeating this cycle for subsequent ticks

func Memorize added in v1.8.1

func Memorize(tick Tick) Tick

Memorize encapsulates a tick, and will cache the first non-running status for each child, per "execution", defined as the period until the first non-running status, of the encapsulated tick, facilitating execution of asynchronous nodes in serial with their siblings, using stateless tick implementations, such as sequence and selector.

Sync provides a similar but more flexible mechanism, at the expense of greater complexity, and more cumbersome usage. Sync supports modification of children mid-execution, and may be used to implement complex guarding behavior as children of a single Tick, equivalent to more complex structures using multiple memorized sequence nodes.

Example (CancellationWithContextCancel)

ExampleMemorize_cancellationWithContextCancel demonstrates how support for reactive logic that uses context can be implemented

type Str string
var (
	ctx    context.Context
	cancel context.CancelFunc
	debug  = func(label string, tick Tick) Tick {
		return func(children []Node) (status Status, err error) {
			status, err = tick(children)
			fmt.Printf("%s returned (%v, %v)\n", label, status, err)
			return
		}
	}
	recorded = func(statuses ...Status) Tick {
		return func([]Node) (status Status, err error) {
			status = statuses[0]
			statuses = statuses[1:]
			return
		}
	}
	counter int
	ticker  = NewTickerStopOnFailure(
		context.Background(),
		time.Millisecond,
		New(
			All,
			New(
				Memorize(debug(`memorized`, All)),
				New(func([]Node) (Status, error) {
					counter++
					ctx, cancel = context.WithCancel(context.WithValue(context.Background(), Str(`n`), counter))
					return Success, nil
				}), // prepare the context
				New(
					debug(`sequence`, Sequence),
					New(debug(`guard`, recorded(
						Success,
						Success,
						Success,
						Success,
						Failure,
					))),
					New(func([]Node) (Status, error) {
						fmt.Printf("[start action] context #%d's err=%v\n", ctx.Value(Str(`n`)), ctx.Err())
						return Success, nil
					}),
					New(debug(`action`, recorded(
						Running,
						Running,
						Success,
						Running,
					))),
				),
				New(func([]Node) (Status, error) {
					cancel()
					return Success, nil
				}), // cancel the context
			),
			New(func([]Node) (Status, error) {
				fmt.Printf("[end memorized] context #%d's err=%v\n", ctx.Value(Str(`n`)), ctx.Err())
				return Success, nil
			}),
		),
	)
)

<-ticker.Done()
if err := ticker.Err(); err != nil {
	panic(err)
}
Output:

guard returned (success, <nil>)
[start action] context #1's err=<nil>
action returned (running, <nil>)
sequence returned (running, <nil>)
memorized returned (running, <nil>)
guard returned (success, <nil>)
[start action] context #1's err=<nil>
action returned (running, <nil>)
sequence returned (running, <nil>)
memorized returned (running, <nil>)
guard returned (success, <nil>)
[start action] context #1's err=<nil>
action returned (success, <nil>)
sequence returned (success, <nil>)
memorized returned (success, <nil>)
[end memorized] context #1's err=context canceled
guard returned (success, <nil>)
[start action] context #2's err=<nil>
action returned (running, <nil>)
sequence returned (running, <nil>)
memorized returned (running, <nil>)
guard returned (failure, <nil>)
sequence returned (failure, <nil>)
memorized returned (failure, <nil>)
[end memorized] context #2's err=context canceled

func Not added in v1.1.0

func Not(tick Tick) Tick

Not inverts a Tick, such that any failure cases will be success and success cases will be failure, note that any error or invalid status will still result in a failure

func RateLimit added in v1.1.0

func RateLimit(d time.Duration) Tick

RateLimit generates a stateful Tick that will return success at most once per a given duration

func Shuffle added in v1.3.0

func Shuffle(tick Tick, source rand.Source) Tick

Shuffle implements randomised child execution order via encapsulation, using the provided source to shuffle the children prior to passing through to the provided tick (a nil source will use global math/rand), note that this function will return nil if a nil tick is provided

Example
rand.Seed(1231244)
var (
	newPrintlnFn = func(fn func() []interface{}) Tick {
		return func([]Node) (Status, error) {
			fmt.Println(fn()...)
			return Success, nil
		}
	}
	newPrintln = func(v ...interface{}) Tick { return newPrintlnFn(func() []interface{} { return v }) }
	done       bool
	ticker     = NewTickerStopOnFailure(context.Background(), time.Millisecond, New(
		Sequence,
		New(newPrintlnFn(func() func() []interface{} {
			var i int
			return func() []interface{} {
				i++
				return []interface{}{`tick number`, i}
			}
		}())),
		New(
			Shuffle(Sequence, nil),
			New(newPrintln(`node 1`)),
			New(newPrintln(`node 2`)),
			New(
				Selector,
				New(func() func(children []Node) (Status, error) {
					remaining := 5
					return func(children []Node) (Status, error) {
						if remaining > 0 {
							remaining--
							return Success, nil
						}
						return Failure, nil
					}
				}()),
				New(
					Shuffle(Selector, nil),
					New(newPrintln(`node 3`)),
					New(newPrintln(`node 4`)),
					New(newPrintln(`node 5`)),
					New(newPrintln(`node 6`)),
					New(func([]Node) (Status, error) {
						done = true
						return Success, nil
					}),
				),
			),
		),
		New(func([]Node) (Status, error) {
			if done {
				return Failure, nil
			}
			return Success, nil
		}),
	))
)
<-ticker.Done()
if err := ticker.Err(); err != nil {
	panic(err)
}
Output:

tick number 1
node 1
node 2
tick number 2
node 2
node 1
tick number 3
node 1
node 2
tick number 4
node 2
node 1
tick number 5
node 2
node 1
tick number 6
node 1
node 2
node 5
tick number 7
node 6
node 1
node 2
tick number 8
node 2
node 5
node 1
tick number 9
node 3
node 2
node 1
tick number 10
node 2
node 1

func (Tick) Frame added in v1.8.1

func (t Tick) Frame() *Frame

Frame will return an approximation of a call frame based on the receiver, or nil.

type Ticker

type Ticker interface {
	// Done will close when the ticker is fully stopped.
	Done() <-chan struct{}

	// Err will return any error that occurs.
	Err() error

	// Stop shutdown the ticker asynchronously.
	Stop()
}

Ticker models a node runner

func NewTicker

func NewTicker(ctx context.Context, duration time.Duration, node Node) Ticker

NewTicker constructs a new Ticker, which simply uses time.Ticker to tick the provided node periodically, note that a panic will occur if ctx is nil, duration is <= 0, or node is nil.

The node will tick until the first error or Ticker.Stop is called, or context is canceled, after which any error will be made available via Ticker.Err, before closure of the done channel, indicating that all resources have been freed, and any error is available.

func NewTickerStopOnFailure

func NewTickerStopOnFailure(ctx context.Context, duration time.Duration, node Node) Ticker

NewTickerStopOnFailure returns a new Ticker that will exit on the first Failure, but won't return a non-nil Err UNLESS there was an actual error returned, it's built on top of the same core implementation provided by NewTicker, and uses that function directly, note that it will panic if the node is nil, the panic cases for NewTicker also apply.

Example (Counter)

ExampleNewTickerStopOnFailure_counter demonstrates the use of NewTickerStopOnFailure to implement more complex "run to completion" behavior using the simple modular building blocks provided by this package

// ticker is what actually runs this example and will tick the behavior tree defined by a given node at a given
// rate and will stop after the first failed tick or error or context cancel
ticker := NewTickerStopOnFailure(
	context.Background(),
	time.Millisecond,
	newExampleCounter(),
)
// waits until ticker stops, which will be on the first failure of it's root node
<-ticker.Done()
// every Tick may return an error which would automatically cause a failure and propagation of the error
if err := ticker.Err(); err != nil {
	panic(err)
}
Output:

< 10: 1
< 10: 2
< 10: 3
< 10: 4
< 10: 5
< 10: 6
< 10: 7
< 10: 8
< 10: 9
< 10: 10
< 20: 11
< 20: 12
< 20: 13
< 20: 14
< 20: 15
< 20: 16
< 20: 17
< 20: 18
< 20: 19
< 20: 20

type TreePrinter added in v1.8.1

type TreePrinter struct {
	// Inspector configures the meta and value for a node with a given tick
	Inspector func(node Node, tick Tick) (meta []interface{}, value interface{})
	// Formatter initialises a new printer tree and returns it as a TreePrinterNode
	Formatter func() TreePrinterNode
}

TreePrinter provides a generalised implementation of Printer used as the DefaultPrinter

func (TreePrinter) Fprint added in v1.8.1

func (p TreePrinter) Fprint(output io.Writer, node Node) error

Fprint implements Printer.Fprint

type TreePrinterNode added in v1.8.1

type TreePrinterNode interface {
	// Add should wire up a new node to the receiver then return it
	Add(meta []interface{}, value interface{}) TreePrinterNode
	// Bytes should encode the node and all children in preparation for use within TreePrinter
	Bytes() []byte
}

TreePrinterNode models a BT node for printing and is used by the TreePrinter implementation in this package

func DefaultPrinterFormatter added in v1.8.1

func DefaultPrinterFormatter() TreePrinterNode

DefaultPrinterFormatter is used by DefaultPrinter

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL