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.