Writing Behavior Trees
Behavior trees are the core abstraction that drives swarmtest agents. Each agent executes a behavior tree on every tick, deciding which game actions to perform based on the current game state.
Tree Node Types
swarmtest behavior trees are JSON objects with a discriminated type field. There are eight node types:
sequence
Runs children in order. Stops on the first failure. Returns success only if all children succeed.
{
"type": "sequence",
"children": [
{ "type": "action", "name": "move", "params": { "direction": "north" } },
{ "type": "wait", "ticks": 5 },
{ "type": "action", "name": "move", "params": { "direction": "south" } }
]
}selector
Tries children in order. Stops on the first success. Returns failure only if all children fail. Useful for fallback logic.
{
"type": "selector",
"children": [
{ "type": "action", "name": "battle_attack", "params": { "move_index": 0 } },
{ "type": "action", "name": "move", "params": { "direction": "__random__" } }
]
}repeat
Repeats a child node a fixed number of times, or forever.
{ "type": "repeat", "count": "forever", "child": { "type": "action", "name": "move", "params": { "direction": "__random__" } } }random_selector
Picks a random child based on weights. Useful for creating varied agent behavior.
{
"type": "random_selector",
"children": [
{ "type": "action", "name": "move", "params": { "direction": "__random__" } },
{ "type": "action", "name": "interact_npc" },
{ "type": "wait", "ticks": 3 }
],
"weights": [5, 2, 1]
}action
A leaf node that sends a game action through the adapter. If the action is not currently available (as determined by adapter.getAvailableActions()), the node returns failure.
{ "type": "action", "name": "move", "params": { "direction": "north" } }Use "__random__" as a parameter value to randomly select from the available options defined by the adapter’s ParamSpec:
{ "type": "action", "name": "move", "params": { "direction": "__random__" } }wait
Waits for a specified number of ticks before returning success. Returns running until the wait is complete.
{ "type": "wait", "ticks": 10 }condition
Branches based on a predicate evaluated against the agent’s game state. The available predicates are:
| Predicate | Description |
|---|---|
in_battle | Agent is currently in a battle |
exploring | Agent is in the exploring phase |
in_trade | Agent is in a trade |
in_npc_dialog | Agent is talking to an NPC |
evolution_pending | A creature evolution is pending |
move_learn_pending | A new move can be learned |
has_team | Agent has at least one team member |
team_healthy | At least one team member has HP > 0 |
has_money | Agent has money > 0 |
{
"type": "condition",
"predicate": "in_battle",
"then": { "type": "action", "name": "battle_attack", "params": { "move_index": "__random__" } },
"else": { "type": "action", "name": "move", "params": { "direction": "__random__" } }
}probability
Executes the child node with a given probability (0-1). Returns failure if the random check fails.
{ "type": "probability", "chance": 0.3, "child": { "type": "action", "name": "interact_npc" } }The Random Walk Fallback
When LLM generation fails or no behavior tree is assigned, agents fall back to the built-in random walk tree:
{
"type": "repeat",
"count": "forever",
"child": {
"type": "sequence",
"children": [
{
"type": "random_selector",
"children": [
{ "type": "action", "name": "move", "params": { "direction": "__random__" } },
{ "type": "action", "name": "move", "params": { "direction": "__random__" } },
{ "type": "action", "name": "move", "params": { "direction": "__random__" } },
{ "type": "wait", "ticks": 3 }
],
"weights": [3, 3, 3, 1]
}
]
}
}LLM-Generated Trees
When LLM generation is enabled, swarmtest prompts Claude with:
- The game context (from
adapter.describeGameContext()) - All available actions and their parameters (from
adapter.describeAvailableActions()) - The full TreeNode JSON schema
- Summaries of existing trees in the library (to encourage diversity)
- A focus area (e.g., “battle system stress”, “rapid connect/disconnect cycling”)
Claude returns a valid JSON behavior tree that is validated against the TreeNode schema before use.
Writing Handwritten Trees
For adapter-specific behavior, you can write trees by hand. Export a function that returns a TreeNode:
import type { TreeNode } from 'swarmtest';
export function myCustomTree(): TreeNode {
return {
type: 'repeat',
count: 'forever',
child: {
type: 'selector',
children: [
{
type: 'condition',
predicate: 'in_battle',
then: {
type: 'sequence',
children: [
{ type: 'action', name: 'battle_attack', params: { move_index: '__random__' } },
{ type: 'wait', ticks: 2 },
],
},
},
{
type: 'random_selector',
children: [
{ type: 'action', name: 'move', params: { direction: '__random__' } },
{ type: 'action', name: 'interact_npc' },
],
weights: [3, 1],
},
],
},
};
}Then pass handwritten trees to the swarm config:
await swarm.run({
// ...
handwrittenTrees: [myCustomTree()],
});Saved Trees and the Tree Library
Behavior trees that trigger findings with severity crash, bug, or jank are automatically recorded and saved to the tree library on disk (as JSON files in ./trees/<game>/). These saved trees include metadata:
- id – Unique identifier (e.g.,
tree-1771647225060-tgbfcs) - game – Which game adapter the tree was used with
- source –
llm,handwritten, orrecorded - findings – IDs of findings this tree triggered
- actionSequenceHash – Hash for deduplication
- tags – Categories from associated findings (e.g.,
crash,desync)
On subsequent runs, these saved trees are loaded as regression tests.