2.6 KiB
bots — host AI
Authority tick: observation → memory → utility → task → route → motor → UserCmd.
bot dummy [N] creates bots with no controller or perception queries. They keep
their aim and weapon, accept bot tp/give/fire, and take damage and respawn normally.
They retain normal bot loadouts/perks; bot hold off leaves dummies idle.
Bots submit TickInput.cmds like players — SIM-STEP.md.
| file | owns |
|---|---|
observation.rs / memory.rs |
what a bot may know, and for how long |
sensor.rs |
visibility probes and the rotation that spends them |
controller.rs / task.rs / intent.rs |
scores, task choice, stages |
nav.rs |
the baked graph, A*, resumable route requests |
query.rs |
the shared collision budget every subsystem draws on |
weapon.rs / motor.rs |
the weapon channel; aim and movement |
The three rules that shape the rest
A negative needs a probe that ran. Visibility answers seen, evaluated and not seen, or not evaluated. Only the second ends a contact; the third holds it, briefly and without coordinates. Firing needs a positive observation of that client, fresh on the tick the command goes out.
A denied query is not a collision. Perception, connectors, execution and combat share one 96-query quota. A query that ran reports what it found even on the last token; one that never ran comes back as denial, so no refusal is ever fabricated from an empty budget.
Work survives a tick that is granted nothing. A route request owns its phase — approach, attachments, search, validation — and every cursor inside it is resumable, so a zero allocation repeats nothing and A* keeps its frontier under the shared 2048-expansion quota. Answers that follow from graph topology alone are cached per request key until the graph changes; collision-dependent refusals are rechecked.
Movement and combat
The graph bakes world-aligned tiles at 48 units, refined to 24 near missing support or a height change, with directed Walk, Drop and BreakGlass edges that keep their entry and exit through execution. A fight moves to a position: one baked support per sector around the threat, checked for line of fire and exposure, then reached through the ordinary route executor — reachability is never inferred from a component label.
HostController::decision() exposes scores, stage and objective; the periodic
aggregate adds per-subsystem query counts and stuck bots. Both read live from a
running match, which is where bot behaviour is judged: make map mp_boneyard
(RUN.md) and watch the aggregate.