# OpenRA-Bench → Unified Eval Stack Plan ## Goal Turn **OpenRA-Bench** into an efficient, customizable harness to evaluate model performance on **spatial, multi-modal, and complex multi-target multi-step reasoning / planning**, by reusing the mature eval stack from **OpenRA-RL-Training** on top of the **Rust** environment (OpenRA-Rust). Two evaluation modes: 1. **Fixed-scenario eval** — N episodes of one model on a controlled scenario, scored with verifiable rubrics + composite score + 95% CIs. 2. **Pairwise adversarial 1v1** — two models in one 1v1 map, win-rate / Elo. Scenarios must be *controlled* and isolate specific capabilities (e.g. the observed failure: model cannot connect an unexplored region to a target area to explore — a vision/perception + spatial-planning deficiency). ## Core framing: the Perception → Reasoning → Action chain Every scenario evaluates one chain: **read the (visual + symbolic) state → form a multi-step plan → emit valid commands that execute it**. The eval must *attribute* failure to a specific link, not just report a score: - **Perception** — did the model correctly read the minimap/state? (e.g. locate the unexplored region and the target). Probe via state-readback / forced-choice checks derived from ground-truth obs. - **Reasoning** — given correct perception, did it form a valid plan? (e.g. connect unexplored→target, sequence multi-target order). Probe via plan quality vs optimal (path length, target ordering, sub-goal coverage). - **Action** — did it emit syntactically/semantically valid commands that realize the plan? Probe via action-validity rate and plan↔execution drift. Per-scenario rubrics carry one diagnostic per link so a low score points at the broken link. This is the primary product differentiator vs a raw win-rate bench. ## Decisions (locked) - **Repo**: refactor OpenRA-Bench in place; it imports Training's eval stack. OpenRA-RL-Training stays source of truth for the engine code. - **Backend**: Rust only (`openra_train` PyO3). C# (openra-rl) is slow/fragile and is dropped from the eval path. Rust must be made faithful to the C# reference where scenarios require it. - **Multi-modal**: reuse Training's `minimap_renderer.render_minimap()` PNG, injected as `image_url` in the agent prompt. ## Source components reused from OpenRA-RL-Training | Component | Path | Role in Bench | |---|---|---| | Episode engine | `openra_rl_training/training/agent_rollout.py` (`play_episodes_async`) | Drives the real model loop (currently Bench's agent fn is a no-op) | | Reward dims | `training/reward_funcs.py` | Per-scenario weighted scoring | | Rust pool | `training/rust_env_pool.py` | The only backend | | Minimap | `training/minimap_renderer.py` | Multi-modal observation | | Scenarios/rubrics | `scenarios/*.yaml`, `curricula/*.yaml`, verifiable metrics | Controlled tasks + pass/fail | | CI comparison | `scripts/build_eval_comparison.py` | Stat-sound model comparison | ## Rust faithfulness gap (drives sequencing) - Commands: 3/22 (Move, Attack, Observe). Missing: Build/Train/Harvest/Deploy/ Sell/Repair/Stance/Transport/Power/RallyPoint/Guard/Patrol… - Observations: ~30% of C# proto. Missing: economy, production, military stats, spatial tensor, kill_events, result/reward fields. - Scenarios: 2 hand-built (rush-hour, scout-maginot). No generic `.oramap` load. - Engine: movement (A*), combat, projectiles, fog, static defenses = done. Economy, production/tech, transport, multi-armament = not done. → Movement/combat/fog scenarios + combat-only 1v1 work **today**. Economy / production / tech scenarios require Rust engine work first. ## CRITICAL FINDING (verified end-to-end, local) `play_episodes_async` (agent_rollout.py:4815) is **hardwired to the C# gRPC server** via `openra_env.mcp_ws_client.OpenRAMCPClient` and is entangled with TRL (tokenizer, prompt_ids/completion_ids, worker pool, partial cache). It is **not reusable as-is on Rust**. `rust_env_pool` is used only by the lighter `rollout.py` path. Also: `minimap_renderer.render_minimap()` expects `state["minimap"]` (ASCII), `units_summary`, `enemy_summary` — **none of which the Rust env emits.** Verified live Rust obs schema (`openra_train` rush-hour, local wheel, Python 3.12, anaconda): ``` keys = enemy_buildings_summary, enemy_hp, enemy_positions, explored_cells, explored_percent, game_tick, unit_hp, unit_positions, units_killed unit_positions = {actor_id: {cell_x, cell_y[, target, activity, ...]}} step() -> (obs, reward=0.0 hardcoded, done:bool, info={game_tick, warnings}) ``` No `minimap` ASCII, no economy/military/result/reward, no terrain. **Consequence:** Phase 0 builds a Bench-side episode loop that reuses *components* (reward_funcs, minimap_renderer, scenario loader, action parser) behind a **Rust→schema adapter** (`openra_bench/rust_adapter.py`). The adapter is the crux and overlaps directly with the "make Rust faithful" workstream: - `unit_positions` → `units_summary` (renderer/prompt schema) - `enemy_positions` + `enemy_buildings_summary` → `enemy_summary` - synthesize ASCII `minimap` from `explored_cells` + scenario map dims - load `terrain_png` from the scenario's base `.oramap` (as Training does) - derive scoring signals (kills, discovery, exploration, outcome) from obs deltas since Rust `reward` is hardcoded 0.0 — feeds reward_funcs + the P/R/A diagnostics directly. ## Target Bench layout ``` OpenRA-Bench/ openra_bench/ eval_core.py # thin wrapper over play_episodes_async, Rust backend forced agent.py # REAL model agent (OpenAI-compatible), minimap multimodal scenarios/ # controlled eval scenarios (symlink/copy + Bench-authored) rubrics.py # verifiable + composite scoring (reuse Training) pairwise.py # 1v1 adversarial orchestration + Elo evaluate.py # fixed-scenario CLI (rewritten to use eval_core) compare.py # CI comparison front-end (wraps build_eval_comparison) app.py # leaderboard (fed by both modes) ``` ## Phases ### Phase 0 — Integration spine (no Rust changes) - Bench depends on `openra_rl_training` + `openra_train`. - `eval_core.py`: wrap `play_episodes_async`, force Rust pool. - `agent.py`: real OpenAI-compatible model agent w/ minimap PNG. - Rewrite `evaluate.py` → fixed-scenario eval producing `eval_stats.json`. - `compare.py` → 95% CI tables. Wire results into `app.py` leaderboard. - Validate on rush-hour + scout-maginot (these *are* the perception tasks). ### Phase 1 — Adversarial 1v1 (combat-only, current mechanics) - Rust: add a second RL-controlled player slot in a 1v1 map (both sides accept Commands; remove scripted-enemy assumption). - Bench `pairwise.py`: two-model orchestration, win-rate + Elo, leaderboard. ### Phase 2 — Controlled scenario library (current mechanics) - Author perception/spatial scenarios that isolate the unexplored→target connection failure; maze/chokepoint pathfinding; multi-target prioritization. - Verifiable rubrics per scenario (intelligence_pct, path-optimality, etc.). ### Phase 3 — Rust mechanics expansion (unlocks scenario families) - 3a Economy: ore/cash/harvester obs + HARVEST cmd + economy reward. - 3b Production/tech: production queue, BUILD/TRAIN, available_production, power. - Each sub-phase ships its scenario family + rubrics. ## Test coverage (live engine, no mocks) `tests/test_rust_integration.py` — 17 tests, ~1.9s, boots real `openra_train` with rule-based bots (idle / charge / hunter): - tool correctness: move_units reaches target; idle units hold; attack path; reset schema; same-seed determinism (bit-for-bit). - corner cases: empty command list, invalid unit id (warns, no raise), invalid attack target, out-of-bounds move — all safe. - invariants: explored% and units_killed monotonic non-decreasing; discovery set cumulative. - stack: adapter signal tracking; win-condition predicates + composites + unknown-key rejection; **deterministic win/fail plumbing** (trivially true win/fail conditions ⇒ exact outcome, not bot-skill dependent); all authored packs run end-to-end. ## Sequencing (locked) `Phase 0 → Phase 2 (+ P/R/A diagnostics) → Phase 1 → Phase 3` Scenario breadth + per-link diagnostics first: fastest path to exposing real model strengths/weaknesses on current Rust mechanics. Adversarial 1v1 and the economy/production engine work follow. ## Model provider abstraction (Phase 0) `openra_bench/agent.py` exposes a provider-agnostic agent. Adapters: - **openai-compatible** (default): covers local vLLM (matches Training's rollout path) and **OpenRouter** (test target). Same Chat Completions + multimodal `image_url` for the minimap PNG. Base URL + key from config/env. - **bedrock**: separate adapter (AWS SDK / Converse API), added when needed. Selected via Bench config (`provider`, `base_url`, `model`, `api_key_env`). Phase 0 validates with OpenRouter.