One canonical Atari frame (~4 VBLs, ~80ms of game time) must produce one joystick
decision. Right now that decision costs 60–100ms of wall time on a modern
desktop — already past budget before Level 3–5 content adds more hazards.
This maps every layer between ReactiveController.choose() and the
injected input, with the actual multipliers measured from a
full-campaign-flow-field-1 offline profile (frames 0–3260, Level 1 only).
_score alone is 63–66% of total decision time.
Every stage runs once per frame except the two shaded ones — those are the multipliers. Score candidates always runs at 9×horizon×hazards; the beam search only triggers during an active escape, but is a second, independent multiplication on top of it.
flowchart TD
A["Canonical Atari frame\nxenon_client.py"] --> B["Rebuild WorldState + TrackedObjects\nworld_state.py"]
B --> C["Update PersistentWorldMap + A* route\nworld_map.py _ensure_navigation_grid"]
C --> D["Select tactic by precedence\n30+ tactic rows, first match wins"]
D --> E["Rank target, compute aim / intercept"]
E --> F["_relevant_scoring_hazards()\ncull off-screen formation + swarm bodies"]
F --> G["score all 9 joystick candidates\n_score() per candidate"]
G --> H["tactic proposes region\n+ reaction band + route tube"]
H --> I["survival arbitration\ncollision time / exposure / route progress"]
I --> J{"worm / swarm needs\nmulti-frame escape?"}
J -- no --> K["lowest-score legal action"]
J -- yes --> L["_formation_escape_decision()\nbeam search, width 24-48, depth <=48"]
L --> K
K --> M["add alternating fire bit, inject, step frame"]
style G fill:#c96a1c22,stroke:#c96a1c,stroke-width:2px
style L fill:#c4324a22,stroke:#c4324a,stroke-width:2px
style C fill:#0f8f8222,stroke:#0f8f82,stroke-width:1.5px
Measured with validate_campaign.py profile ... --cprofile over 3,260
Level 1 frames (2,318 of them real decisions). 807M total function calls
in that window — roughly 247,000 per frame.
_score() — candidate scoring
Called 20,862 times over the window — exactly 9× every decision
frame. prediction_limit is a single shared value: if any
hazard that frame is a swarm member, the horizon for the whole call
jumps to 56, even for hazards that would only need 8.
Hazard geometry itself is already cached once per (frame, horizon) —
_scoring_hazard_sweeps. What still runs 9× is the
distance/contact test between that cached geometry and each candidate's
own predicted position.
_formation_escape_decision() — beam searchEvery call is a two-tier gate: a cheap revalidation of an already-retained plan runs first, and most calls exit there. When it falls through to a fresh search, the node count is horizon × beam_width × 9 — up to ~20,700 node expansions for one decision, each doing its own hull/collision math.
This is a second, independent multiplication layered on top of candidate scoring — not shared work with it — and only triggers during active worm/swarm escape, so its cost is bursty rather than constant.
_ensure_navigation_grid() — route / A*Scales with discovered map area, not with hazard count or candidate count — a different axis entirely from the two above. Flagged here because it was the third-largest single contributor in the same profiling window, not because it shares the candidate×horizon×hazard shape.
_score callsThe candidate count is fixed; everything else is a multiplier the current architecture pays repeatedly rather than once.
| Parameter | Current range | Where it multiplies | Growth |
|---|---|---|---|
| CANDIDATES | 9 (fixed) | Outer loop of _score AND the branching factor of every beam node |
linear |
| prediction_limit | 8 base → 56 | Inner horizon loop in _score; shared across all 9 candidates and ALL hazards that frame, even ones that don't need it |
multiplies scoring |
| hazard count | ~13 kept / ~24 raw | Innermost loop of _score; re-walked once per candidate per horizon frame |
multiplies scoring |
| beam_width | 24 – 48 | Nodes retained per beam-search frame; each expands into 9 children | multiplies beam |
| beam horizon | ≤ 48 frames | Outer loop of the beam search, compounding with beam_width × 9 | compounds beam |
| discovered map area | grows with progress | _ensure_navigation_grid rebuild cost, independent axis |
linear |
prediction_limit is coarse-grained — one shared horizon for
every hazard in a frame, driven by the single longest-horizon hazard present.