ai
stableBehavior tree and blackboard primitives for game AI and agent decision-making, with tick-based tree evaluation.
use plugin ai::{Blackboard.new, Blackboard.set, Blackboard.get, …} Functions (19)
- Blackboard.new Create a shared key-value blackboard
- Blackboard.set Store a value under a key
- Blackboard.get Retrieve a value by key
- Blackboard.has Check whether a key exists
- Blackboard.remove Delete a key
- Blackboard.clear Remove all entries
- Blackboard.keys List all key names
- Blackboard.to_table Convert the blackboard to a plain table
- create_sequence Create a sequence composite node
- create_selector Create a selector composite node
- create_action Create a leaf action node
- create_condition Create a blackboard condition node
- create_parallel Create a parallel composite node
- create_inverter Create an inverter decorator node
- create_repeater Create a repeater decorator node
- create_succeeder Create a succeeder decorator node
- node_type Read the type string of a node
- reset_tree Reset all node statuses to ready
- tick_tree Evaluate a behavior tree one tick
Overview
The ai plugin provides the two classic building blocks of game and agent AI: a behavior tree and a blackboard. Behavior tree nodes are plain tables built with the create_* constructors and composed into a tree; the blackboard is a stateful, handle-backed key-value store that the leaf nodes read during evaluation. Each call to tick_tree walks the tree once and returns one of three statuses — "success", "failure", or "running" — based on the keys currently set on the blackboard. Use it whenever you need declarative, reusable decision logic for an enemy, NPC, or autonomous agent without hand-rolling a state machine.
The leaf nodes are driven entirely by the blackboard. A condition node checks its key for a truthy value; an action node maps true → "success", false → "failure", a string → that string verbatim, and anything missing → "running". Composite nodes (sequence, selector, parallel) and decorators (inverter, repeater, succeeder) combine those results. Because nodes are just tables, you can inspect them with node_type and reset their status with reset_tree between ticks.
Common patterns
Build and tick a tree
Compose constructors into a tree, seed the blackboard, then tick it. The status reflects how the leaves resolved against the blackboard.
use plugin ai::{Blackboard, create_sequence, create_condition, create_action, tick_tree}
let bb = Blackboard.new()
bb.set("enemy_visible", true)
bb.set("attack", true)
let tree = create_sequence([
create_condition("enemy_visible"),
create_action("attack")
])
print(tick_tree(tree, bb))
Fallback behaviour with a selector
A selector tries each child until one succeeds — ideal for "do A, otherwise B" logic. Here the locked door fails, so the agent breaks the window instead.
use plugin ai::{Blackboard, create_selector, create_action, tick_tree}
let bb = Blackboard.new()
bb.set("open_door", false)
bb.set("break_window", true)
let plan = create_selector([
create_action("open_door"),
create_action("break_window")
])
print(tick_tree(plan, bb))
Re-tick a tree across frames
Reset node statuses before re-evaluating, mutating the blackboard each frame to drive the agent.
use plugin ai::{Blackboard, create_sequence, create_action, tick_tree, reset_tree}
let bb = Blackboard.new()
let tree = create_sequence([create_action("patrol"), create_action("rest")])
bb.set("patrol", true)
bb.set("rest", "running")
print(tick_tree(tree, bb))
let tree = reset_tree(tree)
bb.set("rest", true)
print(tick_tree(tree, bb))
Create a shared key-value blackboard
Creates a new blackboard — a shared key-value store used by condition and action nodes during tree evaluation. Pass the resulting handle to tick_tree. The blackboard is the single source of truth that drives leaf-node results.
use plugin ai::{Blackboard}
let bb = Blackboard.new()
bb.set("is_hungry", true)
bb.set("eat", true)
Store a value under a key
Stores value under key, overwriting any existing entry. Values can be any Zolo value, but leaf nodes interpret booleans and strings specially during ticks.
use plugin ai::{Blackboard}
let bb = Blackboard.new()
bb.set("health", 80)
bb.set("alert", true)
bb.set("state", "running")
Retrieve a value by key
Returns the value stored under key, or nil if the key is absent.
use plugin ai::{Blackboard}
let bb = Blackboard.new()
bb.set("health", 80)
print(bb.get("health"))
print(bb.get("mana"))
Check whether a key exists
Returns true if key currently exists on the blackboard, false otherwise.
use plugin ai::{Blackboard}
let bb = Blackboard.new()
bb.set("target", "goblin")
print(bb.has("target"))
print(bb.has("ally"))
Delete a key
Deletes key from the blackboard. Does nothing if the key is not present.
use plugin ai::{Blackboard}
let bb = Blackboard.new()
bb.set("stunned", true)
bb.remove("stunned")
print(bb.has("stunned"))
Remove all entries
Removes every entry from the blackboard, returning it to an empty state. Useful when respawning or fully resetting an agent.
use plugin ai::{Blackboard}
let bb = Blackboard.new()
bb.set("a", 1)
bb.set("b", 2)
bb.clear()
print(bb.has("a"))
List all key names
Returns an array of all key names currently on the blackboard (order is unspecified).
use plugin ai::{Blackboard}
let bb = Blackboard.new()
bb.set("hp", 100)
bb.set("ammo", 30)
for key in bb.keys() {
print(key)
}
Convert the blackboard to a plain table
Returns the entire blackboard as a plain table mapping each key to its stored value — handy for debugging or serializing agent state.
use plugin ai::{Blackboard}
let bb = Blackboard.new()
bb.set("hp", 100)
bb.set("aware", true)
let snapshot = bb.to_table()
print(snapshot["hp"])
print(snapshot["aware"])
Create a sequence composite node
Creates a sequence node that ticks children left to right, returning "success" only if all children succeed. It stops and returns the first non-success result, so a failing or running child short-circuits the rest.
use plugin ai::{create_sequence, create_action, Blackboard, tick_tree}
let bb = Blackboard.new()
bb.set("find_target", true)
bb.set("move_to_target", true)
let seq = create_sequence([create_action("find_target"), create_action("move_to_target")])
print(tick_tree(seq, bb))
A sequence fails as soon as one child fails. Here the second action returns "failure", so the sequence stops there.
use plugin ai::{create_sequence, create_action, Blackboard, tick_tree}
let bb = Blackboard.new()
bb.set("reload", true)
bb.set("aim", false)
let seq = create_sequence([create_action("reload"), create_action("aim")])
print(tick_tree(seq, bb))
Create a selector composite node
Creates a selector node that ticks children left to right, returning "success" as soon as one child succeeds (or "running" if one is running). Returns "failure" only if every child fails.
use plugin ai::{create_selector, create_action, Blackboard, tick_tree}
let bb = Blackboard.new()
bb.set("use_door", false)
bb.set("break_window", true)
let sel = create_selector([create_action("use_door"), create_action("break_window")])
print(tick_tree(sel, bb))
Create a leaf action node
Creates a leaf action node. During tick_tree, the blackboard is checked for a key matching name: true → "success", false → "failure", a string value is returned directly, and anything else (including a missing key) → "running".
use plugin ai::{create_action, Blackboard, tick_tree}
let bb = Blackboard.new()
bb.set("attack", "running")
let node = create_action("attack")
print(tick_tree(node, bb))
An unset action key reports "running", letting you model in-progress work that has not yet resolved.
use plugin ai::{create_action, Blackboard, tick_tree}
let bb = Blackboard.new()
let node = create_action("cast_spell")
print(tick_tree(node, bb))
Create a blackboard condition node
Creates a condition leaf node that checks key on the blackboard. A truthy value yields "success"; false, nil, or a missing key yields "failure".
use plugin ai::{create_condition, Blackboard, tick_tree}
let bb = Blackboard.new()
bb.set("enemy_visible", true)
let cond = create_condition("enemy_visible")
print(tick_tree(cond, bb))
Create a parallel composite node
Creates a parallel node that ticks all children every tick. success_threshold sets how many children must succeed for the node to succeed; the default of -1 means all of them must. It returns "success" once the threshold is met, "failure" when too many children have failed, and "running" while children are still in progress.
use plugin ai::{create_parallel, create_action, Blackboard, tick_tree}
let bb = Blackboard.new()
bb.set("play_anim", true)
bb.set("play_sound", true)
let par = create_parallel([create_action("play_anim"), create_action("play_sound")])
print(tick_tree(par, bb))
With a threshold, the node succeeds once enough children pass, even if others fail. Here only one of two must succeed.
use plugin ai::{create_parallel, create_action, Blackboard, tick_tree}
let bb = Blackboard.new()
bb.set("shoot", true)
bb.set("dodge", false)
let par = create_parallel([create_action("shoot"), create_action("dodge")], 1)
print(tick_tree(par, bb))
Create an inverter decorator node
Creates a decorator that flips the result of its child: "success" becomes "failure" and vice versa. "running" passes through unchanged.
use plugin ai::{create_inverter, create_condition, Blackboard, tick_tree}
let bb = Blackboard.new()
bb.set("is_safe", false)
let inv = create_inverter(create_condition("is_safe"))
print(tick_tree(inv, bb))
Create a repeater decorator node
Creates a decorator that ticks its child up to max_repeats times per tick (default 1) and returns the last child result. It stops early and returns "running" if the child is still running.
use plugin ai::{create_repeater, create_action, Blackboard, tick_tree}
let bb = Blackboard.new()
bb.set("patrol_step", true)
let rep = create_repeater(create_action("patrol_step"), 3)
print(tick_tree(rep, bb))
Create a succeeder decorator node
Creates a decorator that always returns "success" regardless of the child's result. Use it to keep a sequence from failing on optional steps.
use plugin ai::{create_succeeder, create_action, create_sequence, Blackboard, tick_tree}
let bb = Blackboard.new()
bb.set("optional_pickup", false)
bb.set("continue_patrol", true)
let seq = create_sequence([
create_succeeder(create_action("optional_pickup")),
create_action("continue_patrol")
])
print(tick_tree(seq, bb))
Read the type string of a node
Returns the type string of a behavior node table: "sequence", "selector", "action", "condition", "parallel", "inverter", "repeater", or "succeeder". Returns nil if the table has no type field.
use plugin ai::{create_action, node_type}
let node = create_action("jump")
print(node_type(node))
Reset all node statuses to ready
Recursively resets the status field of every node in the tree back to "ready", returning the reset tree. Call this before re-ticking a tree from the start across frames.
use plugin ai::{create_sequence, create_action, reset_tree, node_type}
let tree = create_sequence([create_action("step1"), create_action("step2")])
let tree = reset_tree(tree)
print(node_type(tree))
Evaluate a behavior tree one tick
Evaluates the behavior tree rooted at node for one tick using the provided blackboard, returning "success", "failure", or "running". This is the single entry point that walks composites, decorators, and leaves.
use plugin ai::{Blackboard, create_sequence, create_condition, create_action, tick_tree}
let bb = Blackboard.new()
bb.set("target_found", true)
bb.set("move", true)
let tree = create_sequence([
create_condition("target_found"),
create_action("move")
])
print(tick_tree(tree, bb))
Combine decorators and composites for richer logic. Here a selector falls back to a guarded retreat when the attack branch fails.
use plugin ai::{Blackboard, create_selector, create_sequence, create_condition, create_action, create_inverter, tick_tree}
let bb = Blackboard.new()
bb.set("enemy_visible", true)
bb.set("low_health", true)
bb.set("flee", true)
let tree = create_selector([
create_sequence([
create_inverter(create_condition("low_health")),
create_action("attack")
]),
create_action("flee")
])
print(tick_tree(tree, bb))