OpenRA-Bench / ENGINE_AUDIT.md
yxc20098's picture
Phase 1 engine audit: ENGINE_AUDIT.md + bench-side closures
c634971
|
Raw
History Blame Contribute Delete
22 kB

A newer version of the Gradio SDK is available: 6.19.0

Upgrade

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

  1. 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).
  2. 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

  1. 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.
  2. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.