yxc20098 commited on
Commit
f5e23f8
·
1 Parent(s): 755ab44

Adversarial 1v1 spotlight: ladder family + rating + Elo wiring

Browse files

- new 'adversarial' capability bucket (schema Literal + leaderboard
CAPABILITIES) — the head-to-head axis an RTS engine uniquely owns
- 3 ladder packs (adversarial-duel/-skirmish/-siege), each a 3-rung
easy→medium→hard escalation of a reactive engine-driven opponent;
per-level actor overrides give real rungs; win=destroy force within
ticks, fail=force wiped, hard adds a loss cap
- adversarial.py: pure ladder_rating (contiguous rungs cleared from
easy; all-seeds-won required) + adversarial_summary
- run_eval emits out['adversarial']; leaderboard ingest records
adversarial_rating + per-pack ladders; app capability table column;
Elo path stays via existing comparative pairwise on shared rungs
- documented swap-in to model-vs-model once the engine exposes an
enemy command channel (pairwise.py / task #3)
- tests: test_adversarial.py (6, incl live-engine pack runs) —
313 passed, 1 skipped

SCENARIO_AUDIT.md ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenRA-Bench — Scenario Audit & Gap Analysis
2
+
3
+ Research + audit. No code changes. Prepared 2026-05-17.
4
+
5
+ ---
6
+
7
+ ## Executive Summary
8
+
9
+ OpenRA-Bench currently ships **85 scenario files** (17 hand-authored
10
+ scenario families + 68 procedurally generated `cat-*` packs across 12
11
+ categories, ~200 difficulty levels + 1 TEMPLATE). The design is
12
+ unusually well-grounded: every win condition is a verifiable predicate,
13
+ difficulty scales by *decision hardness* (info down, decoys up, clock
14
+ down, attrition cap) rather than raw numbers, and each pack carries a
15
+ `real_world_meaning` + `robotics_analogue`. The anti-memorization
16
+ discipline (procedural variation, held-out seeds, generalization-gap
17
+ metric) is consistent with current best practice (Procgen, SMACv2,
18
+ ARC-AGI). The "RTS reasoning transfers" hypothesis is *directly
19
+ supported by published evidence*: lmgame-Bench (arXiv:2505.15146)
20
+ showed RL on Sokoban/Tetris lifted Blocksworld (+≥10pts planning) and
21
+ WebShop (+6pts multi-turn decisions) but **not** math/coding — i.e.
22
+ transfer is real but axis-specific (spatial / planning / embodied),
23
+ exactly the axes this suite targets.
24
+
25
+ **The core finding of this audit:** the capability *taxonomy* is
26
+ sound and the transfer story is defensible, but the **coverage is
27
+ badly skewed**. Frontier models in 2026 are differentiated almost
28
+ entirely on (a) agentic **tool-use / function-calling fidelity** under
29
+ strict APIs (BFCL V4, τ²-bench — GPT-5.5 reports 98% τ²-Telecom,
30
+ Kimi-K2.6 50% Toolathlon), (b) **long-horizon multi-step agentic
31
+ execution** (Terminal-Bench 2.0 — GPT-5.5 82.7%, SWE-bench Verified
32
+ ~89%), and (c) **fluid/abstract + spatial reasoning** (ARC-AGI-2 — the
33
+ single largest 2026 model delta; ERQA for embodied/spatial). OpenRA-Bench
34
+ is *strong* on spatial perception/exploration (FRONT/PERC — parallels
35
+ ERQA, the validated transfer target) and *adequate* on constrained
36
+ planning (PLAN/TECH/ECON — parallels PlanBench/Blocksworld). It is
37
+ **weak on adversarial/game-theoretic reasoning** (no opponent-modeling
38
+ scenario despite an RTS engine — the one capability an RTS bench is
39
+ uniquely positioned to own), **weak on long-horizon credit assignment**
40
+ (most clocks are short single-phase tasks, not 30+ step chains like
41
+ Terminal-Bench/OSWorld), and **structurally weak on the very thing
42
+ 2026 leaderboards weight most: tool-use/instruction-following fidelity
43
+ under a strict action API** — this is *measured* (action-validity
44
+ sub-score) but no scenario *isolates* it as the primary objective.
45
+
46
+ Additionally, ~68 of 85 packs are the auto-generated `cat-*` family:
47
+ 12 categories × ~3 levels × ~variants. Many `cat-c5/c6/c7/c8`
48
+ "budget-allocation / base-placement" packs are near-duplicates of each
49
+ other and of the hand-authored `economy-*` and `building-and-planning`
50
+ scenarios — high level count, low *distinct-capability* count. Several
51
+ economy packs degenerate to identical tests because harvest income is
52
+ 0 on the only loadable map (documented engine-prereq S0/S1) — these
53
+ are honest but currently non-discriminating.
54
+
55
+ **Top recommendations (detail in §4):**
56
+ 1. **Add an adversarial/opponent-modeling family** (the unique RTS
57
+ value prop; parallels game-theoretic + StarCraft-II-Arena evals).
58
+ Activate the Phase-1 1v1 path — this is the highest-leverage gap.
59
+ 2. **Add a strict-action-API instruction-following family** isolating
60
+ tool-call fidelity (parallels BFCL V4 / τ²-bench — the most
61
+ leaderboard-weighted 2026 capability).
62
+ 3. **Add 2–3 genuinely long-horizon scenarios** (multi-phase,
63
+ 40k+ ticks, credit assignment across opening→tech→assault) to
64
+ parallel Terminal-Bench/OSWorld long-task tracks.
65
+ 4. **Cut/merge the redundant `cat-*` over-generation** to ~3 distinct
66
+ levels per *distinct* decision; the level *count* is inflating
67
+ apparent coverage without adding capability breadth.
68
+ 5. **Pre-register the transfer panel against ERQA (spatial/embodied),
69
+ Blocksworld/PlanBench (planning), and BFCL/τ²-bench (tool-use)** —
70
+ ERQA is the empirically-validated correlate for the headline claim;
71
+ BFCL validates the under-tested action-fidelity axis.
72
+
73
+ ---
74
+
75
+ ## STEP 1 — Scenario Inventory
76
+
77
+ ### 1.1 Counts
78
+
79
+ | Group | Files | Notes |
80
+ |---|---|---|
81
+ | Hand-authored scenario families | 17 | perception/reasoning/action/strategy/economy |
82
+ | Generated catalog packs (`cat-*`) | 68 | 12 categories (c1–c12), 5–6 variants × 3 levels each |
83
+ | TEMPLATE | 1 | scaffold, not a scenario |
84
+ | **Total scenario files** | **85** | ~200 difficulty levels |
85
+
86
+ The 12 generated categories (from `SCENARIO_CATALOG.md`, verified
87
+ against the `cat-*` files): C1 Frontier Scouting (FRONT), C2 Threat
88
+ Enumeration (PERC), C3 Tech Critical Path (TECH), C4 Power-Budget
89
+ Online (PLAN), C5 Budget Allocation (ECON), C6 Time-Boxed Capital
90
+ Deploy (ECON), C7 Defensive-Direction Commit (PERC), C8 Base-Placement
91
+ & Staging (PERC), C9 Commit-vs-Retreat (RISK), C10 Force Coordination
92
+ (COORD), C11 Tempo/Timing Window (TEMPO), C12 Error Recovery/Replan
93
+ (RISK).
94
+
95
+ ### 1.2 Capability buckets and what each actually tests
96
+
97
+ The Perception→Reasoning→Action chain is the framing
98
+ (`EVAL_STACK_PLAN.md`); every scenario is scored on all three links
99
+ with a `weakest_link` diagnostic (`openra_bench/scoring.py`), plus a
100
+ continuous `reward_vector` goal tracker (`openra_bench/goal_tracker.py`)
101
+ and pass/fail via pure predicates (`openra_bench/scenarios/win_conditions.py`).
102
+
103
+ | Bucket | Sub-skill | Scenarios | What it actually tests (predicate grammar) |
104
+ |---|---|---|---|
105
+ | **Perception** | spatial state-estimation under fog | `perception-frontier-reading`, `perception-target-vs-fog`, C2, C7, C8 | `explored_pct_gte`, `enemies/buildings_discovered_gte` under a clock + attrition cap. Read the minimap, steer sensing. |
106
+ | **Perception/Front** | which-unexplored-region-to-commit | `reasoning-frontier-commit`, C1 | Frontier selection (pathfinding solved; *choosing* the region is the test). Strong ERQA/Active-Neural-SLAM analogue. |
107
+ | **Reasoning** | constrained sequential planning | C3 (tech path), C4 (power budget), `building-and-planning` | Precondition-ordered build to a deadline with `power_surplus_gte≥0`. Blocksworld/PlanBench analogue. |
108
+ | **Reasoning/Econ** | resource allocation / multi-objective | `economy-investment`, `economy-time-box`, `economy-force-buildup`, C5, C6, `economy-harvest-*` | Convert a fixed budget into units AND infrastructure under a clock; commit a wide-vs-deep allocation. Harvest variants currently non-discriminating (income=0, see §1.3). |
109
+ | **Reasoning/Risk** | risk call / replan under partial info | `reasoning-risk-route`, `strategy-dilemma`, C9 (commit-vs-retreat), C12 (error recovery) | Safe-long vs short-lethal route; engage-vs-hold with attrition cap; rebuild after a setback (`after_ticks` gate). |
110
+ | **Action** | multi-unit coordination / execution | `action-multiunit-coordination`, `strategy-twobody`, C10 | Drive dispersed squads to converge in a region with `units_lost_lte:0`. Watch-And-Help / SMAC analogue. |
111
+ | **Action** | sequenced execution to deadline | `action-sequenced-execution`, `rush-hour`, `strategy-gauntlet` | Ordered build→deploy→reposition without dropping a step; multi-squad sweep-and-clear. |
112
+ | **Tempo** | timing-window discipline | C11 | `after_ticks:t0` then `units_killed_gte:N` within `T1` — act in a window, not before. |
113
+
114
+ All run on the single Rust-loadable map `rush-hour-arena`.
115
+
116
+ ### 1.3 Honest limitations already documented in-repo
117
+
118
+ - **Harvest economy is non-functional** on the only loadable map: the
119
+ Python schema rejects `mine`/`gmine`, the `.oramap` files seed no
120
+ ore, and `silo` storage is hardcoded inert
121
+ (`SCENARIO_BRAINSTORM.md` §"Verified ground-truth constraints").
122
+ Consequence: `economy-harvest-timebox` / `economy-harvest-investment`
123
+ / harvest variants degenerate to spend-only tests identical to the
124
+ non-harvest economy packs. They are honestly tagged but **currently
125
+ non-discriminating** — they inflate the economy-bucket count without
126
+ adding a distinct capability. Blocked on engine-prereqs S0 (ore
127
+ source) + S1 (silo storage).
128
+ - **No enemy-destruction predicate** — offensive success is proxied by
129
+ `units_killed_gte` + positional `reach_region`/`building_in_region`.
130
+ - **No adversarial slot today** — the 1v1/Elo path is Phase-1, not
131
+ built (`EVAL_STACK_PLAN.md`). The "Elo leaderboard" is currently
132
+ only meaningful for the fixed-scenario composite, not head-to-head.
133
+
134
+ ---
135
+
136
+ ## STEP 2 — What Frontier Benchmarks Measure Today (2026)
137
+
138
+ Verified via the four required sources + targeted search. URLs in §5.
139
+
140
+ ### 2.1 Required model pages
141
+
142
+ - **Qwen3.6-35B-A3B** (HF page, released ~Apr 2026). Reports a
143
+ coding/agentic-heavy battery: SWE-bench Verified 73.4, SWE-bench
144
+ Pro 49.5, Terminal-Bench 2.0 51.5, **TAU3-Bench 67.2**, Tool
145
+ Decathlon 26.9, MCPMark 37.0; reasoning: MMLU-Pro 85.2, GPQA 86.0,
146
+ AIME26 92.7; **spatial-intelligence block: EmbSpatialBench 84.3,
147
+ RefSpatialBench 64.3, RefCOCO 92.0**. Takeaway: even a mid-size
148
+ model now reports a *dedicated embodied-spatial benchmark block* and
149
+ a *dedicated tool/agent block* — these are the differentiating axes.
150
+ - **Kimi-K2.6** (HF page). Agentic-first: OSWorld-Verified 73.1,
151
+ **Toolathlon 50.0, MCPMark 55.9**, BrowseComp 83.2, Terminal-Bench
152
+ 2.0 66.7, SWE-bench Verified 80.2; reasoning AIME 2026 96.4,
153
+ GPQA-Diamond 90.5. Tool-use / computer-use / long-horizon browsing
154
+ dominate the card.
155
+ - **GPT-5.5** (OpenAI announcement; page is 403 to direct fetch,
156
+ corroborated via search). Released ~Apr 23 2026. Headline numbers:
157
+ **SWE-bench Verified 88.7**, SWE-bench Pro 58.6, **Terminal-Bench
158
+ 2.0 82.7 (SOTA)**, **τ²-bench Telecom 98.0**, **ARC-AGI-2 85.0
159
+ (+11.7 over prior, the launch's single largest delta)**, GDPval
160
+ 84.9. The two most-emphasized capabilities: agentic
161
+ coding/tool-orchestration and ARC-AGI-2 fluid reasoning.
162
+ - **Artificial Analysis** leaderboard. The page no longer exposes
163
+ components inline, but the current **Intelligence Index v4.0** =
164
+ 10 evals: **GDPval-AA, τ²-Bench Telecom, Terminal-Bench Hard,
165
+ SciCode, AA-LCR (long-context reasoning), AA-Omniscience, IFBench
166
+ (instruction-following), Humanity's Last Exam, GPQA Diamond,
167
+ CritPt**. Note the heavy weighting toward *agentic tool-use*
168
+ (τ²-bench), *long-horizon terminal agency* (Terminal-Bench Hard),
169
+ and *instruction-following fidelity* (IFBench). Pass@1, ±<1% CI.
170
+
171
+ ### 2.2 Broadly-used benchmarks, the capability each isolates, and the
172
+ real-world proxy
173
+
174
+ | Benchmark | Isolates | Real-world proxy | How models differentiate (2026) |
175
+ |---|---|---|---|
176
+ | **SWE-bench Verified / Pro** | repo-level multi-file code editing to a passing test | autonomous coding agents | Top cluster ~80–89% Verified (GPT-5.5 88.7); Pro still <60% — the hard frontier |
177
+ | **Terminal-Bench 2.0** | long-horizon shell agency: plan→iterate→tool-coordinate, deterministic check | CLI/devops automation agents | Wide spread: GPT-5.5 82.7 vs Qwen3.6 51.5 — strong separator |
178
+ | **τ-bench / τ²-bench (Telecom)** | multi-turn **tool use under a strict policy/API**, dialog state | customer-service / API agents | Near-saturating at the top (GPT-5.5 98.0) but mid-tier collapses — fidelity cliff |
179
+ | **BFCL V4** | function-calling: parallel/multiple selection, **relevance (when NOT to call)**, multi-turn, agentic memory | tool/function-calling fidelity | AST-graded; single-turn solved, multi-turn/relevance still discriminating |
180
+ | **GAIA** | compound multi-step tool-use + reasoning across web/files | general assistant agents | Multi-hop chains expose planning/credit-assignment failures |
181
+ | **OSWorld** | full GUI computer control, 15- vs 50-step tasks | computer-use agents | Long-task tracks isolate horizon length specifically |
182
+ | **ARC-AGI-2** | fluid abstract pattern induction, anti-memorization | genuine generalization | Largest 2026 deltas; GPT-5.5 85.0 — the prestige reasoning metric |
183
+ | **GPQA / AIME / MMLU-Pro / HLE** | hard knowledge + math reasoning | expert QA | Largely saturating top-end except HLE — *not* a transfer target for game RL (lmgame-Bench) |
184
+ | **ERQA** | **embodied spatial / trajectory / state-estimation / multi-view / task reasoning** (400 MCQ VQA) | robotics perception+planning | CoT moves it only +4–6.5pts → *reasoning-shaping* finetune is what moves it; the validated OpenRA-Bench transfer target |
185
+ | **PlanBench / Blocksworld** | precondition-ordered sequential planning, cost-optimal, replanning | task-graph scheduling, robot task planning | Mystery-Blocksworld defeats memorization; *the* planning correlate |
186
+ | **lmgame-Bench (Sokoban/Tetris RL → transfer)** | game-RL → spatial/planning transfer | the OpenRA-Bench hypothesis itself | RL→Blocksworld +≥10, →WebShop +6; NOT →GSM8K/coding |
187
+ | **SMAC → SMACv2** | multi-agent micro; the anti-memorization cautionary tale | strategy-game RL | Open-loop policy beat SMAC ⇒ procedural variation mandatory (the suite follows this) |
188
+
189
+ Net 2026 picture: differentiation has moved off knowledge/math QA
190
+ (saturating) onto **agentic tool-use fidelity, long-horizon execution,
191
+ computer/embodied control, and fluid reasoning (ARC-AGI-2)**.
192
+
193
+ ---
194
+
195
+ ## STEP 3 — Capability ↔ Coverage ↔ Parallel Benchmark Gap Table
196
+
197
+ | Capability / real-world use case | OpenRA-Bench coverage | Parallel benchmark | Verdict |
198
+ |---|---|---|---|
199
+ | Spatial state-estimation / perception under partial obs | `perception-target-vs-fog`, `perception-frontier-reading`, C2, C7, C8 | **ERQA** (spatial/state-est), SpatialVLM, VSR | **Well-covered** — and this is the empirically-validated transfer target |
200
+ | Frontier exploration: which unknown region to commit | `reasoning-frontier-commit`, C1 | Active Neural SLAM, frontier exploration, ERQA-trajectory | **Well-covered** |
201
+ | Constrained sequential planning (precondition graph + deadline) | C3, C4, `building-and-planning` | **PlanBench / Blocksworld**, ALFWorld | **Adequate** (parallels exist; single-map limits variety) |
202
+ | Resource allocation / multi-objective under a budget | `economy-investment`, `economy-time-box`, C5, C6 | PlanBench cost-optimal, SmartPlay | **Adequate** (spend-only) — harvest variants **non-discriminating** (engine S0/S1) |
203
+ | Risk assessment / replanning under partial info | `reasoning-risk-route`, `strategy-dilemma`, C9, C12 | PlanBench replanning, StarCraft-II-Arena | **Adequate** |
204
+ | Long-horizon multi-step credit assignment | thin: most clocks are short single-phase; C12 only gates with `after_ticks` | **Terminal-Bench 2.0**, OSWorld 50-step, GAIA | **Weak** — no genuine opening→tech→assault chain |
205
+ | Multi-unit / multi-agent coordination | `action-multiunit-coordination`, `strategy-twobody`, C10 | Watch-And-Help, SMAC(v2) | **Adequate** |
206
+ | Tool-use / function-calling **fidelity under a strict action API** | only as a *cross-cutting score* (`actions_warned/issued`); no scenario isolates it as the objective | **BFCL V4**, **τ²-bench**, IFBench | **Weak/structural gap** — yet this is the most leaderboard-weighted 2026 axis |
207
+ | Instruction-following under strict constraints | implicit in win predicates; not isolated | **IFBench** (in AA Index v4) | **Weak** |
208
+ | Adversarial / game-theoretic reasoning (opponent modeling, deception, counter-strategy) | **none** — no live adversary; `rush-hour` enemy is scripted; 1v1/Elo is Phase-1 unbuilt | StarCraft-II-Arena, game-theory evals; *the unique RTS value prop* | **Missing** — largest strategic gap |
209
+ | Tempo / timing-window discipline | C11 | TextStarCraft II, SmartPlay | **Adequate** (but only 1 category, 15 levels) |
210
+ | Fluid/abstract anti-memorization generalization | enforced by procedural variation + held-out seeds + generalization-gap metric | **ARC-AGI-2** philosophy | **Methodologically well-covered** (design discipline is correct) |
211
+
212
+ ### Scenarios with weak / no transfer story (flag)
213
+
214
+ - **`rush-hour` / `strategy-gauntlet` "sweep-and-clear"** — primarily a
215
+ search-and-destroy execution task. Defensible as multi-robot
216
+ patrol-and-clear, but with a *scripted* enemy it tests execution,
217
+ not adversarial reasoning. Keep, but it should not be load-bearing
218
+ for the "strategy" claim.
219
+ - **`economy-harvest-timebox` / `economy-harvest-investment`** —
220
+ currently economically identical to the non-harvest economy packs
221
+ (income=0). Honest, but they are *placeholders*, not distinct tests;
222
+ do not count them as economy coverage until S0/S1 land.
223
+ - **`cat-c5/c6/c7/c8` over-generation** — many variants differ only in
224
+ region coordinates / target counts. The *decision* is the same; this
225
+ is level inflation, not capability breadth (mild
226
+ SMAC-style memorization risk if seeds aren't truly held out per
227
+ variant). Not "game trivia," but low marginal information.
228
+ - The suite has **no** scenario that is pure game-trivia with zero
229
+ transfer story — the `real_world_meaning`/`robotics_analogue`
230
+ discipline is doing its job. The problem is *skew and redundancy*,
231
+ not arbitrariness.
232
+
233
+ ---
234
+
235
+ ## STEP 4 — Concrete, Prioritized Edit Advice
236
+
237
+ ### P0 — Add an adversarial / opponent-modeling family (highest leverage)
238
+
239
+ An RTS engine's unique, defensible value vs every other benchmark is
240
+ **live adversarial / game-theoretic reasoning** — and the suite
241
+ currently has *zero*. Build the Phase-1 second RL-controlled slot
242
+ (`EVAL_STACK_PLAN.md` Phase 1) and add:
243
+
244
+ - **Counter-Strategy Read**: opponent commits an observable opening
245
+ (rush vs tech vs expand); agent must read it from partial recon and
246
+ pick the dominant counter. Capability: adversarial reasoning +
247
+ perception. Real-world: competitive multi-agent / negotiation /
248
+ red-teaming. Parallel: **StarCraft-II-Arena (ICLR'25)**,
249
+ game-theoretic LLM evals.
250
+ - **Deception / feint handling**: opponent shows a decoy force; win
251
+ predicate rewards committing against the *real* axis
252
+ (`building_in_region`/`units_killed_gte` keyed off the true threat).
253
+ Parallel: adversarial robustness, opponent modeling.
254
+
255
+ This also makes the **Elo leaderboard** genuinely meaningful
256
+ (head-to-head), which it currently is not.
257
+
258
+ ### P0 — Add a strict-action-API instruction-following family
259
+
260
+ The most leaderboard-weighted 2026 axis (τ²-bench in AA Index v4,
261
+ BFCL V4, IFBench) is only measured as a *side diagnostic* here. Add a
262
+ family whose **primary objective is action-API fidelity**:
263
+
264
+ - Tasks where the win predicate is only reachable via a *specific
265
+ command sequence/format under explicit policy constraints* (e.g.
266
+ "achieve X but never issue `attack` before tick T", "use only
267
+ `move_units` + `deploy`, no `build`"), scoring relevance (issuing a
268
+ disallowed call = fail, à la BFCL "relevance detection").
269
+ - Capability: instruction-following + tool-call fidelity under a
270
+ strict API. Real-world: agentic API/tool orchestration, policy
271
+ compliance. Parallel: **BFCL V4 (relevance/multi-turn), τ²-bench,
272
+ IFBench**.
273
+
274
+ ### P1 — Add 2–3 genuinely long-horizon, multi-phase scenarios
275
+
276
+ Current clocks are mostly short single-phase. Add scenarios spanning
277
+ opening→scout→economy→tech→army→engagement in **one episode (40k+
278
+ ticks)** with a terminal objective, so the score depends on **credit
279
+ assignment across phases** (an early scouting error must surface as a
280
+ late failure). Capability: long-horizon credit assignment. Parallel:
281
+ **Terminal-Bench 2.0, OSWorld 50-step track, GAIA** multi-hop. This is
282
+ where 2026 models separate most and the suite is currently thin.
283
+
284
+ ### P1 — Cut / merge the redundant `cat-*` over-generation
285
+
286
+ Reduce each generated category to **≤3 levels per genuinely distinct
287
+ decision** instead of 5–6 near-duplicate coordinate variants. Net
288
+ ~200→~90 levels but *higher distinct-capability density* and lower
289
+ memorization risk. Specifically merge: C5↔C6 (both "budget→units+
290
+ buildings" ECON), C7↔C8 (both "place building in inferred region"
291
+ PERC). Keep the procedural seed variation *within* a level (the
292
+ anti-memorization mechanism) — cut the redundant *level* multiplication.
293
+
294
+ ### P1 — Quarantine non-discriminating economy packs
295
+
296
+ Tag `economy-harvest-*` (and any harvest variant) as **engine-prereq /
297
+ not-scored** until S0 (ore source) + S1 (silo storage) land
298
+ (`SCENARIO_BRAINSTORM.md`). Do not count them in economy coverage
299
+ claims. Land S0 (a one-line `VALID_ACTOR_TYPES` add or a
300
+ `resource_fields` scenario field) — it is cheap and unblocks a real,
301
+ distinct economy-throughput capability.
302
+
303
+ ### Over/under-representation summary
304
+
305
+ - **Over-represented:** ECON spend-allocation (C5/C6 + 5 hand-authored
306
+ economy packs, several non-discriminating), and generated PERC
307
+ region-placement (C7/C8). Level count >> capability count.
308
+ - **Under-represented:** adversarial/game-theoretic (zero),
309
+ tool-use/API-fidelity isolation (zero dedicated), long-horizon
310
+ credit assignment (thin), tempo (1 category).
311
+ - **Right-sized:** spatial perception/frontier (the validated transfer
312
+ target — keep strong), coordination, risk/replan.
313
+
314
+ ### Strengthening the generalization-transfer argument
315
+
316
+ The headline claim ("rush-hour finetune lifted ERQA") needs a
317
+ pre-registered external transfer panel, scored as **per-axis deltas,
318
+ not aggregate** (lmgame-Bench protocol):
319
+
320
+ 1. **ERQA** — the *primary* correlate. It is reasoning-sensitive
321
+ (CoT-only moves it just +4–6.5pts), embodied/spatial, and is the
322
+ axis the observed transfer hit. Report ERQA spatial / trajectory /
323
+ task subscores separately. Strongest evidence for the claim.
324
+ 2. **Blocksworld / PlanBench (incl. Mystery-Blocksworld)** —
325
+ validates the PLAN/TECH/ECON families; this is the exact transfer
326
+ lmgame-Bench demonstrated (RL→Blocksworld +≥10pts) and defeats
327
+ memorization.
328
+ 3. **BFCL V4 / τ²-bench** — validates the *new* P0 action-fidelity
329
+ family and tests whether strict-action-API discipline transfers
330
+ (the most commercially relevant axis; currently untested here).
331
+ 4. **Negative controls: GSM8K + a coding eval (e.g. LiveCodeBench)** —
332
+ lmgame-Bench showed game-RL does **not** transfer to math/coding;
333
+ include these to demonstrate the transfer is *specific* (spatial/
334
+ planning), not a generic capability bump. This negative result is
335
+ what makes the positive claim credible.
336
+ 5. Continue reporting the **generalization gap on held-out seeds**
337
+ (Procgen/SMACv2/ARC-AGI discipline) — already designed in; keep it
338
+ front-and-center as the anti-memorization guarantee.
339
+
340
+ ---
341
+
342
+ ## STEP 5 — Sources (verified via search/fetch, 2026-05)
343
+
344
+ - Qwen3.6-35B-A3B model card — https://huggingface.co/Qwen/Qwen3.6-35B-A3B
345
+ - Kimi-K2.6 model card — https://huggingface.co/moonshotai/Kimi-K2.6
346
+ - Introducing GPT-5.5 — https://openai.com/index/introducing-gpt-5-5/ (direct fetch 403; corroborated via search: interestingengineering.com/ai-robotics/opanai-gpt-5-5-agentic-coding-gains, kingy.ai GPT-5.5 benchmarks, llm-stats.com/models/gpt-5.5)
347
+ - Artificial Analysis leaderboard — https://artificialanalysis.ai/leaderboards/models ; methodology — https://artificialanalysis.ai/methodology/intelligence-benchmarking ; Intelligence Index — https://artificialanalysis.ai/evaluations/artificial-analysis-intelligence-index
348
+ - BFCL V4 — https://gorilla.cs.berkeley.edu/leaderboard.html ; paper https://openreview.net/forum?id=2GmDdhBdDk
349
+ - τ²-bench (Sierra) via Agentic AI Benchmarks — https://awesomeagents.ai/leaderboards/agentic-ai-benchmarks-leaderboard/
350
+ - ERQA — https://github.com/embodiedreasoning/ERQA ; Gemini Robotics arXiv:2503.20020 https://arxiv.org/html/2503.20020v1
351
+ - lmgame-Bench arXiv:2505.15146 — https://arxiv.org/abs/2505.15146
352
+ - GAIA / OSWorld / Terminal-Bench / ARC-AGI-2 overview — https://www.marktechpost.com/2026/04/26/top-7-benchmarks-that-actually-matter-for-agentic-reasoning-in-large-language-models/ ; https://www.spheron.network/blog/ai-agent-benchmarking-gpu-cloud-swebench-gaia/
353
+ - In-repo: `SCENARIO_CATALOG.md`, `SCENARIO_BRAINSTORM.md`, `EVAL_STACK_PLAN.md`, `openra_bench/scenarios/win_conditions.py`, `openra_bench/scoring.py`, `openra_bench/goal_tracker.py`, `openra_bench/scenarios/packs/*.yaml`
354
+ - Supporting literature cited in-repo and corroborated: SMAC→SMACv2 (arXiv:2212.07489), Procgen (arXiv:1912.01588), ARC-AGI (arXiv:1911.01547), PlanBench (arXiv:2206.10498), Active Neural SLAM (arXiv:2004.05155)
app.py CHANGED
@@ -172,8 +172,9 @@ def load_capability_leaderboard() -> pd.DataFrame:
172
  rows = []
173
  cols = [
174
  "rank", "model", "episodes", "win_rate", "composite",
175
- "objective", "perception", "reasoning", "action", "weakest_link",
176
- "reward_vector", "held_out_composite", "generalization_gap",
 
177
  ]
178
  if not rows:
179
  return pd.DataFrame(columns=cols)
 
172
  rows = []
173
  cols = [
174
  "rank", "model", "episodes", "win_rate", "composite",
175
+ "objective", "adversarial_rating", "perception", "reasoning",
176
+ "action", "weakest_link", "reward_vector",
177
+ "held_out_composite", "generalization_gap",
178
  ]
179
  if not rows:
180
  return pd.DataFrame(columns=cols)
openra_bench/adversarial.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Adversarial ladder rating (the 1v1 spotlight metric).
2
+
3
+ Each `adversarial-*` pack is a 3-rung ladder (easy → medium → hard) of
4
+ increasing reactive-opponent strength. A model's **ladder rating** on a
5
+ pack is the number of rungs cleared *contiguously from the bottom* — a
6
+ monotone difficulty signal that complements the Elo (which ranks models
7
+ head-to-head on shared rungs via `pairwise.pairwise_elo`).
8
+
9
+ Pure + deterministic; the live opponent is the engine's reactive force
10
+ today, with a documented swap-in to model-vs-model once the engine
11
+ exposes an enemy command channel (see pairwise.py / task #3).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ RUNGS: tuple[str, ...] = ("easy", "medium", "hard")
17
+
18
+
19
+ def ladder_rating(outcomes: dict[str, str]) -> int:
20
+ """Rungs cleared contiguously from easy. A rung is cleared iff its
21
+ outcome == "win". easy lost → 0; easy+medium won, hard lost → 2."""
22
+ n = 0
23
+ for r in RUNGS:
24
+ if outcomes.get(r) == "win":
25
+ n += 1
26
+ else:
27
+ break
28
+ return n
29
+
30
+
31
+ def is_adversarial_episode(ep: dict) -> bool:
32
+ return ep.get("capability") == "adversarial"
33
+
34
+
35
+ def ladder_ratings(stats: dict) -> dict[str, int]:
36
+ """Per adversarial pack → ladder rating, from a run_eval stats dict.
37
+
38
+ `cell` is "<pack>:<level>"; only the public split counts (held-out
39
+ seeds are anti-memorization, not ladder progression). When a rung
40
+ ran multiple seeds it is cleared only if it was won on *every*
41
+ seed (no lucky-seed promotion)."""
42
+ rungs: dict[str, dict[str, list[str]]] = {}
43
+ for e in stats.get("episodes", []):
44
+ if not is_adversarial_episode(e) or e.get("split", "public") != "public":
45
+ continue
46
+ pack, _, level = str(e.get("cell", "")).rpartition(":")
47
+ if not pack or level not in RUNGS:
48
+ continue
49
+ rungs.setdefault(pack, {}).setdefault(level, []).append(
50
+ e.get("outcome", "?")
51
+ )
52
+ out: dict[str, int] = {}
53
+ for pack, by_level in rungs.items():
54
+ collapsed = {
55
+ lv: ("win" if outs and all(o == "win" for o in outs) else "loss")
56
+ for lv, outs in by_level.items()
57
+ }
58
+ out[pack] = ladder_rating(collapsed)
59
+ return out
60
+
61
+
62
+ def adversarial_summary(stats: dict) -> dict:
63
+ """Spotlight roll-up: per-pack ratings + the headline mean rating
64
+ (0–3) across adversarial packs played."""
65
+ ratings = ladder_ratings(stats)
66
+ mean = round(sum(ratings.values()) / len(ratings), 4) if ratings else 0.0
67
+ return {
68
+ "ladder_ratings": ratings,
69
+ "mean_ladder_rating": mean,
70
+ "packs": sorted(ratings),
71
+ "max_rung": len(RUNGS),
72
+ }
openra_bench/leaderboard.py CHANGED
@@ -18,7 +18,7 @@ from collections import Counter
18
  from pathlib import Path
19
  from typing import Any
20
 
21
- CAPABILITIES = ("perception", "reasoning", "action")
22
  DEFAULT_STORE = Path(__file__).parent.parent / "data" / "leaderboard.jsonl"
23
  # A run must cover at least this many episodes to be rankable (mirrors
24
  # the existing app.py min-games gate; keeps one-off noise off the board).
@@ -69,6 +69,13 @@ def ingest_run(
69
  # reward-vector signature, comparable across models/runs.
70
  "objective": overall.get("objective_mean", 0.0),
71
  "reward_vector": stats.get("reward_vector_mean", {}),
 
 
 
 
 
 
 
72
  "weakest_link_hist": overall.get("weakest_link_hist", {}),
73
  # Anti-memorization: held-out composite + generalization gap
74
  # (public − held-out). None when the run had no held-out split.
 
18
  from pathlib import Path
19
  from typing import Any
20
 
21
+ CAPABILITIES = ("perception", "reasoning", "action", "adversarial")
22
  DEFAULT_STORE = Path(__file__).parent.parent / "data" / "leaderboard.jsonl"
23
  # A run must cover at least this many episodes to be rankable (mirrors
24
  # the existing app.py min-games gate; keeps one-off noise off the board).
 
69
  # reward-vector signature, comparable across models/runs.
70
  "objective": overall.get("objective_mean", 0.0),
71
  "reward_vector": stats.get("reward_vector_mean", {}),
72
+ # Adversarial 1v1 spotlight: mean ladder rating (0–3) + per-pack.
73
+ "adversarial_rating": stats.get("adversarial", {}).get(
74
+ "mean_ladder_rating", 0.0
75
+ ),
76
+ "adversarial_ladders": stats.get("adversarial", {}).get(
77
+ "ladder_ratings", {}
78
+ ),
79
  "weakest_link_hist": overall.get("weakest_link_hist", {}),
80
  # Anti-memorization: held-out composite + generalization gap
81
  # (public − held-out). None when the run had no held-out split.
openra_bench/run_eval.py CHANGED
@@ -202,6 +202,12 @@ def evaluate(
202
  "episodes": episodes,
203
  "skipped": skipped,
204
  }
 
 
 
 
 
 
205
  if held_scores:
206
  ho = _agg(held_scores)
207
  out["overall_held_out"] = ho
 
202
  "episodes": episodes,
203
  "skipped": skipped,
204
  }
205
+ # Adversarial spotlight: per-pack ladder ratings + headline mean.
206
+ from .adversarial import adversarial_summary
207
+
208
+ adv = adversarial_summary(out)
209
+ if adv["packs"]:
210
+ out["adversarial"] = adv
211
  if held_scores:
212
  ho = _agg(held_scores)
213
  out["overall_held_out"] = ho
openra_bench/scenarios/packs/adversarial-duel.yaml ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ meta:
2
+ id: adversarial-duel
3
+ title: 'Adversarial: Force Duel'
4
+ capability: adversarial
5
+ real_world_meaning: "Defeat a reactive opposing force of escalating strength:\
6
+ \ focus-fire, target priority and kiting against an enemy that fights\
7
+ \ back — head-to-head tactical reasoning, not micro against a dummy."
8
+ robotics_analogue: "Adversarial multi-agent engagement: prevail over a\
9
+ \ reactive opponent team whose strength increases each ladder rung."
10
+ author: openra-bench
11
+ base_map: rush-hour-arena
12
+ # The ladder: easy/medium/hard are rungs of increasing enemy strength.
13
+ # A model's adversarial rating = the highest rung it can clear (see
14
+ # adversarial.ladder_rating); models are Elo-ranked on shared rungs.
15
+ base:
16
+ agent:
17
+ faction: allies
18
+ cash: 0
19
+ enemy:
20
+ faction: soviet
21
+ cash: 0
22
+ bot_type: ''
23
+ actors:
24
+ - type: 2tnk
25
+ owner: agent
26
+ position: [6, 8]
27
+ stance: 3
28
+ health: 100
29
+ facing: -1
30
+ count: 2
31
+ randomize:
32
+ position:
33
+ base: [6, 8]
34
+ offset: 2
35
+ - type: 1tnk
36
+ owner: agent
37
+ position: [6, 11]
38
+ stance: 3
39
+ health: 100
40
+ facing: -1
41
+ count: 2
42
+ - type: e1
43
+ owner: enemy
44
+ position: [34, 22]
45
+ stance: 3
46
+ health: 100
47
+ facing: -1
48
+ count: 3
49
+ levels:
50
+ easy:
51
+ description: 'Rung 1 — even-ish duel: 4 agent vehicles vs 3 enemy rifle.'
52
+ win_condition:
53
+ all_of:
54
+ - units_killed_gte: 3
55
+ - within_ticks: 14000
56
+ fail_condition:
57
+ not:
58
+ own_units_gte: 1
59
+ max_turns: 60
60
+ medium:
61
+ description: 'Rung 2 — outnumbered: same force vs a reactive armour mix.'
62
+ overrides:
63
+ actors:
64
+ - type: 2tnk
65
+ owner: agent
66
+ position: [6, 8]
67
+ stance: 3
68
+ health: 100
69
+ facing: -1
70
+ count: 2
71
+ randomize:
72
+ position:
73
+ base: [6, 8]
74
+ offset: 2
75
+ - type: 1tnk
76
+ owner: agent
77
+ position: [6, 11]
78
+ stance: 3
79
+ health: 100
80
+ facing: -1
81
+ count: 2
82
+ - type: e1
83
+ owner: enemy
84
+ position: [34, 22]
85
+ stance: 3
86
+ health: 100
87
+ facing: -1
88
+ count: 4
89
+ - type: 1tnk
90
+ owner: enemy
91
+ position: [38, 26]
92
+ stance: 3
93
+ health: 100
94
+ facing: -1
95
+ count: 2
96
+ win_condition:
97
+ all_of:
98
+ - units_killed_gte: 6
99
+ - within_ticks: 12000
100
+ fail_condition:
101
+ not:
102
+ own_units_gte: 1
103
+ max_turns: 70
104
+ hard:
105
+ description: 'Rung 3 — badly outnumbered + a loss cap: win by maneuver.'
106
+ overrides:
107
+ actors:
108
+ - type: 2tnk
109
+ owner: agent
110
+ position: [6, 8]
111
+ stance: 3
112
+ health: 100
113
+ facing: -1
114
+ count: 2
115
+ randomize:
116
+ position:
117
+ base: [6, 8]
118
+ offset: 2
119
+ - type: 1tnk
120
+ owner: agent
121
+ position: [6, 11]
122
+ stance: 3
123
+ health: 100
124
+ facing: -1
125
+ count: 2
126
+ - type: e1
127
+ owner: enemy
128
+ position: [34, 22]
129
+ stance: 3
130
+ health: 100
131
+ facing: -1
132
+ count: 5
133
+ - type: 2tnk
134
+ owner: enemy
135
+ position: [40, 28]
136
+ stance: 3
137
+ health: 100
138
+ facing: -1
139
+ count: 3
140
+ win_condition:
141
+ all_of:
142
+ - units_killed_gte: 8
143
+ - within_ticks: 10000
144
+ - units_lost_lte: 3
145
+ fail_condition:
146
+ not:
147
+ own_units_gte: 1
148
+ max_turns: 80
openra_bench/scenarios/packs/adversarial-siege.yaml ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ meta:
2
+ id: adversarial-siege
3
+ title: 'Adversarial: Break the Position'
4
+ capability: adversarial
5
+ real_world_meaning: "Dislodge a reactive force holding a prepared\
6
+ \ position: commit, absorb the defended ground, and break it before\
7
+ \ the clock — assaulting an opponent with the terrain advantage."
8
+ robotics_analogue: "Adversarial objective seizure: overcome a reactive\
9
+ \ defending team entrenched at the goal under a time budget."
10
+ author: openra-bench
11
+ base_map: rush-hour-arena
12
+ base:
13
+ agent:
14
+ faction: allies
15
+ cash: 0
16
+ enemy:
17
+ faction: soviet
18
+ cash: 0
19
+ bot_type: ''
20
+ actors:
21
+ - type: 2tnk
22
+ owner: agent
23
+ position: [6, 10]
24
+ stance: 3
25
+ health: 100
26
+ facing: -1
27
+ count: 4
28
+ - type: e1
29
+ owner: enemy
30
+ position: [40, 26]
31
+ stance: 3
32
+ health: 100
33
+ facing: -1
34
+ count: 4
35
+ levels:
36
+ easy:
37
+ description: 'Rung 1 — 4 tanks break a 4-rifle position.'
38
+ win_condition:
39
+ all_of:
40
+ - units_killed_gte: 4
41
+ - within_ticks: 13000
42
+ fail_condition:
43
+ not:
44
+ own_units_gte: 1
45
+ max_turns: 60
46
+ medium:
47
+ description: 'Rung 2 — defenders reinforced with armour.'
48
+ overrides:
49
+ actors:
50
+ - type: 2tnk
51
+ owner: agent
52
+ position: [6, 10]
53
+ stance: 3
54
+ health: 100
55
+ facing: -1
56
+ count: 4
57
+ - type: e1
58
+ owner: enemy
59
+ position: [40, 26]
60
+ stance: 3
61
+ health: 100
62
+ facing: -1
63
+ count: 5
64
+ - type: 2tnk
65
+ owner: enemy
66
+ position: [43, 29]
67
+ stance: 3
68
+ health: 100
69
+ facing: -1
70
+ count: 2
71
+ win_condition:
72
+ all_of:
73
+ - units_killed_gte: 7
74
+ - within_ticks: 11000
75
+ fail_condition:
76
+ not:
77
+ own_units_gte: 1
78
+ max_turns: 75
79
+ hard:
80
+ description: 'Rung 3 — dug-in mixed force, tight clock + loss cap.'
81
+ overrides:
82
+ actors:
83
+ - type: 2tnk
84
+ owner: agent
85
+ position: [6, 10]
86
+ stance: 3
87
+ health: 100
88
+ facing: -1
89
+ count: 5
90
+ - type: e1
91
+ owner: enemy
92
+ position: [40, 26]
93
+ stance: 3
94
+ health: 100
95
+ facing: -1
96
+ count: 6
97
+ - type: 2tnk
98
+ owner: enemy
99
+ position: [43, 29]
100
+ stance: 3
101
+ health: 100
102
+ facing: -1
103
+ count: 4
104
+ win_condition:
105
+ all_of:
106
+ - units_killed_gte: 10
107
+ - within_ticks: 9000
108
+ - units_lost_lte: 3
109
+ fail_condition:
110
+ not:
111
+ own_units_gte: 1
112
+ max_turns: 85
openra_bench/scenarios/packs/adversarial-skirmish.yaml ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ meta:
2
+ id: adversarial-skirmish
3
+ title: 'Adversarial: Outnumbered Skirmish'
4
+ capability: adversarial
5
+ real_world_meaning: "Win while outnumbered by trading space for time:\
6
+ \ pick fights, retreat, and re-engage a reactive enemy on favourable\
7
+ \ terms instead of a head-on brawl — asymmetric tactical reasoning."
8
+ robotics_analogue: "Asymmetric adversarial control: a smaller agent team\
9
+ \ must defeat a larger reactive force by engagement selection."
10
+ author: openra-bench
11
+ base_map: rush-hour-arena
12
+ base:
13
+ agent:
14
+ faction: allies
15
+ cash: 0
16
+ enemy:
17
+ faction: soviet
18
+ cash: 0
19
+ bot_type: ''
20
+ actors:
21
+ - type: 2tnk
22
+ owner: agent
23
+ position: [6, 9]
24
+ stance: 3
25
+ health: 100
26
+ facing: -1
27
+ count: 2
28
+ - type: e1
29
+ owner: enemy
30
+ position: [30, 20]
31
+ stance: 3
32
+ health: 100
33
+ facing: -1
34
+ count: 4
35
+ levels:
36
+ easy:
37
+ description: 'Rung 1 — 2 tanks vs 4 rifle: doable if you fight smart.'
38
+ win_condition:
39
+ all_of:
40
+ - units_killed_gte: 4
41
+ - within_ticks: 14000
42
+ fail_condition:
43
+ not:
44
+ own_units_gte: 1
45
+ max_turns: 60
46
+ medium:
47
+ description: 'Rung 2 — 2 tanks vs 6 mixed: must split the enemy.'
48
+ overrides:
49
+ actors:
50
+ - type: 2tnk
51
+ owner: agent
52
+ position: [6, 9]
53
+ stance: 3
54
+ health: 100
55
+ facing: -1
56
+ count: 2
57
+ - type: e1
58
+ owner: enemy
59
+ position: [30, 20]
60
+ stance: 3
61
+ health: 100
62
+ facing: -1
63
+ count: 4
64
+ - type: 1tnk
65
+ owner: enemy
66
+ position: [35, 24]
67
+ stance: 3
68
+ health: 100
69
+ facing: -1
70
+ count: 2
71
+ win_condition:
72
+ all_of:
73
+ - units_killed_gte: 6
74
+ - within_ticks: 12000
75
+ fail_condition:
76
+ not:
77
+ own_units_gte: 1
78
+ max_turns: 70
79
+ hard:
80
+ description: 'Rung 3 — 3 tanks vs 8, loss-capped: maneuver or lose.'
81
+ overrides:
82
+ actors:
83
+ - type: 2tnk
84
+ owner: agent
85
+ position: [6, 9]
86
+ stance: 3
87
+ health: 100
88
+ facing: -1
89
+ count: 3
90
+ - type: e1
91
+ owner: enemy
92
+ position: [30, 20]
93
+ stance: 3
94
+ health: 100
95
+ facing: -1
96
+ count: 5
97
+ - type: 1tnk
98
+ owner: enemy
99
+ position: [35, 24]
100
+ stance: 3
101
+ health: 100
102
+ facing: -1
103
+ count: 3
104
+ win_condition:
105
+ all_of:
106
+ - units_killed_gte: 8
107
+ - within_ticks: 10000
108
+ - units_lost_lte: 2
109
+ fail_condition:
110
+ not:
111
+ own_units_gte: 1
112
+ max_turns: 80
openra_bench/scenarios/schema.py CHANGED
@@ -26,7 +26,9 @@ from pydantic import BaseModel, Field, field_validator
26
  from .win_conditions import WinCondition
27
 
28
  LevelName = Literal["easy", "medium", "hard"]
29
- Capability = Literal["perception", "reasoning", "action"]
 
 
30
 
31
 
32
  def deep_merge(base: dict, patch: dict) -> dict:
 
26
  from .win_conditions import WinCondition
27
 
28
  LevelName = Literal["easy", "medium", "hard"]
29
+ # "adversarial" = head-to-head reasoning vs a reactive opponent (the
30
+ # axis an RTS engine uniquely owns); ranked by a difficulty ladder + Elo.
31
+ Capability = Literal["perception", "reasoning", "action", "adversarial"]
32
 
33
 
34
  def deep_merge(base: dict, patch: dict) -> dict:
tests/test_adversarial.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Adversarial 1v1 spotlight: ladder rating + family integ.
2
+
3
+ The ladder metric is pure logic (fast, exhaustive); the 3 packs are
4
+ also compiled and smoke-run on the live Rust engine to prove the new
5
+ `adversarial` capability flows end to end (schema → engine → score →
6
+ leaderboard breakdown).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+
15
+ from openra_bench.adversarial import (
16
+ RUNGS,
17
+ adversarial_summary,
18
+ ladder_rating,
19
+ ladder_ratings,
20
+ )
21
+ from openra_bench.leaderboard import _capability_breakdown, ingest_run
22
+
23
+ PACKS = Path(__file__).parent.parent / "openra_bench" / "scenarios" / "packs"
24
+ ADV = ["adversarial-duel", "adversarial-skirmish", "adversarial-siege"]
25
+
26
+
27
+ def test_ladder_rating_is_contiguous_from_easy():
28
+ assert ladder_rating({}) == 0
29
+ assert ladder_rating({"easy": "loss"}) == 0
30
+ assert ladder_rating({"easy": "win"}) == 1
31
+ assert ladder_rating({"easy": "win", "medium": "win"}) == 2
32
+ assert ladder_rating(dict.fromkeys(RUNGS, "win")) == 3
33
+ # non-contiguous: hard won but medium lost → still 1
34
+ assert ladder_rating(
35
+ {"easy": "win", "medium": "loss", "hard": "win"}
36
+ ) == 1
37
+ # draw does not clear a rung
38
+ assert ladder_rating({"easy": "draw"}) == 0
39
+
40
+
41
+ def test_ladder_ratings_need_all_seeds_won():
42
+ stats = {
43
+ "episodes": [
44
+ {"cell": "adversarial-duel:easy", "capability": "adversarial",
45
+ "split": "public", "outcome": "win"},
46
+ {"cell": "adversarial-duel:easy", "capability": "adversarial",
47
+ "split": "public", "outcome": "loss"}, # one seed lost
48
+ {"cell": "adversarial-duel:medium", "capability": "adversarial",
49
+ "split": "public", "outcome": "win"},
50
+ # non-adversarial + held-out ignored
51
+ {"cell": "rush-hour:easy", "capability": "action",
52
+ "split": "public", "outcome": "win"},
53
+ {"cell": "adversarial-duel:hard", "capability": "adversarial",
54
+ "split": "held_out", "outcome": "win"},
55
+ ]
56
+ }
57
+ # easy not all-won → rating 0 (medium can't count, non-contiguous)
58
+ assert ladder_ratings(stats) == {"adversarial-duel": 0}
59
+ s = adversarial_summary(stats)
60
+ assert s["packs"] == ["adversarial-duel"]
61
+ assert s["mean_ladder_rating"] == 0.0 and s["max_rung"] == 3
62
+
63
+
64
+ def test_summary_and_leaderboard_carry_adversarial(tmp_path):
65
+ stats = {
66
+ "episodes": [
67
+ {"cell": "adversarial-duel:easy", "capability": "adversarial",
68
+ "split": "public", "outcome": "win", "composite": 0.7},
69
+ {"cell": "adversarial-duel:medium", "capability": "adversarial",
70
+ "split": "public", "outcome": "win", "composite": 0.6},
71
+ {"cell": "adversarial-duel:hard", "capability": "adversarial",
72
+ "split": "public", "outcome": "loss", "composite": 0.2},
73
+ ],
74
+ "overall": {"n": 3, "win_rate": 0.66, "composite_mean": 0.5},
75
+ "adversarial": None,
76
+ }
77
+ stats["adversarial"] = adversarial_summary(stats)
78
+ assert stats["adversarial"]["ladder_ratings"] == {"adversarial-duel": 2}
79
+ rec = ingest_run(stats, "m1", store=tmp_path / "lb.jsonl")
80
+ assert rec["adversarial_rating"] == 2 / 1 # mean over 1 pack
81
+ assert rec["adversarial_ladders"] == {"adversarial-duel": 2}
82
+ cap = _capability_breakdown(stats["episodes"])
83
+ assert "adversarial" in cap and cap["adversarial"]["n"] == 3
84
+
85
+
86
+ @pytest.mark.parametrize("pid", ADV)
87
+ def test_adversarial_pack_compiles_and_runs(pid):
88
+ pytest.importorskip("openra_train")
89
+ from openra_bench.eval_core import run_level
90
+ from openra_bench.scenarios import load_pack
91
+ from openra_bench.scenarios.loader import compile_level
92
+
93
+ pack = load_pack(PACKS / f"{pid}.yaml")
94
+ for lvl in RUNGS:
95
+ c = compile_level(pack, lvl)
96
+ assert c.meta.capability == "adversarial"
97
+ assert c.map_supported, f"{pid}:{lvl} map must be Rust-loadable"
98
+ c = compile_level(pack, "easy")
99
+ res = run_level(c, lambda rs, C: [C.observe()], seed=1)
100
+ assert res.outcome in {"win", "draw", "loss"}
101
+ assert res.turns >= 1