Behaviour trees
A behaviour tree that decides for every agent of a population at once, and the nodes to build it from.
agents.Tree
agents.Tree(pop, root)A behaviour tree that every agent of a population runs, ticked all at once.
Each agent has its own state in the tree (which child each composite is running for it), so agents in different branches, or at different steps of the same Sequence, tick together. The same nodes may appear more than once in a tree, and in several trees.
The tree follows the population’s stable ids (pop.id) from tick to tick: agents killed since the last tick are dropped, the survivors keep their state although their indices moved, and spawned agents start at the top of the tree. Killed agents get no halt hook call, so a leaf that holds per-agent resources must release them when its agents die.
If a leaf raises during tick, the tick stops part-way and the tree’s per-agent state (running children and counts) is undefined: build a new Tree before ticking again.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| pop | Population | Sized | The agents, an agents.Population. The tree reads len(pop) and pop.id and hands pop to every leaf. Any object with len() will do for a population that never changes size. |
required |
| root | _Node | The root node. | required |
Attributes
| Name | Type | Description |
|---|---|---|
| pop | Any | The population the tree ticks. |
| root | The root node. |
Raises
| Name | Type | Description |
|---|---|---|
| TypeError | If root is not a tree node. |
|
| ValueError | If an is_kind names a kind the population does not have. |
Methods
| Name | Description |
|---|---|
| draw | Draws the tree on screen: each node with how many agents it returned what for. |
| tick | Ticks the tree once for every agent. |
draw
agents.Tree.draw(x, y)Draws the tree on screen: each node with how many agents it returned what for.
A panel in the look of kiln.Gui, with its top left corner at (x, y). It has one row per node, indented by depth, with the node’s name, a bar and the counts of the agents it returned SUCCESS, RUNNING and FAILURE for at the last tick (a dot for none). Call it from draw, after the scene. It leaves the colour, blend mode, line width and transform as they were.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| x | float | Left edge of the panel, in the current coordinates. | required |
| y | float | Top edge of the panel. | required |
Raises
| Name | Type | Description |
|---|---|---|
| RuntimeError | If no App is running. |
tick
agents.Tree.tick()Ticks the tree once for every agent.
Agents that a higher-priority branch takes away from a running branch are halted (see the module notes), so their leaves’ halt hooks run during the tick.
Returns
| Name | Type | Description |
|---|---|---|
| np.ndarray | The root’s status per agent, an int8 array of len(pop) (Status values). |
Raises
| Name | Type | Description |
|---|---|---|
| RuntimeError | If a population without ids (pop.id) changed size, or a population reordered its agents. |
|
| ValueError | If a leaf returns the wrong number of statuses or a value that is not a Status, or a condition returns anything but booleans. |
agents.Sequence
agents.Sequence(*children, memory=False, name=None)Ticks its children in order while they succeed.
For each agent it stops at the first child that returns FAILURE or RUNNING and takes that status; it returns SUCCESS when every child succeeds (an empty Sequence succeeds). Guards go first: Sequence(battery_low, fly_home).
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| *children | _Node | The child nodes, in order. | () |
| memory | bool | When True, an agent the sequence was RUNNING for resumes at its running child on the next tick, skipping the children before it: use it for a chain of steps (fly home, land, charge). When False, every tick starts again at the first child, so the guards are re-checked and a failing guard halts the running step. | False |
| name | str | None | The name the overlay and Tree.nodes show for this node, instead of "Sequence". Set it here: changing .name after the node is in a Tree has no effect, as the tree takes its names when it is built. |
None |
Raises
| Name | Type | Description |
|---|---|---|
| TypeError | If a child is not a tree node, or name is not a str. |
|
| ValueError | If there are more than 127 children, or name is empty. |
agents.Selector
agents.Selector(*children, memory=False, name=None)Tries its children in priority order until one succeeds or runs.
For each agent it stops at the first child that returns SUCCESS or RUNNING and takes that status; it returns FAILURE when every child fails (an empty Selector fails). The first child is the highest priority.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| *children | _Node | The child nodes, highest priority first. | () |
| memory | bool | When True, an agent the selector was RUNNING for resumes at its running child, without re-checking the higher priorities. When False, the higher priorities are re-checked every tick, and one that succeeds or runs halts the lower child the agent was running. | False |
| name | str | None | The name the overlay and Tree.nodes show for this node, instead of "Selector". Set it here: changing .name after the node is in a Tree has no effect, as the tree takes its names when it is built. |
None |
Raises
| Name | Type | Description |
|---|---|---|
| TypeError | If a child is not a tree node, or name is not a str. |
|
| ValueError | If there are more than 127 children, or name is empty. |
agents.leaf
agents.leaf(fn=None, *, halt=None, name=None)Makes a tree leaf from a function that acts on many agents at once.
The function is called as fn(pop, idx) with the indices of the agents that reach the leaf this tick, and returns a Status per agent in idx (an array of len(idx)) or a single Status for all of them::
@agents.leaf
def fly_home(pop, idx):
pop.target[idx] = pop.home[idx]
arrived = np.linalg.norm(pop.pos[idx] - pop.home[idx], axis=1) < 1
return np.where(arrived, agents.Status.SUCCESS, agents.Status.RUNNING)
Use it bare, or with a halt hook. The hook is called as halt(pop, idx) with the agents that were RUNNING this leaf when a higher-priority branch took them away, so the leaf can undo what it started::
def stop_charging(pop, idx):
pop.charging[idx] = False
@agents.leaf(halt=stop_charging)
def charge(pop, idx):
pop.charging[idx] = True
pop.battery[idx] += 0.01
return np.where(pop.battery[idx] >= 1, agents.Status.SUCCESS, agents.Status.RUNNING)
The hook runs after the branch that took over has ticked those agents, so it should reset the leaf’s own bookkeeping (pop.charging above), never fields that other leaves write (such as pop.vel): it would overwrite what the new branch just did. A leaf placed at two positions in a tree can be called for an agent at one position and halted for the same agent at the other within one tick.
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| fn | Callable[[Any, np.ndarray], Any] | None | The leaf function. It must return Status values (ints 0 to 2); a test that returns booleans is a condition. |
None |
| halt | Callable[[Any, np.ndarray], None] | None | Optional function called with the agents whose run of this leaf was cut off. | None |
| name | str | None | The name the overlay and Tree.nodes show, instead of the function’s name: @agents.leaf(name="go home"). Set it here: changing .name after the leaf is in a Tree has no effect. |
None |
Returns
| Name | Type | Description |
|---|---|---|
| _Leaf | Callable[[Callable[[Any, np.ndarray], Any]], _Leaf] | A tree node, or a decorator when fn is omitted. |
Raises
| Name | Type | Description |
|---|---|---|
| TypeError | If fn or halt is not callable, or name is not a str. |
|
| ValueError | If name is empty. |
agents.condition
agents.condition(fn=None, *, name=None)Makes a tree leaf from a test on many agents at once.
The function is called as fn(pop, idx) and returns one boolean (or 0 or 1) per agent in idx: True gives SUCCESS and False gives FAILURE. A condition is never RUNNING, and any other value raises ValueError at the tick::
@agents.condition
def battery_low(pop, idx):
return pop.battery[idx] < 0.2
Use it bare, or with a name for the overlay: @agents.condition(name="low?").
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| fn | Callable[[Any, np.ndarray], Any] | None | The test function. | None |
| name | str | None | The name the overlay and Tree.nodes show, instead of the function’s name. Set it here: changing .name after the condition is in a Tree has no effect. |
None |
Returns
| Name | Type | Description |
|---|---|---|
| _Leaf | Callable[[Callable[[Any, np.ndarray], Any]], _Leaf] | A tree node, or a decorator when fn is omitted. |
Raises
| Name | Type | Description |
|---|---|---|
| TypeError | If fn is not callable, or name is not a str. |
|
| ValueError | If name is empty. |
agents.is_kind
agents.is_kind(kind, *, name=None)Returns a condition that succeeds for the agents of one kind.
It compares pop.kind (an index into pop.kinds) with the index of kind. Put it first in a branch to give a kind its own behaviour::
agents.Selector(
agents.Sequence(agents.is_kind("predator"), hunt),
graze,
)
Parameters
| Name | Type | Description | Default |
|---|---|---|---|
| kind | str | One of the population’s kinds. Tree checks it against pop.kinds when it is built. |
required |
| name | str | None | The name the overlay and Tree.nodes show, instead of "is_kind('predator')". |
None |
Returns
| Name | Type | Description |
|---|---|---|
| _Leaf | A condition node. |
Raises
| Name | Type | Description |
|---|---|---|
| TypeError | If name is not a str. |
|
| ValueError | If name is empty. |
agents.Status
agents.Status()The result of ticking a node for one agent.
Status arrays compare with the members directly: tree.tick() == Status.RUNNING.