Skip to content

Bots & behavior trees

Bots are engine-controlled players. A ruleset declares one or more bot classes; each has movement, state, GM-tunable knobs, and a behavior tree re-evaluated every tick.

interface BotClass {
id: string;
name: string;
description?: string;
teamId?: string;
initialState?: Record<string, unknown>;
speedMps?: number; // default 2
minCount?: number; // pre-staged; can't remove below this
maxCount?: number; // adds clamp to headroom
behavior?: BehaviorNode;
tunable?: TunableParam[];
}
  • id — referenced by spawned bots (Player.botClassId) and by GM tunable calls.
  • name — shown on the GM’s bot card; also the prefix for spawned bot names ("Tag Bot 1", "Tag Bot 2").
  • teamId — team to place bots on. Omit for free-for-all games.
  • initialState — starting state for every bot of this class (the same bag humans get from playerJoinStates).
  • speedMps — movement speed, meters/second (default 2). GM-tunable when listed in tunable.
  • minCount / maxCount — population floor/ceiling. minCount bots are pre-staged at session creation; the engine refuses to drop below it. Omit for none.
  • behavior — the behavior tree (below). If omitted, bots fall back to a legacy state.behavior lookup.
  • tunable — numeric knobs exposed to the GM.
bots:
- id: tag-bot
name: "Tag Bot"
description: "Chases when it's 'it'; flees otherwise."
initialState: { it: false }
minCount: 0
maxCount: 12
speedMps: 2
tunable:
- { key: speedMps, label: "Speed (m/s)", min: 0.5, max: 8, step: 0.5 }
behavior: { kind: selector, children: [ /* … */ ] }

Source: apps/wage-engine/src/games/tag/game.yaml.

The tree uses the same nested kind/children grammar as conditions, so authors learn one shape. It’s evaluated top-down every tick and the chosen action sets the bot’s heading for that tick.

type BehaviorNode =
| { kind: "selector"; children: BehaviorNode[] }
| { kind: "sequence"; children: BehaviorNode[] }
| { kind: "state_equals"; key: string; value: unknown }
| { kind: "chase_nearest"; where?: { key: string; value: unknown } }
| { kind: "flee_nearest"; where?: { key: string; value: unknown } }
| { kind: "wander" };
  • selector — try children left-to-right; succeed at the first that succeeds (logical OR — “first applicable behavior wins”).
  • sequence — run children left-to-right; succeed only if all succeed (logical AND — “guard, then act”).
  • state_equals { key, value } — succeeds iff the bot’s own state[key] strictly equals value. Used as a guard inside a sequence.

Each sets the bot’s intended heading and reports success/failure:

  • chase_nearest { where? } — head toward the nearest matching player; fails if no match exists.
  • flee_nearest { where? } — head away from the nearest matching player; fails if no match.
  • wander — hold the current bearing (or pick a random one); always succeeds — the natural fallback at the end of a selector.

where: { key, value } filters candidate target players by a single state equality. Omit it to match any other player.

behavior:
kind: selector
children:
- kind: sequence # If it → chase a non-it player
children:
- { kind: state_equals, key: it, value: true }
- { kind: chase_nearest, where: { key: it, value: false } }
- kind: sequence # Else if not it → flee the it player
children:
- { kind: state_equals, key: it, value: false }
- { kind: flee_nearest, where: { key: it, value: true } }
- { kind: wander } # Fallback
interface TunableParam {
key: string; // a field name on BotClass, e.g. "speedMps"
label?: string; // GM-facing name; defaults to key
min?: number;
max?: number;
step?: number; // default 1
}

Listing a field in tunable gives the GM a slider. The class field is the default; the GM’s override (per class, or per bot) wins at evaluation time.

tunable:
- { key: speedMps, label: "Speed (m/s)", min: 0.5, max: 8, step: 0.5 }
{ humanTeamId: string; botTeamId?: string }

A top-level ruleset field (not part of a bot class). It pins human WebSocket joiners to humanTeamId instead of auto-balancing across all teams — required for asymmetric “humans vs bots” games so players never get sorted onto the bot team.

teamAssignment:
humanTeamId: blue
botTeamId: red

Source: apps/wage-engine/src/games/bots_vs_humans/game.json.