Spaces:
Running
A newer version of the Gradio SDK is available: 6.19.0
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-simlib testgamerules::tests::defaults_have_all_common_unitsโ MCV vs Vehicle kind classification regression.openra-simintegration testssync_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_buildingreportsPLACE BLOCKED: pbox not completed in queuerepeatedly in the relatedtest_pbox_fires.py. The shared symptom suggests a production-queue advance regression in one of the recent merges (order_place_building.has_completedevaluates 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
productionETA: the briefing-levelproductionfield losesprogress/donebecauseRustObsAdaptercollapses 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 inrender_state()or documenting that the model must use the raw obs.spatialdocumented but discoverability is low:SPATIAL_CHANNELS = 6lives in the engine doc-comment; the bench doesn't surface a schema describing what channel means what. Add a sentence toagent.py::build_briefingor the prompt-v2 system text.- 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
place_building"completion" race regression โ the pre-existing failures intests/test_parallel_production.pyandtests/test_pbox_fires.py(engine logsPLACE BLOCKED: <type> not completed in queueeven after the build timer should have expired) point at a regression inorder_place_building'sis_done()check or in the production-queue tick advance. Likely landed in one of the recent merges (2a1cd30naval,9f2181bair,0a13243resource,b828c3bsuperweapon). 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.
- Who-affected: every
P1 โ capability gap closing real-world packs
Helicopter passenger carry โ
transport_capacity("tran") == 5butEnterTransportpath uses ground pathfinding only; atranactor cannot actually board passengers via theMobileactivity tick. Either implement aircraft-load (Aircraft kind needs its own board tick), or droptranfromtransport_capacityto 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).
Naval landing craft (LST) ship-to-shore unload โ
transport_capacity("lst") == 5but 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 forEnterTransport, or an explicitDockactivity 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
Production ETA surfacing in
render_state()โ adapter collapsesproductionto item-name list; the briefing can't say "tank in 4 turns" without the model reading the raw obs. Surface asproduction: [{item, eta_ticks, done}]inRustObsAdapter.render_state().- Who-affected: every reasoning pack ("how many turns of cash do I have to spare?").
- Effort: ~1 hour.
Spatial-tensor channel schema in prompt-v2 โ
SPATIAL_CHANNELS = 6is documented in engine code but not in the system prompt the model sees. Add a one-line description tobriefing_image_primaryso an image-channel model knows what each plane means.- Who-affected:
image-*perception ablation cells. - Effort: ~30 minutes.
- Who-affected:
P3 โ small footguns
find_refinery_fromfallback 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 inlast_warnings.- Effort: ~1 hour.
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 benchCLAUDE.md. If the pack wants per-base supply chains, the model has toset_primaryon the new proc.- Effort: 0 โ already documented.
SetPrimarylacks a dedicated Rust unit test โ exercised indirectly viafind_spawn_location'sprimary_buildingssort key but never in isolation.- Effort: ~1 hour to add.
CancelProductionlacks 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โ addedspawn_unit_near_building,find_refinery_from,spawn_unit_at; refactoredspawn_unitto sharespawn_unit_at; wiredorder_place_buildingproc-harv auto-spawn to use the new helpers; wiredharvester_start_deliverystale-id resolve to prefer path-shortest.
New tests
OpenRA-Rust/openra-sim/tests/test_proc_auto_spawn_at_new_proc.rsโ 1 testOpenRA-Rust/openra-sim/tests/test_apc_transport.rsโ 1 testOpenRA-Bench/tests/test_proc_auto_spawn_python.pyโ 1 testOpenRA-Bench/tests/test_apc_transport_end_to_end.pyโ 1 testOpenRA-Bench/tests/test_superweapons_python.pyโ 4 tests
Bench surface
OpenRA-Bench/openra_bench/agent.pyโ addedfire_superweapontool entry +_to_commandscase.OpenRA-Bench/tests/test_tools.pyโ correctedtest_wildcard_exposes_everythingexpectation (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_superweaponPython surface.OpenRA-Bench/ENGINE_AUDIT.mdโ this file.
All changes uncommitted per the Phase 1 constraint.