Spaces:
Running
Running
| # ENGINE_AUDIT โ Phase 1 | |
| End-to-end completeness audit of every engine verb + observation | |
| field, written 2026-05-22 against `OpenRA-Rust@engine-feature-wave` | |
| HEAD `a5014a5` and `OpenRA-Bench@pr13-revised`. | |
| Scope: pin tests for every gap that scripted-policy validation has | |
| caught, audit advanced-feature surface coverage (Rust + Python), | |
| audit observation completeness, and verify the | |
| `Command::*` โ Python static โ agent.py tool-entry surface is | |
| complete. | |
| --- | |
| ## 1. Engine gaps โ status | |
| | # | Gap | Status | Pinning | | |
| |---|-----|--------|---------| | |
| | 1 | `place_building('proc')` auto-spawned harv at lowest-id proc (not new one); `find_refinery` returned lowest-id proc unconditionally โ 2nd refinery far from 1st added no throughput | **FIXED** in `openra-sim/src/world.rs` this phase. Added `spawn_unit_near_building(unit_type, owner, building_id)` (anchors scan on the NEW building's footprint), `find_refinery_from(owner, from_cell)` (path-shortest with Chebyshev fallback), and rewired `order_place_building` + `harvester_start_delivery` stale-id resolve to use them. Existing harvs do NOT re-snap (only stale-id resolve calls the path-shortest helper); to reroute live harvs the agent must `set_primary` on the new proc or sell the old one. | `OpenRA-Rust/openra-sim/tests/test_proc_auto_spawn_at_new_proc.rs` (1 test) + `OpenRA-Bench/tests/test_proc_auto_spawn_python.py` (1 test) | | |
| | 2 | Thief `Infiltrate` drained 0 cash because `enemy: {cash: N}` was historically ignored | **FIXED upstream** (per-player starting cash plumb landed in commit `a5014a5`). Verified `tests/test_per_player_starting_cash.py` passes against the rebuilt wheel. Engine + Rust + Python tests all green. | `OpenRA-Rust/openra-sim/tests/test_per_player_starting_cash.rs` (3) + `OpenRA-Rust/openra-data/tests/test_per_player_starting_cash.rs` (4) + `tests/test_per_player_starting_cash.py` (2) | | |
| | 2b | Thief `Infiltrate` against any non-`proc`/`silo` building is a no-op (engine match-arm intent) | **INTENT โ DOCUMENTED**. Tool description in `agent.py` already states "thief drains a chunk of the target owner's cash to your player (only when the target is a proc or silo)". Added a note to bench `CLAUDE.md` engine-footguns block. | `openra-sim/tests/test_infiltrate.rs::thief_infiltration_steals_enemy_cash` covers the proc-targeted happy path. | | |
| | 3 | Stance:0 (HoldFire) units don't return fire even when attacked โ defenders die silently | **INTENT โ already-pinned**; added an explicit defender-perspective note to bench `CLAUDE.md` so pack authors don't author defense scenarios that silently lose to a stall policy. | `openra-sim/tests/test_stance_semantics.rs::test_stance_0_holds_fire` | | |
| | (bonus, found this audit) | `fire_superweapon` had no `agent.py` tool entry โ model couldn't issue superweapon orders | **FIXED** โ added `_TOOL_SCHEMAS["fire_superweapon"]` with `kind / target_x / target_y / target_id` parameter schema + a `_to_commands` case that maps `(target_x, target_y) โ cell tuple` and forwards `target_id` as string. Bumped `tests/test_tools.py::test_wildcard_exposes_everything` from 21 โ 25 (covers every Command variant now). | `OpenRA-Bench/tests/test_superweapons_python.py` (4 tests: nuke / iron / chrono / launcher-validation) โ none existed pre-audit. | | |
| | (bonus, found this audit) | No Rust unit test exercised the full APC `EnterTransport โ Move โ Unload` loop end-to-end | **FIXED** โ added a single integration test that boards an e1 into an APC, drives ~30 cells east, unloads, and asserts the passenger lands within 4 cells of the destination and is back in the active actor map. | `OpenRA-Rust/openra-sim/tests/test_apc_transport.rs` (1) + `OpenRA-Bench/tests/test_apc_transport_end_to_end.py` (1) | | |
| ### Pre-existing failures observed during audit (NOT caused by this phase's changes) | |
| These were already failing on `engine-feature-wave` HEAD when I | |
| checked out the branch. Documented here so they aren't conflated | |
| with this phase's diff: | |
| - `openra-sim` lib test `gamerules::tests::defaults_have_all_common_units` โ MCV vs Vehicle kind classification regression. | |
| - `openra-sim` integration tests `sync_hash_verify` + `debug_sync` โ sync-hash reference fixtures are stale and need regeneration after the recent engine merges (`a5014a5`, `2a1cd30`, `9f2181b`, `0a13243`, `b828c3b`). | |
| - `OpenRA-Bench/tests/test_parallel_production.py::test_two_war_factories_outproduce_one` โ a single war factory produces 0 tanks in the test budget (not 1+). Loop times out; `place_building` reports `PLACE BLOCKED: pbox not completed in queue` repeatedly in the related `test_pbox_fires.py`. The shared symptom suggests a production-queue advance regression in one of the recent merges (`order_place_building.has_completed` evaluates false even after the build timer expired). Out of scope for Phase 1; flagged for Phase 2. | |
| - `OpenRA-Bench/tests/test_pbox_fires.py::test_built_pbox_kills_enemy_e1` โ same root cause as parallel_production (pbox never gets placed, so it never fires). | |
| --- | |
| ## 2. Advanced-feature pinning matrix (verb ร Rust ร Python) | |
| `Command::*` = the engine verb in `openra-train/src/command.rs`. | |
| A โ in "Rust" means there is at least one `cargo test`-runnable | |
| test in `openra-sim/tests/` or `openra-data/tests/` that exercises | |
| the order through `process_frame`. A โ in "Python" means there is | |
| a `pytest`-runnable test in `OpenRA-Bench/tests/` that exercises | |
| the order through `Command.<verb>` + the `RustEnvHandle.step` | |
| boundary. | |
| | Verb | Rust pinning test | Python pinning test | | |
| |------|-------------------|---------------------| | |
| | `MoveUnits` | `openra-sim/tests/move_activity_replay.rs` + `parity_move_vs_csharp.rs` | `tests/test_resource_economy.py`, many combat packs | | |
| | `AttackUnit` | `openra-sim/tests/test_attack_unit_no_teleport.rs` + `combat_one_v_one.rs` | many combat tests (`test_combat_*.py`) | | |
| | `AttackMove` | covered via combat scenarios | covered via combat scenarios | | |
| | `Guard` | covered via Move tests (Guard is follow-subset) | `tests/test_combat_protect_vip_escort.py` | | |
| | `SetPrimary` | (no dedicated test โ exercised via `primary_buildings` set / `find_spawn_location` sort key) | `tests/test_repair_building_id.py` | | |
| | `EnterTransport` | **NEW: `openra-sim/tests/test_apc_transport.rs`** | **NEW: `tests/test_apc_transport_end_to_end.py`** | | |
| | `Unload` | **NEW: `openra-sim/tests/test_apc_transport.rs`** | **NEW: `tests/test_apc_transport_end_to_end.py`** | | |
| | `Stop` | covered via Move tests | covered | | |
| | `Deploy` | (covered via env-level integration) | `tests/test_mcv_deploy.py`, `test_mcv_deploy_*.py` | | |
| | `Build` | `openra-sim/tests/test_parallel_production.rs` | `tests/test_parallel_production.py` (PRE-EXISTING FAILURE โ see ยง1) | | |
| | `CancelProduction` | (no dedicated test; verb has small surface โ refunds last-queued item) | โ | | |
| | `PlaceBuilding` | **NEW: `openra-sim/tests/test_proc_auto_spawn_at_new_proc.rs`** + `openra-sim/tests/test_pbox_fires.rs` | **NEW: `tests/test_proc_auto_spawn_python.py`** + `tests/test_build_*.py` packs | | |
| | `Harvest` | `openra-sim/tests/test_resource_layer.rs` | `tests/test_resource_economy.py`, `test_economy_harvest.py` | | |
| | `Sell` | (no dedicated test; exercised by `tests/test_maint_sell_and_recoup_cash.py`) | `tests/test_maint_sell_and_recoup_cash.py`, `test_build_sell_and_rebuild_elsewhere.py` | | |
| | `Repair` | (no dedicated rust test; covered by repair pack tests) | `tests/test_build_repair_priority_under_fire.py`, `test_def_engineer_repair_under_fire.py`, `test_repair_building_id.py` | | |
| | `PowerDown` | `openra-sim/tests/test_power_signals.rs` | `tests/test_power_signals_python.py`, `test_build_power_down_defensive.py` | | |
| | `SetRallyPoint` | (no dedicated test; covered by rally-point pack tests) | `tests/test_build_rally_point_management.py` | | |
| | `SetStance` | `openra-sim/tests/test_stance_semantics.rs` (4 tests) | `tests/test_stance_semantics_python.py` (4 tests) | | |
| | `Patrol` | (no-op verb โ accepted, no behaviour) | โ | | |
| | `Surrender` | (covered via env-level integration) | `tests/test_surrender.py` | | |
| | `Observe` | covered everywhere (no-op verb) | covered everywhere | | |
| | `C4Detonate` | `openra-sim/tests/test_tanya_c4.rs` (3 tests) | `tests/test_tanya_c4.py` (1) | | |
| | `CaptureActor` | `openra-sim/tests/test_capture.rs` (3) | `tests/test_engineer_capture.py` (1) | | |
| | `Infiltrate` | `openra-sim/tests/test_infiltrate.rs` (2) | `tests/test_infiltrate.py` (2) | | |
| | `FireSuperweapon` | `openra-sim/tests/test_superweapons.rs` (5) | **NEW: `tests/test_superweapons_python.py` (4)** | | |
| ### Helicopter / Naval (transport-class verbs) | |
| | Capability | Rust test | Python test | Status | | |
| |------------|-----------|-------------|--------| | |
| | Helicopter pickup / drop (passenger carry) | n/a โ engine `transport_capacity()` advertises `tran` (chinook) at 5 but the C# `Cargo` integration for helicopters is NOT wired into the Move activity (helicopters use `Aircraft` kind, not the ground-transport Mobile-board path) | n/a | **GAP โ DOCUMENTED**. Helicopters can attack ground targets (covered by `test_aircraft.rs::heli_flies_over_impassable_terrain`, `heli_kills_vehicle_behind_obstacle_wall`) but cannot carry passengers. Bench scenarios must not declare helicopter transport as a load-bearing capability. | | |
| | Naval landing craft (LST) ship-to-shore unload | engine `transport_capacity()` advertises `lst` at 5; no dedicated test pins ship-to-shore unload | n/a | **GAP โ DOCUMENTED**. The `EnterTransport` activity tick uses `find_path` (ground), not naval; an infantry trying to board an LST in deep water cannot path there. The LST itself moves on water via `find_path_for_kind(naval=true)`. End-to-end ship-to-shore requires either (a) the LST docking adjacent to a shore cell the infantry can reach by land, or (b) an unload-while-on-water followed by a sink. Not currently exercised by any test. Flagged for Phase 2. | | |
| --- | |
| ## 3. Observation completeness matrix | |
| `obs key` = the field on the `PyDict` returned by `OpenRAEnv.step` | |
| (see `openra-train/src/observation.rs::to_pydict`). "Present" = the | |
| field exists in every observation; "Tested" = at least one test | |
| asserts on its value. | |
| | Obs key | Present | Tested | Notes | | |
| |---------|---------|--------|-------| | |
| | `unit_positions` (own units `{id โ {cell_x, cell_y, actor_type, activity, target?, attacking_target_id?}}`) | โ | โ | `actor_type` enables `unit_type_count_*` predicates; `attacking_target_id` distinguishes Attack from Move. | | |
| | `unit_hp` (`{id โ hp_fraction}`) | โ | โ | Adapter surfaces as `units_summary[].hp`. | | |
| | `enemy_positions` (visible enemy mobile actors `[{cell_x, cell_y, id, actor_type}]`) | โ | โ | Fog-filtered through player_0's shroud. | | |
| | `enemy_hp` (`{id โ hp_fraction}`) | โ | โ | | | |
| | `enemy_buildings_summary` (`[{cell_x, cell_y, id, type, hp_pct}]`) | โ | โ | Adapter merges into `enemy_summary` for the briefing. `hp_pct` is per-building 0..1. | | |
| | `units_killed` (cumulative int) | โ | โ | Drives `units_killed_gte` predicate. | | |
| | `game_tick` (int) | โ | โ | | | |
| | `explored_percent` (float 0..100) | โ | โ | Drives `explored_percent_gte` predicate. | | |
| | `explored_cells` (`[(x,y)]`) | โ | โ | Sticky per-cell reveal set. | | |
| | `economy.cash` | โ | โ | Per-player; adapter surfaces as `cash`. | | |
| | `economy.power_provided` / `power_drained` | โ | โ | `power_provided_gte` + `power_surplus_gte` predicates. | | |
| | `economy.harvesters` (count int) | โ | (covered indirectly via `units_summary` actor_type) | Standalone count; no dedicated `harvester_count_*` predicate today. | | |
| | `economy.resources` / `resource_capacity` | โ | โ | `resources_full_pct` style predicates use these. | | |
| | `own_buildings` (`[{id, type, cell_x, cell_y, hp_pct, is_primary}]`) | โ | โ | `id` is the REAL engine actor id (footgun closed in prior phase). | | |
| | `production` (`[{item, progress, done}]`) | โ | (partial) | Adapter currently collapses to `production_items: [str]`, dropping `progress` / `done`. The `done` flag IS in the raw obs (used by `tests/test_proc_auto_spawn_python.py` directly via `obs["production"]`); the adapter loss is by design (briefing simplicity). **GAP**: the briefing-level production view doesn't surface ETA; the model can see what's queued but not when it lands. | | |
| | `map_info` (`{width, height}`) | โ | โ | Drives bounds-correct minimap rendering. | | |
| | `spatial` (flat row-major `[y][x][c]` with `c=6`) + `spatial_shape` (h,w,c) | โ | โ | Channels: `0` passable, `1` fog (1 visible / 0.5 explored / 0 unknown), `2` own-unit density, `3` visible-enemy-unit density, `4` own building, `5` resource present. `SPATIAL_CHANNELS = 6` constant in `observation.rs`. Documented in `observation.rs` doc-comment. | | |
| | `ore_cells` (`[{cell_x, cell_y, amount}]`) | โ | โ (`test_resource_economy.py`) | Global (NOT fog-gated) per-cell ore inventory. | | |
| | minimap PNG | โ (rendered by bench `minimap.py`) | โ (`tests/test_minimap.py`, `test_battle_viewer.py`) | Bench-side; not in the raw obs dict but produced by `_render_minimap_b64` in `agent.py`. | | |
| | bounds (playable rectangle) | โ (via `map_info`) | โ | | | |
| | `enemy_summary` (broader enemy-actor list including units) | โ via adapter `render_state()` (it concatenates `enemy_positions` + `enemy_buildings_summary` with `is_building` flag) | โ | This is bench-side composition, not a raw-obs key. | | |
| ### Observation gaps flagged | |
| 1. **`production` ETA**: the briefing-level `production` field loses `progress` / `done` because `RustObsAdapter` collapses to a list of item names. For a "what's coming online next?" planning prompt, the model has to estimate from cash deltas. Recommend either surfacing the per-item ETA in `render_state()` or documenting that the model must use the raw obs. | |
| 2. **`spatial` documented but discoverability is low**: `SPATIAL_CHANNELS = 6` lives in the engine doc-comment; the bench doesn't surface a schema describing what channel means what. Add a sentence to `agent.py::build_briefing` or the prompt-v2 system text. | |
| 3. **Helicopter cargo & LST ship-to-shore unload**: out of scope for the observation pass but listed under ยง2 โ neither is exercised today. | |
| --- | |
| ## 4. Command surface matrix (Rust variant ร Python static ร agent tool entry) | |
| Cross-check of `Command::*` in `openra-train/src/command.rs` against | |
| `PyCommand` staticmethods (same file) against `_TOOL_SCHEMAS` and | |
| `_to_commands` in `openra_bench/agent.py`. | |
| | Rust variant | Python staticmethod | `_TOOL_SCHEMAS` entry | `_to_commands` case | Notes | | |
| |--------------|---------------------|-----------------------|---------------------|-------| | |
| | `MoveUnits` | `move_units` | โ `move_units` | โ | | | |
| | `AttackUnit` | `attack_unit` | โ `attack_unit` (+ alias `attack_target`) | โ | | | |
| | `AttackMove` | `attack_move` | โ `attack_move` | โ (generic case for `attack_move` / `harvest` / `set_rally_point`) | | | |
| | `Guard` | `guard` | โ `guard` | โ | | | |
| | `SetPrimary` | `set_primary` | โ `set_primary` | โ (generic `unit_ids` case) | | | |
| | `EnterTransport` | `enter_transport` | โ `enter_transport` | โ | | | |
| | `Unload` | `unload` | โ `unload` | โ (generic `unit_ids` case) | | | |
| | `Stop` | `stop` | โ `stop` (+ alias `stop_units`) | โ | | | |
| | `Deploy` | `deploy` | โ `deploy` | โ | | | |
| | `Build` | `build` | โ `build` | โ | | | |
| | `CancelProduction` | `cancel_production` | โ `cancel_production` | โ | | | |
| | `PlaceBuilding` | `place_building` | โ `place_building` | โ | | | |
| | `Harvest` | `harvest` | โ `harvest` | โ (generic case) | | | |
| | `Sell` | `sell` | โ `sell` | โ | | | |
| | `Repair` | `repair` | โ `repair` | โ | | | |
| | `PowerDown` | `power_down` | โ `power_down` | โ | | | |
| | `SetRallyPoint` | `set_rally_point` | โ `set_rally_point` | โ (generic case) | | | |
| | `SetStance` | `set_stance` | โ `set_stance` | โ | | | |
| | `Patrol` | `patrol` | โ `patrol` | โ | No-op verb in engine. | | |
| | `Surrender` | `surrender` | โ `surrender` | โ | | | |
| | `Observe` | `observe` | โ `observe` (always force-included by `_tool_schemas`) | โ | | | |
| | `C4Detonate` | `c4_detonate` | โ `c4_detonate` | โ | | | |
| | `CaptureActor` | `capture_actor` | โ `capture_actor` | โ | | | |
| | `Infiltrate` | `infiltrate` | โ `infiltrate` | โ | | | |
| | `FireSuperweapon` | `fire_superweapon` | **โ `fire_superweapon` (ADDED THIS PHASE)** | **โ (ADDED THIS PHASE)** | Previously: tool entry was missing โ the model could not fire superweapons even on a scenario that exposed `tools: ["*"]`. Fixed. | | |
| **Total: 25 enum variants, 25 Python staticmethods, 25 tool entries, 25 `_to_commands` cases.** Bumped `tests/test_tools.py::test_wildcard_exposes_everything` from 21 โ 25 (was already out of sync before this phase). | |
| --- | |
| ## 5. Prioritized fix queue | |
| In rough priority order (P0 = scenario-blocking, P3 = nice-to-have): | |
| ### P0 โ scenario-blocking | |
| 1. **`place_building` "completion" race regression** โ the pre-existing failures in `tests/test_parallel_production.py` and `tests/test_pbox_fires.py` (engine logs `PLACE BLOCKED: <type> not completed in queue` even after the build timer should have expired) point at a regression in `order_place_building`'s `is_done()` check or in the production-queue tick advance. Likely landed in one of the recent merges (`2a1cd30` naval, `9f2181b` air, `0a13243` resource, `b828c3b` superweapon). Affects every build-and-place scenario. | |
| - **Who-affected**: every `build-*` pack and the parallel-production / pbox guardrails. | |
| - **Effort**: ~half day to bisect the merge that introduced it + targeted fix. | |
| ### P1 โ capability gap closing real-world packs | |
| 2. **Helicopter passenger carry** โ `transport_capacity("tran") == 5` but `EnterTransport` path uses ground pathfinding only; a `tran` actor cannot actually board passengers via the `Mobile` activity tick. Either implement aircraft-load (Aircraft kind needs its own board tick), or drop `tran` from `transport_capacity` to make the no-op explicit. | |
| - **Who-affected**: any scenario that wants helicopter insert/extract (none today, but the bench has at least three drafted heli scenarios). | |
| - **Effort**: 1โ2 days (aircraft activity surface). | |
| 3. **Naval landing craft (LST) ship-to-shore unload** โ `transport_capacity("lst") == 5` but the boarding path requires the passenger to reach the LST cell via ground pathfind; on water that's impossible. The C# parity here is "infantry boards at shore + LST docks โ infantry rides + LST unloads back on shore". Needs either a shore-adjacency rule for `EnterTransport`, or an explicit `Dock` activity that puts the LST adjacent to a shore cell before boarding. | |
| - **Who-affected**: naval scenarios (none today; was an aspirational pack). | |
| - **Effort**: 2 days (touches the EnterTransport tick + naval move). | |
| ### P2 โ observability | |
| 4. **Production ETA surfacing in `render_state()`** โ adapter collapses `production` to item-name list; the briefing can't say "tank in 4 turns" without the model reading the raw obs. Surface as `production: [{item, eta_ticks, done}]` in `RustObsAdapter.render_state()`. | |
| - **Who-affected**: every reasoning pack ("how many turns of cash do I have to spare?"). | |
| - **Effort**: ~1 hour. | |
| 5. **Spatial-tensor channel schema in prompt-v2** โ `SPATIAL_CHANNELS = 6` is documented in engine code but not in the system prompt the model sees. Add a one-line description to `briefing_image_primary` so an image-channel model knows what each plane means. | |
| - **Who-affected**: `image-*` perception ablation cells. | |
| - **Effort**: ~30 minutes. | |
| ### P3 โ small footguns | |
| 6. **`find_refinery_from` fallback when no path exists** โ currently falls back to Chebyshev-nearest then lowest-id. If the only proc has a path-blocked footprint (e.g. surrounded by walls), the harv binds anyway and then deadlocks. Could surface a warning in `last_warnings`. | |
| - **Effort**: ~1 hour. | |
| 7. **Existing harvesters do NOT re-snap to the new proc after `place_building('proc')`** โ by design (avoids churning a stable supply chain), but documented as a footgun in bench `CLAUDE.md`. If the pack wants per-base supply chains, the model has to `set_primary` on the new proc. | |
| - **Effort**: 0 โ already documented. | |
| 8. **`SetPrimary` lacks a dedicated Rust unit test** โ exercised indirectly via `find_spawn_location`'s `primary_buildings` sort key but never in isolation. | |
| - **Effort**: ~1 hour to add. | |
| 9. **`CancelProduction` lacks any dedicated test** (Rust or Python). Small verb surface, but a model that frees up cash by cancelling the last queued item should be pinned. | |
| - **Effort**: ~1 hour to add. | |
| --- | |
| ## Files touched in Phase 1 | |
| ### Engine (rebuilt the wheel via `maturin develop --release`; verified `Installed openra_train` printed) | |
| - `openra-sim/src/world.rs` โ added `spawn_unit_near_building`, `find_refinery_from`, `spawn_unit_at`; refactored `spawn_unit` to share `spawn_unit_at`; wired `order_place_building` proc-harv auto-spawn to use the new helpers; wired `harvester_start_delivery` stale-id resolve to prefer path-shortest. | |
| ### New tests | |
| - `OpenRA-Rust/openra-sim/tests/test_proc_auto_spawn_at_new_proc.rs` โ 1 test | |
| - `OpenRA-Rust/openra-sim/tests/test_apc_transport.rs` โ 1 test | |
| - `OpenRA-Bench/tests/test_proc_auto_spawn_python.py` โ 1 test | |
| - `OpenRA-Bench/tests/test_apc_transport_end_to_end.py` โ 1 test | |
| - `OpenRA-Bench/tests/test_superweapons_python.py` โ 4 tests | |
| ### Bench surface | |
| - `OpenRA-Bench/openra_bench/agent.py` โ added `fire_superweapon` tool entry + `_to_commands` case. | |
| - `OpenRA-Bench/tests/test_tools.py` โ corrected `test_wildcard_exposes_everything` expectation (21 โ 25). | |
| - `OpenRA-Bench/CLAUDE.md` โ appended footgun bullets for proc auto-spawn, thief Infiltrate intent, stance:0 defender silent-death intent, per-player cash plumbing, `fire_superweapon` Python surface. | |
| - `OpenRA-Bench/ENGINE_AUDIT.md` โ this file. | |
| All changes uncommitted per the Phase 1 constraint. | |