Spaces:
Running
Paper-experiment prep: human-study harness + pass^k + paper plan
Browse filesA coordinated batch landing the infrastructure the paper experiments
need before the full 200-pack run.
PAPER_PLAN.md — the working paper plan: central thesis, the
"perception not planning" framing, contributions, paper structure,
the three findings with sharpened claims + experiments + reviewer
attacks/defenses, §5 strategy-diversity (1v1 trajectory-embedding
figure), §12 pre-full-run audits (quality / coverage / pass^k),
limitations, stretch ideas.
Human-baseline study (the apples-to-apple human reference):
- openra_bench/human_study.py — frozen stratified 24-pack subset
(one pack per scenario family, 8 easy / 8 medium / 8 hard),
3 conditions (vision-fog / vision-clear / handoff-bad), a per-
player counterbalanced 72-cell playlist, and an
open_study_session() constructor that configures each condition
(vision-clear -> reveal_map; handoff-bad -> HANDOFF_K=3 stall
prefix auto-played).
- openra_bench/human_labeling.py — InteractiveSession.from_pack
gains a fog_mode param so the human Play path can run the no-fog
condition.
- app.py — Play-tab "Human-study mode" accordion: player id,
Begin study / Next scenario, progress tracker; walks a recruited
player through all 72 cells. Every game auto-saves apples-to-
apple with model runs.
- tests/test_human_study.py — subset packs verified to exist; the
three conditions verified live (vision-clear reveals 100% +
every enemy; handoff-bad starts HANDOFF_K turns deep); the
study session persists in the standard Playback format.
Multi-run reliability (the missing variance metric):
- openra_bench/run_eval.py — --repeats N runs each (cell, seed) N
times varying only model nondeterminism, so the report can carry
mean +- CI and pass^k (all-k wins) alongside pass@k. Each record
carries a repeat index; only rep=0 writes a Playback (no per-turn
dump bloat). --temperature T threads through ProviderConfig so
the repeats are meaningful.
- tests/test_run_eval_repeats.py — tasks multiply by N; records
carry the repeat index.
Full suite 8098 passed, 1 skipped, 3 xfailed, 2 xpassed.
- PAPER_PLAN.md +336 -0
- app.py +106 -0
- openra_bench/human_labeling.py +6 -1
- openra_bench/human_study.py +120 -0
- openra_bench/run_eval.py +29 -5
- tests/test_human_study.py +78 -0
- tests/test_run_eval_repeats.py +35 -0
|
@@ -0,0 +1,336 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenRA-Bench — Paper Plan & Critique
|
| 2 |
+
|
| 3 |
+
Working document. Captures the central thesis, the paper framing, the
|
| 4 |
+
three findings, the experiment program, and the open critiques /
|
| 5 |
+
reviewer-defenses. Living doc — update as experiments land.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## 1. Central thesis (as stated)
|
| 10 |
+
|
| 11 |
+
We build a benchmark on the Real-Time Strategy game *Red Alert* and
|
| 12 |
+
measure LLM performance on **adversarial, multi-modal, long-horizon
|
| 13 |
+
strategic planning and execution**. Three findings:
|
| 14 |
+
|
| 15 |
+
1. **The major gap is image-based perception.** In pure-text
|
| 16 |
+
battlefields models execute complex strategies; in mixed modality
|
| 17 |
+
— where own and enemy positions must be read off a minimap — they
|
| 18 |
+
struggle. We show this by SFT on a small set of text-modality
|
| 19 |
+
results with the image attached, and observe improvement on this
|
| 20 |
+
benchmark *and* on external vision benchmarks (ERQA).
|
| 21 |
+
2. **Models panic in unfavorable / uncertain situations.** Scores
|
| 22 |
+
drop sharply under fog of war while humans hold a baseline. Under
|
| 23 |
+
heavy losses, models choose `observe`/`stop` instead of actively
|
| 24 |
+
redirecting units. Hypothesis: a transfer of behavior from
|
| 25 |
+
code-heavy training, where the rewarded move in an unfavorable
|
| 26 |
+
state is to stop and wait for human intervention.
|
| 27 |
+
3. **Bridging both gaps via simple SFT** lets a *smaller* model beat
|
| 28 |
+
large reasoning models on 1v1 ELO.
|
| 29 |
+
|
| 30 |
+
Contributions: 200 research-grounded scenarios; an active 1v1
|
| 31 |
+
battleground; evidence that models form complex, diverse strategies
|
| 32 |
+
despite the perception and adversity gaps (and that strategy varies
|
| 33 |
+
with temperature and is model-characteristic — turtle / balanced /
|
| 34 |
+
aggressive).
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
## 2. Framing & positioning
|
| 39 |
+
|
| 40 |
+
**The hook is the inversion:** LLMs can do the *hard* part
|
| 41 |
+
(long-horizon adversarial strategy) but fail the "easy" parts
|
| 42 |
+
(reading a map, holding their nerve). Lead with that — it inverts the
|
| 43 |
+
usual "LLMs can't plan" narrative, and the failures are specific and
|
| 44 |
+
*fixable*.
|
| 45 |
+
|
| 46 |
+
Position OpenRA-Bench as a **diagnostic instrument**, not a
|
| 47 |
+
leaderboard. The methodological novelty vs. the crowded
|
| 48 |
+
SC2 / TextStarCraft LLM-agent space is the **ablation methodology that
|
| 49 |
+
decomposes failure** into perception / reasoning / action / adversity.
|
| 50 |
+
Arc of the paper: **diagnosis → localization → treatment → transfer.**
|
| 51 |
+
|
| 52 |
+
Title candidates:
|
| 53 |
+
- *Map-Blind and Panic-Prone: Diagnosing LLM Strategic Agents in
|
| 54 |
+
Real-Time Strategy*
|
| 55 |
+
- *The Bottleneck Is Perception, Not Planning: A Diagnostic RTS
|
| 56 |
+
Benchmark*
|
| 57 |
+
- *LLMs Can Strategize But Cannot See: Decomposing Agent Failure in
|
| 58 |
+
Red Alert*
|
| 59 |
+
|
| 60 |
+
Venue: strong enough for a main track (ICLR/NeurIPS/ICML) on the
|
| 61 |
+
diagnosis+treatment+transfer arc — not only Datasets & Benchmarks.
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
## 3. Contributions (sharpened)
|
| 66 |
+
|
| 67 |
+
1. **OpenRA-Bench** — 200 controlled RTS scenarios on a deterministic
|
| 68 |
+
engine, each anchored to a named real-world capability / external
|
| 69 |
+
benchmark, built to a **no-cheat / no-defect** bar (every lazy /
|
| 70 |
+
brute / stall policy provably loses on every level and seed). The
|
| 71 |
+
validation rigor is itself a contribution: it is why the benchmark
|
| 72 |
+
measures the *intended* capability.
|
| 73 |
+
2. **An ablation methodology that decomposes failure** — the
|
| 74 |
+
perception grid (channel × fog) and the handoff sweep — turning
|
| 75 |
+
"the model lost" into "the model lost *because of perception /
|
| 76 |
+
adversity / planning*."
|
| 77 |
+
3. **A 1v1 self-play battleground** with ELO.
|
| 78 |
+
4. **Two localized, fixable gaps** — perception-binding and
|
| 79 |
+
inaction-under-adversity — each with an SFT remedy, cross-benchmark
|
| 80 |
+
transfer (ERQA), and a small-beats-large result.
|
| 81 |
+
|
| 82 |
+
---
|
| 83 |
+
|
| 84 |
+
## 4. Paper structure
|
| 85 |
+
|
| 86 |
+
§1 Intro (the inversion) · §2 Related work · §3 OpenRA-Bench (engine,
|
| 87 |
+
200 scenarios, no-cheat design, ablation axes, 1v1/ELO,
|
| 88 |
+
human-labeling) · §4 Setup (models, sweeps, metrics) · **§5 Models
|
| 89 |
+
*can* strategize** (the diversity control) · §6 Finding 1: perception
|
| 90 |
+
gap · §7 Finding 2: panic · §8 Finding 3: SFT remedy + transfer ·
|
| 91 |
+
§9 Limitations · §10 Conclusion.
|
| 92 |
+
|
| 93 |
+
**Key structural move:** promote the "diverse strategies" result from
|
| 94 |
+
a bonus to **§5, before the failure findings.** Showing models produce
|
| 95 |
+
coherent, diverse, model-characteristic strategies is what licenses
|
| 96 |
+
the claim that the later failures are *not* failures of strategic
|
| 97 |
+
reasoning. Without it a reviewer says "maybe they just can't plan."
|
| 98 |
+
|
| 99 |
+
---
|
| 100 |
+
|
| 101 |
+
## 5. §5 control — models *can* strategize
|
| 102 |
+
|
| 103 |
+
Promote from "bonus" to a load-bearing control.
|
| 104 |
+
|
| 105 |
+
- Classify each game's strategy (turtle / balanced / aggressive) via a
|
| 106 |
+
trajectory classifier (first-attack tick, army-vs-econ ratio,
|
| 107 |
+
expansion count, aggression index, build order). Show per-model
|
| 108 |
+
distributions differ.
|
| 109 |
+
- Temperature sweep: strategy entropy vs. temperature for one model.
|
| 110 |
+
- Show strategies are *coherent* (executed consistently once chosen),
|
| 111 |
+
not random.
|
| 112 |
+
|
| 113 |
+
**The headline figure — the strategy-embedding scatter, in 1v1.** Each
|
| 114 |
+
game's trajectory is featurized and embedded to 2D (PCA / UMAP);
|
| 115 |
+
points colored by model. The plot shows, at a glance:
|
| 116 |
+
- **inter-model clusters** — model A lives in the aggressive region,
|
| 117 |
+
model B turtles — models have characteristic strategy *priors*;
|
| 118 |
+
- **intra-model spread** — the same model across games (especially at
|
| 119 |
+
high temperature) scatters — one model generates *diverse* play.
|
| 120 |
+
1v1 is the right venue: full-macro adversarial games make strategy
|
| 121 |
+
legible in a way single-scenario tasks do not. A bigger model roster
|
| 122 |
+
(8–12, Together + Bedrock) makes the clustering visually striking.
|
| 123 |
+
|
| 124 |
+
This is the evidence that planning is not the bottleneck — and a
|
| 125 |
+
genuinely compelling standalone result.
|
| 126 |
+
|
| 127 |
+
---
|
| 128 |
+
|
| 129 |
+
## 6. Finding 1 — Perception gap
|
| 130 |
+
|
| 131 |
+
**Claim, sharpened.** Decompose perception into **extraction** (read
|
| 132 |
+
state from the image) and **binding** (act on image-derived state).
|
| 133 |
+
The perception sweep already separates these: `image`-channel on
|
| 134 |
+
perception packs (count / locate) isolates extraction; `image` on
|
| 135 |
+
action packs isolates binding. Report *which* breaks. The result is
|
| 136 |
+
"the perception→action **binding** fails while pure planning and pure
|
| 137 |
+
extraction are intact" — not the weak "mixed modality is hard."
|
| 138 |
+
|
| 139 |
+
**Experiments.**
|
| 140 |
+
- Perception sweep (6 cells) × model roster × scenario set × seeds →
|
| 141 |
+
the modality-gap number per model (`structured − image`).
|
| 142 |
+
- A "perceive-then-act" scaffold (force the model to transcribe
|
| 143 |
+
positions first) — if it rescues performance, the bottleneck is
|
| 144 |
+
binding, not extraction.
|
| 145 |
+
- **Cross-modal action distillation SFT**: text-mode *winning*
|
| 146 |
+
trajectories + the attached minimap → train image→action. Eval
|
| 147 |
+
`image` channel on **held-out** scenarios.
|
| 148 |
+
- **Transfer**: ERQA + ≥2 other spatial / vision benchmarks
|
| 149 |
+
(e.g. BLINK, a spatial-VQA set), before / after SFT.
|
| 150 |
+
|
| 151 |
+
**Why transfer is the crown jewel.** Training image→action where the
|
| 152 |
+
action is good could teach good *action priors* without the model
|
| 153 |
+
ever using the image. The cross-benchmark transfer is what proves the
|
| 154 |
+
SFT taught the model to *see*. Without it, finding 1 is "task
|
| 155 |
+
finetuning helps task." With it, it is "agentic RTS data improves
|
| 156 |
+
general visual spatial reasoning." Use ≥2 external benchmarks so it
|
| 157 |
+
cannot be called cherry-picked.
|
| 158 |
+
|
| 159 |
+
**Reviewer attack → defense.** "VLMs are known to be bad at dense
|
| 160 |
+
small images — this is just OCR." → We measure it in a *consequential
|
| 161 |
+
agentic loop*, we *localize* extraction vs binding, and we show a fix
|
| 162 |
+
that *transfers*. Plus a render-style mini-ablation (the
|
| 163 |
+
`constant_colors` / scale knobs) so the gap is not an artifact of one
|
| 164 |
+
minimap style.
|
| 165 |
+
|
| 166 |
+
---
|
| 167 |
+
|
| 168 |
+
## 7. Finding 2 — Panic / inaction under adversity
|
| 169 |
+
|
| 170 |
+
**Claim.** Models degrade far more than humans under fog; under heavy
|
| 171 |
+
losses they default to `observe` / `stop` instead of active redirect.
|
| 172 |
+
Operational definition = the **`passivity` metric** (fraction of the
|
| 173 |
+
model's turns spent on `observe` / `stop` only). Keep "panic" only as
|
| 174 |
+
an informal label; formal text says "inaction bias under adversity."
|
| 175 |
+
|
| 176 |
+
**Experiments.**
|
| 177 |
+
- Fog ablation × models × **humans** — report the *fog-penalty gap*
|
| 178 |
+
(human degrades little, model degrades a lot), not raw fog scores.
|
| 179 |
+
- Handoff bad-prefix → passivity, models vs. human. Models freeze
|
| 180 |
+
(high passivity), humans redirect (low).
|
| 181 |
+
- Show passivity is *causally costly* — within-model, passive turns
|
| 182 |
+
predict worse outcomes controlling for position quality.
|
| 183 |
+
|
| 184 |
+
**The coding-bias hypothesis is the spiciest claim — handle with
|
| 185 |
+
care.** Cannot be asserted as fact. Keep as a hypothesis AND support
|
| 186 |
+
with ≥1 of:
|
| 187 |
+
- **base vs. RLHF / instruct** versions of the same model — if base
|
| 188 |
+
models panic less, post-training is implicated;
|
| 189 |
+
- a **loss-aware prompt intervention** ("inaction is costly; never
|
| 190 |
+
just observe when losing") — if a prompt largely fixes it, the
|
| 191 |
+
behavior is a shallow prior, consistent with the hypothesis;
|
| 192 |
+
- passivity vs. known code / RLHF intensity across the roster.
|
| 193 |
+
|
| 194 |
+
**Critical-path risk.** The human baseline needs *real human data at
|
| 195 |
+
scale*. The Play tab is built; data is not collected. Scope: a
|
| 196 |
+
representative scenario subset, several humans, disclosed RTS skill. A
|
| 197 |
+
strong scripted policy can serve as a secondary "competent
|
| 198 |
+
non-panicking" reference if human N is thin.
|
| 199 |
+
|
| 200 |
+
---
|
| 201 |
+
|
| 202 |
+
## 8. Finding 3 — SFT remedy, small beats large
|
| 203 |
+
|
| 204 |
+
**Reviewer attack (serious).** "A task-finetuned small model beating a
|
| 205 |
+
zero-shot large model is trivial." **Defenses to build in:**
|
| 206 |
+
- The SFT is **small and targeted** (perception grounding + active
|
| 207 |
+
recovery), explicitly *not* trained on eval scenarios — state the
|
| 208 |
+
train / eval split loudly.
|
| 209 |
+
- **Also SFT the large model** — show the gap is real and fixable at
|
| 210 |
+
every scale. Headline becomes: "the perception+panic gap is large
|
| 211 |
+
enough that closing it in a small model *outweighs a 10× reasoning
|
| 212 |
+
advantage* — and it was never a reasoning gap."
|
| 213 |
+
- Ablate the SFT: perception-only / recovery-only / both — each
|
| 214 |
+
component contributes.
|
| 215 |
+
- ELO with enough games + confidence intervals; held-out 1v1 maps.
|
| 216 |
+
|
| 217 |
+
**Infrastructure synergy (state explicitly).** The handoff
|
| 218 |
+
`TrajectoryController` + the human-Playback format *are* the SFT data
|
| 219 |
+
pipeline — recovered bad-prefix episodes are active-recovery
|
| 220 |
+
exemplars; text-mode wins are perception-distillation data. The
|
| 221 |
+
ablation infrastructure doubles as the training-data factory.
|
| 222 |
+
|
| 223 |
+
---
|
| 224 |
+
|
| 225 |
+
## 9. Experiment program
|
| 226 |
+
|
| 227 |
+
| # | Experiment | Infra | Status |
|
| 228 |
+
|---|---|---|---|
|
| 229 |
+
| §5 strategy diversity | classify 1v1 games; temp sweep; per-model dists | 1v1 harness ✓; needs strategy classifier | TODO |
|
| 230 |
+
| Perception sweep | 6-cell × roster × scenarios × seeds | ✓ `--perception-sweep` | **run** |
|
| 231 |
+
| Handoff / passivity | base/bad/good × roster | ✓ `--handoff-sweep` | **run** |
|
| 232 |
+
| Human baseline | fog × scenario subset × humans | Play tab ✓, **no data** | **highest-risk** |
|
| 233 |
+
| Cross-modal SFT + transfer | distill text→image; ERQA + 2 more | data pipeline ✓, needs finetuning | **biggest compute** |
|
| 234 |
+
| 1v1 ELO tournament | round-robin + CIs | harness ✓ | run |
|
| 235 |
+
| Recovery SFT | active-recovery exemplars → finetune | handoff bank ✓ | run |
|
| 236 |
+
|
| 237 |
+
Critical path: human baseline (logistics) and the SFT (compute).
|
| 238 |
+
|
| 239 |
+
---
|
| 240 |
+
|
| 241 |
+
## 10. Metrics
|
| 242 |
+
|
| 243 |
+
Win-rate / outcome · composite P/R/A score · objective-progress
|
| 244 |
+
(continuous) · ELO (1v1, with CIs) · **passivity** (freeze metric) ·
|
| 245 |
+
generalization gap (public vs. held-out seeds) · strategy class /
|
| 246 |
+
entropy · human-normalized score (model / human) · derived gaps:
|
| 247 |
+
modality gap = `score(structured) − score(image)`, fog penalty =
|
| 248 |
+
`score(clear) − score(fog)`, per model and per human.
|
| 249 |
+
|
| 250 |
+
---
|
| 251 |
+
|
| 252 |
+
## 11. Threats to validity / limitations to preempt
|
| 253 |
+
|
| 254 |
+
- **One game (RA).** Lean on the capability taxonomy
|
| 255 |
+
(`meta.benchmark_anchor`) + the ERQA transfer for generality.
|
| 256 |
+
- **Engine is a reimplementation.** Deterministic + validated is the
|
| 257 |
+
answer.
|
| 258 |
+
- **One minimap render style.** Render-robustness ablation.
|
| 259 |
+
- **Human skill / N.** Disclose; representative subset.
|
| 260 |
+
- **"Panic = code training."** Hypothesis, not claim — support with
|
| 261 |
+
the probes in §7.
|
| 262 |
+
- **SFT leakage.** Loud train/eval scenario split.
|
| 263 |
+
- **ELO methodology.** Game count, pairing, confidence intervals.
|
| 264 |
+
|
| 265 |
+
---
|
| 266 |
+
|
| 267 |
+
## 12. Pre-full-run audits (must land before the 200-pack sweep)
|
| 268 |
+
|
| 269 |
+
After the pilot finishes and *before* committing compute to the full
|
| 270 |
+
200-pack run, three audits gate the rigor of the headline numbers:
|
| 271 |
+
|
| 272 |
+
### 12.1 Scenario quality audit
|
| 273 |
+
Two layers:
|
| 274 |
+
- **Static** — re-run the scripted-policy bar (`stall` / `brute` /
|
| 275 |
+
`intended`) across all 200 packs. Engine fixes may have drifted a
|
| 276 |
+
pack since authoring (a lazy policy now wins, or `intended` now
|
| 277 |
+
loses). Catches benchmark rot.
|
| 278 |
+
- **Empirical** — from pilot/full-run data, flag packs where *every*
|
| 279 |
+
model wins (too easy / a trivial idiom dominates — task #43) or
|
| 280 |
+
*every* model loses (unsolvable or a predicate is mis-tuned —
|
| 281 |
+
task #44). Discriminative packs are the only useful ones.
|
| 282 |
+
|
| 283 |
+
Paper payoff: a post-hoc audit table converts the "no-defect bar"
|
| 284 |
+
claim into something you can *show* — a strong methodology subsection.
|
| 285 |
+
|
| 286 |
+
### 12.2 Coverage map — RTS phase × decision-divergence
|
| 287 |
+
Map all 200 packs (plus the 1v1 battleground) onto the **RTS phase ×
|
| 288 |
+
decision-divergence matrix** from the original plan
|
| 289 |
+
(opening / early-mid / mid / mid-late / late × the canonical decisions
|
| 290 |
+
in each). Produce a coverage heatmap; flag empty / thin cells. Surface
|
| 291 |
+
the `meta.capability`-tag imbalance (`adversarial`=1 pack — full
|
| 292 |
+
end-to-end macro lives in the 1v1 battleground, both belong on the
|
| 293 |
+
map). Paper payoff: a figure showing the bench spans the *real* game,
|
| 294 |
+
not just easy probes.
|
| 295 |
+
|
| 296 |
+
### 12.3 Multi-run reliability — `pass^k`
|
| 297 |
+
Each (cell, seed) is run N times varying only model nondeterminism
|
| 298 |
+
(requires temperature > 0). Report mean ± CI **and** `pass^k`
|
| 299 |
+
(all-k-wins). A model that wins 5/10 identical runs is a fundamentally
|
| 300 |
+
different finding than 10/10. `--repeats N` in `run_eval`; default
|
| 301 |
+
`k=5` (Codex / SWE-bench convention). Paper payoff: mean-only is
|
| 302 |
+
fragile; reliability is itself a possible headline result.
|
| 303 |
+
|
| 304 |
+
## 13. Stretch ideas
|
| 305 |
+
|
| 306 |
+
- **Pivotal-turn analysis** — single-turn counterfactual swaps to show
|
| 307 |
+
RTS losses are 1–2 catastrophic decisions, not uniform decay.
|
| 308 |
+
|
| 309 |
+
---
|
| 310 |
+
|
| 311 |
+
## 13. Ablation infrastructure already built (this is real, today)
|
| 312 |
+
|
| 313 |
+
- **Fog axis** — engine `reveal_map` no-fog flag (`OpenRA-Rust`),
|
| 314 |
+
the `-clear` perception cells.
|
| 315 |
+
- **Modality axis** — `structured` / `vision` / `image` (image-
|
| 316 |
+
primary, text redacted, labelled minimap) channels;
|
| 317 |
+
`run_eval --perception-sweep` expands `pack:level` into the 6
|
| 318 |
+
modality cells.
|
| 319 |
+
- **Handoff axis** — `openra_bench/handoff.py`
|
| 320 |
+
(`HandoffController`, `TrajectoryController`), `run_eval
|
| 321 |
+
--handoff-sweep`; the `passivity` metric on every result.
|
| 322 |
+
- **1v1 battleground + ELO** — `one_v_one.py`, scripted ladder.
|
| 323 |
+
- **Human-labeling** — the Play tab persists human runs in the
|
| 324 |
+
standard `Playback` format (apples-to-apples with model runs).
|
| 325 |
+
- **200 scenario packs** — no-cheat-validated, capability-anchored.
|
| 326 |
+
|
| 327 |
+
---
|
| 328 |
+
|
| 329 |
+
## 14. Open decisions
|
| 330 |
+
|
| 331 |
+
- Model roster for the sweeps (which models; vision-capable required
|
| 332 |
+
for `vision` / `image` channels).
|
| 333 |
+
- Compute / API budget for the full sweeps.
|
| 334 |
+
- Human-study scope (how many humans, which scenario subset).
|
| 335 |
+
- SFT base model(s) and the small/large pairing for finding 3.
|
| 336 |
+
- Strategy-classifier definition for §5.
|
|
@@ -1242,6 +1242,76 @@ def _play_start(prev_sess, pack, level, seed):
|
|
| 1242 |
)
|
| 1243 |
|
| 1244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1245 |
def _play_click(sess, sel, queue, evt: gr.SelectData):
|
| 1246 |
"""Contextual minimap click — classic RTS interaction, no mode:
|
| 1247 |
|
|
@@ -1567,6 +1637,29 @@ def build_app() -> gr.Blocks:
|
|
| 1567 |
play_sess = gr.State(None)
|
| 1568 |
play_sel = gr.State([])
|
| 1569 |
play_queue = gr.State([])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1570 |
with gr.Row():
|
| 1571 |
play_scen = gr.Dropdown(
|
| 1572 |
choices=_play_scenarios(), label="Scenario",
|
|
@@ -1613,6 +1706,19 @@ def build_app() -> gr.Blocks:
|
|
| 1613 |
play_img, play_brief, play_status, play_units,
|
| 1614 |
],
|
| 1615 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1616 |
play_img.select(
|
| 1617 |
_play_click,
|
| 1618 |
inputs=[play_sess, play_sel, play_queue],
|
|
|
|
| 1242 |
)
|
| 1243 |
|
| 1244 |
|
| 1245 |
+
# ── Human-study mode ─────────────────────────────────────────────────
|
| 1246 |
+
# Walks a recruited player through the fixed 24-pack study subset under
|
| 1247 |
+
# 3 conditions (72 games, per-player counterbalanced). Every game saves
|
| 1248 |
+
# to the standard Playback format — apples-to-apple with model runs.
|
| 1249 |
+
|
| 1250 |
+
def _study_progress_md(st: dict) -> str:
|
| 1251 |
+
pl = st.get("playlist", [])
|
| 1252 |
+
i = st.get("idx", 0)
|
| 1253 |
+
if not pl:
|
| 1254 |
+
return "_Enter your name and click **Begin study**._"
|
| 1255 |
+
if i >= len(pl):
|
| 1256 |
+
return (
|
| 1257 |
+
f"**✅ Study complete** — all {len(pl)} games done. "
|
| 1258 |
+
f"Thank you, `{st['player']}`!"
|
| 1259 |
+
)
|
| 1260 |
+
pack, level, cond = pl[i]
|
| 1261 |
+
return (
|
| 1262 |
+
f"**Study — game {i + 1} / {len(pl)}** · player `{st['player']}`\n\n"
|
| 1263 |
+
f"`{pack}` [{level}] · condition: **{cond}** \n"
|
| 1264 |
+
f"_Play to game-over, then click **Next scenario ▶**._"
|
| 1265 |
+
)
|
| 1266 |
+
|
| 1267 |
+
|
| 1268 |
+
def _study_render(st: dict, prev_sess):
|
| 1269 |
+
"""Open the study session for st's current cell and render it."""
|
| 1270 |
+
if prev_sess is not None:
|
| 1271 |
+
try:
|
| 1272 |
+
prev_sess.close()
|
| 1273 |
+
except Exception: # noqa: BLE001
|
| 1274 |
+
pass
|
| 1275 |
+
empty = _play_units_df(None, [])
|
| 1276 |
+
pl = st.get("playlist", [])
|
| 1277 |
+
if st.get("idx", 0) >= len(pl):
|
| 1278 |
+
return (None, [], [], "", None, "", "", empty, st,
|
| 1279 |
+
_study_progress_md(st))
|
| 1280 |
+
pack, level, cond = pl[st["idx"]]
|
| 1281 |
+
try:
|
| 1282 |
+
from openra_bench.human_study import open_study_session
|
| 1283 |
+
|
| 1284 |
+
sess = open_study_session(
|
| 1285 |
+
pack, level, cond, player=st["player"], seed=1
|
| 1286 |
+
)
|
| 1287 |
+
except Exception as e: # noqa: BLE001
|
| 1288 |
+
return (None, [], [], "", None, f"⚠️ {e}",
|
| 1289 |
+
"_study load failed_", empty, st, _study_progress_md(st))
|
| 1290 |
+
img, brief, status, units = _play_render(sess, [], [])
|
| 1291 |
+
return (sess, [], [], _play_objective_md(sess), img, brief, status,
|
| 1292 |
+
units, st, _study_progress_md(st))
|
| 1293 |
+
|
| 1294 |
+
|
| 1295 |
+
def _study_begin(prev_sess, player):
|
| 1296 |
+
import hashlib
|
| 1297 |
+
|
| 1298 |
+
from openra_bench.human_study import study_playlist
|
| 1299 |
+
|
| 1300 |
+
player = (player or "").strip() or "anon"
|
| 1301 |
+
# Per-player counterbalancing — a stable seed from the name.
|
| 1302 |
+
seed = int(hashlib.md5(player.encode()).hexdigest()[:8], 16)
|
| 1303 |
+
st = {"player": player, "playlist": study_playlist(seed), "idx": 0}
|
| 1304 |
+
return _study_render(st, prev_sess)
|
| 1305 |
+
|
| 1306 |
+
|
| 1307 |
+
def _study_next(prev_sess, st):
|
| 1308 |
+
if not st or "playlist" not in st:
|
| 1309 |
+
return _study_render({"player": "anon", "playlist": []}, prev_sess)
|
| 1310 |
+
st = dict(st)
|
| 1311 |
+
st["idx"] = st.get("idx", 0) + 1
|
| 1312 |
+
return _study_render(st, prev_sess)
|
| 1313 |
+
|
| 1314 |
+
|
| 1315 |
def _play_click(sess, sel, queue, evt: gr.SelectData):
|
| 1316 |
"""Contextual minimap click — classic RTS interaction, no mode:
|
| 1317 |
|
|
|
|
| 1637 |
play_sess = gr.State(None)
|
| 1638 |
play_sel = gr.State([])
|
| 1639 |
play_queue = gr.State([])
|
| 1640 |
+
study_state = gr.State({})
|
| 1641 |
+
with gr.Accordion(
|
| 1642 |
+
"📋 Human-study mode — 24-pack subset, 3 conditions",
|
| 1643 |
+
open=False,
|
| 1644 |
+
):
|
| 1645 |
+
gr.Markdown(
|
| 1646 |
+
"For the **human-baseline study**. Enter your name "
|
| 1647 |
+
"and click **Begin study** — you'll be walked "
|
| 1648 |
+
"through 72 games (24 scenarios × fog / no-fog / "
|
| 1649 |
+
"handoff-deficit), counterbalanced per player. "
|
| 1650 |
+
"Play each to game-over, then **Next scenario ▶**. "
|
| 1651 |
+
"Every game auto-saves apples-to-apple with the "
|
| 1652 |
+
"model runs."
|
| 1653 |
+
)
|
| 1654 |
+
with gr.Row():
|
| 1655 |
+
study_player = gr.Textbox(
|
| 1656 |
+
label="Your name / id", scale=2,
|
| 1657 |
+
)
|
| 1658 |
+
study_begin_btn = gr.Button("Begin study", scale=1)
|
| 1659 |
+
study_next_btn = gr.Button(
|
| 1660 |
+
"Next scenario ▶", variant="primary", scale=1,
|
| 1661 |
+
)
|
| 1662 |
+
study_progress = gr.Markdown()
|
| 1663 |
with gr.Row():
|
| 1664 |
play_scen = gr.Dropdown(
|
| 1665 |
choices=_play_scenarios(), label="Scenario",
|
|
|
|
| 1706 |
play_img, play_brief, play_status, play_units,
|
| 1707 |
],
|
| 1708 |
)
|
| 1709 |
+
_study_outputs = [
|
| 1710 |
+
play_sess, play_sel, play_queue, play_objective,
|
| 1711 |
+
play_img, play_brief, play_status, play_units,
|
| 1712 |
+
study_state, study_progress,
|
| 1713 |
+
]
|
| 1714 |
+
study_begin_btn.click(
|
| 1715 |
+
_study_begin, inputs=[play_sess, study_player],
|
| 1716 |
+
outputs=_study_outputs,
|
| 1717 |
+
)
|
| 1718 |
+
study_next_btn.click(
|
| 1719 |
+
_study_next, inputs=[play_sess, study_state],
|
| 1720 |
+
outputs=_study_outputs,
|
| 1721 |
+
)
|
| 1722 |
play_img.select(
|
| 1723 |
_play_click,
|
| 1724 |
inputs=[play_sess, play_sel, play_queue],
|
|
@@ -535,14 +535,19 @@ class InteractiveSession:
|
|
| 535 |
record: bool = True,
|
| 536 |
playback_root: Any = None,
|
| 537 |
player: str = "Human",
|
|
|
|
| 538 |
) -> "InteractiveSession":
|
| 539 |
-
"""Compile a pack by id and open a session on it.
|
|
|
|
|
|
|
|
|
|
| 540 |
from .scenarios import load_pack
|
| 541 |
from .scenarios.loader import PACKS_DIR, compile_level
|
| 542 |
|
| 543 |
compiled = compile_level(
|
| 544 |
load_pack(PACKS_DIR / f"{pack_id}.yaml"), level
|
| 545 |
)
|
|
|
|
| 546 |
return cls(
|
| 547 |
compiled, seed=seed, record=record,
|
| 548 |
playback_root=playback_root, player=player,
|
|
|
|
| 535 |
record: bool = True,
|
| 536 |
playback_root: Any = None,
|
| 537 |
player: str = "Human",
|
| 538 |
+
fog_mode: str = "vision",
|
| 539 |
) -> "InteractiveSession":
|
| 540 |
+
"""Compile a pack by id and open a session on it. `fog_mode`
|
| 541 |
+
selects the observation modality — `vision-clear` (or any
|
| 542 |
+
`-clear` mode) reveals the whole map (engine `reveal_map`), the
|
| 543 |
+
no-fog condition of the human-study fog-penalty pair."""
|
| 544 |
from .scenarios import load_pack
|
| 545 |
from .scenarios.loader import PACKS_DIR, compile_level
|
| 546 |
|
| 547 |
compiled = compile_level(
|
| 548 |
load_pack(PACKS_DIR / f"{pack_id}.yaml"), level
|
| 549 |
)
|
| 550 |
+
compiled.fog_mode = fog_mode
|
| 551 |
return cls(
|
| 552 |
compiled, seed=seed, record=record,
|
| 553 |
playback_root=playback_root, player=player,
|
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Human-baseline study — the apples-to-apple human reference.
|
| 2 |
+
|
| 3 |
+
External tournament replays cannot be scored on this benchmark: a real
|
| 4 |
+
RTS game is a different engine, continuous-time, a different action
|
| 5 |
+
space, and not one of the constructed tasks. The ONLY comparable human
|
| 6 |
+
data comes through the Play tab — same engine, scenario, observation,
|
| 7 |
+
tool API, win predicate, and `Playback` format the models use.
|
| 8 |
+
|
| 9 |
+
This module defines a fixed, stratified 24-pack subset (one pack per
|
| 10 |
+
scenario family, difficulty spread 8 easy / 8 medium / 8 hard) and the
|
| 11 |
+
three conditions a player plays each under:
|
| 12 |
+
|
| 13 |
+
* `vision-fog` — the canonical fogged minimap (normal play).
|
| 14 |
+
* `vision-clear` — no fog (engine `reveal_map`): the fog-penalty pair.
|
| 15 |
+
* `handoff-bad` — the player inherits a losing position (a `stall`
|
| 16 |
+
prefix of `HANDOFF_K` turns), then plays on — the
|
| 17 |
+
recovery / freeze comparison.
|
| 18 |
+
|
| 19 |
+
A per-player counterbalanced playlist walks all 24 x 3 = 72 cells.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
from __future__ import annotations
|
| 23 |
+
|
| 24 |
+
import random
|
| 25 |
+
from typing import Any
|
| 26 |
+
|
| 27 |
+
# Frozen subset — one representative pack per scenario family, levels
|
| 28 |
+
# spread across difficulty. Stratified by family (not by the lopsided
|
| 29 |
+
# `meta.capability` tag). Keep this list STABLE — it pins the baseline.
|
| 30 |
+
STUDY_SUBSET: list[tuple[str, str]] = [
|
| 31 |
+
("combat-pincer-coordination", "easy"),
|
| 32 |
+
("econ-overflow-to-silos", "medium"),
|
| 33 |
+
("def-pre-position-mobile-reserve", "hard"),
|
| 34 |
+
("build-rally-point-management", "easy"),
|
| 35 |
+
("scout-detect-incoming-army", "medium"),
|
| 36 |
+
("lh-opening-to-defense-to-counter", "hard"),
|
| 37 |
+
("proc-only-defend-no-attack", "easy"),
|
| 38 |
+
("mfb-supply-line-link-between-bases", "medium"),
|
| 39 |
+
("rob-objective-shift-with-or-clause", "hard"),
|
| 40 |
+
("coord-mutual-support", "easy"),
|
| 41 |
+
("tp-rush-objective-very-fast", "medium"),
|
| 42 |
+
("mcv-deploy-relocate-under-pressure", "hard"),
|
| 43 |
+
("artofwar-lure-the-tiger", "easy"),
|
| 44 |
+
("economy-harvest-timebox", "medium"),
|
| 45 |
+
("perception-frontier-reading", "hard"),
|
| 46 |
+
("strategy-trilemma", "easy"),
|
| 47 |
+
("tech-production-planning", "medium"),
|
| 48 |
+
("expansion-balanced-2-base-defended", "hard"),
|
| 49 |
+
("mid-economy-under-fire", "easy"),
|
| 50 |
+
("strict-sequence", "medium"),
|
| 51 |
+
("action-sequenced-execution", "hard"),
|
| 52 |
+
("adv-rps-counter-pick", "easy"),
|
| 53 |
+
("coordination-staggered-window", "medium"),
|
| 54 |
+
("maint-sell-and-recoup-cash", "hard"),
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
STUDY_CONDITIONS: tuple[str, ...] = ("vision-fog", "vision-clear", "handoff-bad")
|
| 58 |
+
|
| 59 |
+
# Prefix length for the `handoff-bad` condition — the player inherits a
|
| 60 |
+
# game already `HANDOFF_K` observe-only (stall) turns deep.
|
| 61 |
+
HANDOFF_K = 3
|
| 62 |
+
|
| 63 |
+
# fog_mode each condition compiles the scenario under.
|
| 64 |
+
_CONDITION_FOG = {
|
| 65 |
+
"vision-fog": "vision",
|
| 66 |
+
"vision-clear": "vision-clear",
|
| 67 |
+
"handoff-bad": "vision",
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def study_playlist(player_seed: int = 0) -> list[tuple[str, str, str]]:
|
| 72 |
+
"""The 72-cell playlist — every (pack, level) x condition — in a
|
| 73 |
+
per-player counterbalanced (deterministically shuffled) order, so
|
| 74 |
+
condition ordering is not confounded across players."""
|
| 75 |
+
cells = [
|
| 76 |
+
(pack, level, cond)
|
| 77 |
+
for pack, level in STUDY_SUBSET
|
| 78 |
+
for cond in STUDY_CONDITIONS
|
| 79 |
+
]
|
| 80 |
+
random.Random(player_seed).shuffle(cells)
|
| 81 |
+
return cells
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def open_study_session(
|
| 85 |
+
pack: str,
|
| 86 |
+
level: str,
|
| 87 |
+
condition: str,
|
| 88 |
+
player: str,
|
| 89 |
+
seed: int = 1,
|
| 90 |
+
playback_root: Any = None,
|
| 91 |
+
):
|
| 92 |
+
"""Open an `InteractiveSession` for one study cell, configured for
|
| 93 |
+
`condition`. For `handoff-bad` the engine is advanced `HANDOFF_K`
|
| 94 |
+
observe-only turns BEFORE the player takes over — so the player
|
| 95 |
+
inherits a real deficit, exactly like the model handoff ablation.
|
| 96 |
+
|
| 97 |
+
The run persists to the standard `Playback` format (apples-to-apple
|
| 98 |
+
with model runs); `playback_root` defaults to a per-condition dir
|
| 99 |
+
so the condition is recoverable from the path."""
|
| 100 |
+
from pathlib import Path
|
| 101 |
+
|
| 102 |
+
from .human_labeling import InteractiveSession
|
| 103 |
+
|
| 104 |
+
if condition not in STUDY_CONDITIONS:
|
| 105 |
+
raise ValueError(f"unknown study condition {condition!r}")
|
| 106 |
+
if playback_root is None:
|
| 107 |
+
playback_root = Path("playback/human_study") / condition
|
| 108 |
+
|
| 109 |
+
sess = InteractiveSession.from_pack(
|
| 110 |
+
pack, level, seed,
|
| 111 |
+
record=True, playback_root=playback_root, player=player,
|
| 112 |
+
fog_mode=_CONDITION_FOG[condition],
|
| 113 |
+
)
|
| 114 |
+
if condition == "handoff-bad":
|
| 115 |
+
# Stall prefix — the player inherits the resulting losing board.
|
| 116 |
+
for _ in range(HANDOFF_K):
|
| 117 |
+
if sess.done:
|
| 118 |
+
break
|
| 119 |
+
sess.submit_turn([])
|
| 120 |
+
return sess
|
|
@@ -165,6 +165,7 @@ def evaluate(
|
|
| 165 |
handoff_sweep: bool = False,
|
| 166 |
handoff_k: int = 3,
|
| 167 |
handoff_bank: str | Path | None = None,
|
|
|
|
| 168 |
) -> dict:
|
| 169 |
"""Run packs×levels×seeds. If `held_out_seeds` is given, those are
|
| 170 |
run too and tagged split='held_out'; the report adds
|
|
@@ -184,6 +185,11 @@ def evaluate(
|
|
| 184 |
position replayed from a `handoff_bank` trajectory (`good` — the
|
| 185 |
capitalize-on-advantage test). `handoff_k` is the prefix length.
|
| 186 |
Each record carries a `passivity` stat (observe/stop-only fraction).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
"""
|
| 188 |
from .resilience import (
|
| 189 |
BudgetExceeded,
|
|
@@ -309,12 +315,16 @@ def evaluate(
|
|
| 309 |
continue
|
| 310 |
for split, slist in (("public", seeds), ("held_out", held_out_seeds)):
|
| 311 |
for seed in slist:
|
| 312 |
-
|
|
|
|
| 313 |
|
| 314 |
def _run_one(task: tuple) -> dict:
|
| 315 |
-
compiled, cell, split, seed = task
|
| 316 |
pb = None
|
| 317 |
-
|
|
|
|
|
|
|
|
|
|
| 318 |
from .playback import Playback
|
| 319 |
|
| 320 |
pb = Playback(
|
|
@@ -359,6 +369,7 @@ def evaluate(
|
|
| 359 |
"capability": compiled.meta.capability,
|
| 360 |
"split": split,
|
| 361 |
"seed": seed,
|
|
|
|
| 362 |
"outcome": sc.outcome,
|
| 363 |
"composite": sc.composite,
|
| 364 |
"perception": sc.perception,
|
|
@@ -446,7 +457,7 @@ def evaluate(
|
|
| 446 |
# not abort a multi-hour sweep or lose the report — record
|
| 447 |
# it as outcome="error" and continue. Budget is the only
|
| 448 |
# signal that intentionally stops the whole run.
|
| 449 |
-
compiled, cell, split, seed = task
|
| 450 |
try:
|
| 451 |
return _run_one(task)
|
| 452 |
except BudgetExceeded:
|
|
@@ -458,6 +469,7 @@ def evaluate(
|
|
| 458 |
"capability": compiled.meta.capability,
|
| 459 |
"split": split,
|
| 460 |
"seed": seed,
|
|
|
|
| 461 |
"outcome": "error",
|
| 462 |
"composite": 0.0,
|
| 463 |
"perception": 0.0,
|
|
@@ -688,6 +700,14 @@ def main(argv: list[str]) -> int:
|
|
| 688 |
ap.add_argument("--handoff-bank", default=None,
|
| 689 |
help="dir of Playback runs — source of winning "
|
| 690 |
"trajectories for the handoff-good prefix")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 691 |
a = ap.parse_args(argv[1:])
|
| 692 |
|
| 693 |
cfg = None
|
|
@@ -704,7 +724,7 @@ def main(argv: list[str]) -> int:
|
|
| 704 |
if quant:
|
| 705 |
pr["quantizations"] = [quant]
|
| 706 |
extra_body["provider"] = pr
|
| 707 |
-
|
| 708 |
provider=a.provider,
|
| 709 |
model=a.model,
|
| 710 |
base_url=a.base_url,
|
|
@@ -713,6 +733,9 @@ def main(argv: list[str]) -> int:
|
|
| 713 |
fog_mode=a.fog_mode,
|
| 714 |
extra_body=extra_body,
|
| 715 |
)
|
|
|
|
|
|
|
|
|
|
| 716 |
|
| 717 |
stats = evaluate(
|
| 718 |
_resolve_packs(a.packs),
|
|
@@ -733,6 +756,7 @@ def main(argv: list[str]) -> int:
|
|
| 733 |
handoff_sweep=a.handoff_sweep,
|
| 734 |
handoff_k=a.handoff_k,
|
| 735 |
handoff_bank=a.handoff_bank,
|
|
|
|
| 736 |
progress=lambda d, n, rec, c: print(
|
| 737 |
f"[{d}/{n}] {rec['cell']}:{rec['split']}#{rec['seed']} "
|
| 738 |
f"{rec['outcome']} comp={rec['composite']} "
|
|
|
|
| 165 |
handoff_sweep: bool = False,
|
| 166 |
handoff_k: int = 3,
|
| 167 |
handoff_bank: str | Path | None = None,
|
| 168 |
+
repeats: int = 1,
|
| 169 |
) -> dict:
|
| 170 |
"""Run packs×levels×seeds. If `held_out_seeds` is given, those are
|
| 171 |
run too and tagged split='held_out'; the report adds
|
|
|
|
| 185 |
position replayed from a `handoff_bank` trajectory (`good` — the
|
| 186 |
capitalize-on-advantage test). `handoff_k` is the prefix length.
|
| 187 |
Each record carries a `passivity` stat (observe/stop-only fraction).
|
| 188 |
+
|
| 189 |
+
`repeats` runs each (cell, seed) `N` times, varying only model
|
| 190 |
+
nondeterminism (assumes temperature > 0). Records carry a `repeat`
|
| 191 |
+
index 0..N-1, so aggregation can report mean ± CI and `pass^k`
|
| 192 |
+
(all-k wins) alongside `pass@k` — the reliability metric.
|
| 193 |
"""
|
| 194 |
from .resilience import (
|
| 195 |
BudgetExceeded,
|
|
|
|
| 315 |
continue
|
| 316 |
for split, slist in (("public", seeds), ("held_out", held_out_seeds)):
|
| 317 |
for seed in slist:
|
| 318 |
+
for rep in range(max(1, repeats)):
|
| 319 |
+
tasks.append((compiled, cell, split, seed, rep))
|
| 320 |
|
| 321 |
def _run_one(task: tuple) -> dict:
|
| 322 |
+
compiled, cell, split, seed, rep = task
|
| 323 |
pb = None
|
| 324 |
+
# Only the first repeat writes a Playback — the records (the
|
| 325 |
+
# lightweight per-rep results) carry the pass^k data; saving N
|
| 326 |
+
# full per-turn dumps per cell would just bloat disk.
|
| 327 |
+
if playback_root is not None and rep == 0:
|
| 328 |
from .playback import Playback
|
| 329 |
|
| 330 |
pb = Playback(
|
|
|
|
| 369 |
"capability": compiled.meta.capability,
|
| 370 |
"split": split,
|
| 371 |
"seed": seed,
|
| 372 |
+
"repeat": rep,
|
| 373 |
"outcome": sc.outcome,
|
| 374 |
"composite": sc.composite,
|
| 375 |
"perception": sc.perception,
|
|
|
|
| 457 |
# not abort a multi-hour sweep or lose the report — record
|
| 458 |
# it as outcome="error" and continue. Budget is the only
|
| 459 |
# signal that intentionally stops the whole run.
|
| 460 |
+
compiled, cell, split, seed, rep = task
|
| 461 |
try:
|
| 462 |
return _run_one(task)
|
| 463 |
except BudgetExceeded:
|
|
|
|
| 469 |
"capability": compiled.meta.capability,
|
| 470 |
"split": split,
|
| 471 |
"seed": seed,
|
| 472 |
+
"repeat": rep,
|
| 473 |
"outcome": "error",
|
| 474 |
"composite": 0.0,
|
| 475 |
"perception": 0.0,
|
|
|
|
| 700 |
ap.add_argument("--handoff-bank", default=None,
|
| 701 |
help="dir of Playback runs — source of winning "
|
| 702 |
"trajectories for the handoff-good prefix")
|
| 703 |
+
ap.add_argument("--repeats", type=int, default=1,
|
| 704 |
+
help="run each (cell, seed) N times varying only "
|
| 705 |
+
"model nondeterminism — enables mean +- CI and "
|
| 706 |
+
"pass^k reliability metrics (needs temperature > 0)")
|
| 707 |
+
ap.add_argument("--temperature", type=float, default=None,
|
| 708 |
+
help="sampling temperature for the model "
|
| 709 |
+
"(overrides ProviderConfig.temperature). Set > 0 "
|
| 710 |
+
"to make --repeats meaningful.")
|
| 711 |
a = ap.parse_args(argv[1:])
|
| 712 |
|
| 713 |
cfg = None
|
|
|
|
| 724 |
if quant:
|
| 725 |
pr["quantizations"] = [quant]
|
| 726 |
extra_body["provider"] = pr
|
| 727 |
+
cfg_kw = dict(
|
| 728 |
provider=a.provider,
|
| 729 |
model=a.model,
|
| 730 |
base_url=a.base_url,
|
|
|
|
| 733 |
fog_mode=a.fog_mode,
|
| 734 |
extra_body=extra_body,
|
| 735 |
)
|
| 736 |
+
if a.temperature is not None:
|
| 737 |
+
cfg_kw["temperature"] = a.temperature
|
| 738 |
+
cfg = ProviderConfig(**cfg_kw)
|
| 739 |
|
| 740 |
stats = evaluate(
|
| 741 |
_resolve_packs(a.packs),
|
|
|
|
| 756 |
handoff_sweep=a.handoff_sweep,
|
| 757 |
handoff_k=a.handoff_k,
|
| 758 |
handoff_bank=a.handoff_bank,
|
| 759 |
+
repeats=a.repeats,
|
| 760 |
progress=lambda d, n, rec, c: print(
|
| 761 |
f"[{d}/{n}] {rec['cell']}:{rec['split']}#{rec['seed']} "
|
| 762 |
f"{rec['outcome']} comp={rec['composite']} "
|
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The human-baseline study harness — the apples-to-apple human
|
| 2 |
+
reference: a fixed 24-pack subset, three conditions, a per-player
|
| 3 |
+
counterbalanced playlist, run through the same Play-tab `Playback`
|
| 4 |
+
pipeline the models use."""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
import pytest
|
| 11 |
+
|
| 12 |
+
pytest.importorskip("openra_rl_training", reason="Rust env wheel not installed")
|
| 13 |
+
|
| 14 |
+
from openra_bench.human_study import (HANDOFF_K, STUDY_CONDITIONS,
|
| 15 |
+
STUDY_SUBSET, open_study_session,
|
| 16 |
+
study_playlist)
|
| 17 |
+
|
| 18 |
+
PACKS = Path(__file__).parent.parent / "openra_bench" / "scenarios" / "packs"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_subset_is_24_packs_that_all_exist():
|
| 22 |
+
assert len(STUDY_SUBSET) == 24
|
| 23 |
+
for pack, level in STUDY_SUBSET:
|
| 24 |
+
assert (PACKS / f"{pack}.yaml").exists(), pack
|
| 25 |
+
assert level in ("easy", "medium", "hard"), (pack, level)
|
| 26 |
+
# difficulty is stratified, not all one level
|
| 27 |
+
levels = {lvl for _, lvl in STUDY_SUBSET}
|
| 28 |
+
assert levels == {"easy", "medium", "hard"}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def test_playlist_is_full_grid_and_counterbalanced():
|
| 32 |
+
pl0 = study_playlist(player_seed=0)
|
| 33 |
+
assert len(pl0) == len(STUDY_SUBSET) * len(STUDY_CONDITIONS) == 72
|
| 34 |
+
# every (pack, level, condition) cell appears exactly once
|
| 35 |
+
assert len(set(pl0)) == 72
|
| 36 |
+
for pack, level, cond in pl0:
|
| 37 |
+
assert (pack, level) in STUDY_SUBSET
|
| 38 |
+
assert cond in STUDY_CONDITIONS
|
| 39 |
+
# different players get a different order (counterbalancing)
|
| 40 |
+
assert study_playlist(1) != pl0
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@pytest.mark.parametrize("cond", STUDY_CONDITIONS)
|
| 44 |
+
def test_open_study_session_each_condition(cond, tmp_path):
|
| 45 |
+
sess = open_study_session(
|
| 46 |
+
"perception-frontier-reading", "hard", cond,
|
| 47 |
+
player="tester", seed=1, playback_root=tmp_path,
|
| 48 |
+
)
|
| 49 |
+
try:
|
| 50 |
+
rs = sess.render_state()
|
| 51 |
+
if cond == "vision-clear":
|
| 52 |
+
# no fog — engine reveal_map: whole map explored
|
| 53 |
+
assert rs.get("explored_percent", 0) > 99.0
|
| 54 |
+
assert sess.compiled.fog_mode == "vision-clear"
|
| 55 |
+
elif cond == "handoff-bad":
|
| 56 |
+
# the player inherits a HANDOFF_K-turn-deep deficit
|
| 57 |
+
assert sess.turn == HANDOFF_K
|
| 58 |
+
assert sess.compiled.fog_mode == "vision"
|
| 59 |
+
else: # vision-fog — normal fogged start
|
| 60 |
+
assert sess.turn == 0
|
| 61 |
+
assert rs.get("explored_percent", 100) < 50.0
|
| 62 |
+
finally:
|
| 63 |
+
sess.close()
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def test_study_session_persists_playback(tmp_path):
|
| 67 |
+
"""A study game must save in the standard Playback format so it is
|
| 68 |
+
apples-to-apple with model runs."""
|
| 69 |
+
sess = open_study_session(
|
| 70 |
+
"perception-frontier-reading", "easy", "vision-fog",
|
| 71 |
+
player="tester", seed=1, playback_root=tmp_path,
|
| 72 |
+
)
|
| 73 |
+
try:
|
| 74 |
+
sess.submit_turn([]) # one observe turn
|
| 75 |
+
assert sess._playback is not None
|
| 76 |
+
assert sess._playback.dir.exists()
|
| 77 |
+
finally:
|
| 78 |
+
sess.close()
|
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""--repeats N: run each (cell, seed) N times so the report carries
|
| 2 |
+
the variance + pass^k a single inference per cell cannot give."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import pytest
|
| 9 |
+
|
| 10 |
+
pytest.importorskip("openra_rl_training", reason="Rust env wheel not installed")
|
| 11 |
+
|
| 12 |
+
from openra_bench.run_eval import evaluate
|
| 13 |
+
|
| 14 |
+
PACK = (
|
| 15 |
+
Path(__file__).parent.parent
|
| 16 |
+
/ "openra_bench" / "scenarios" / "packs"
|
| 17 |
+
/ "perception-count-the-threat.yaml"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_repeats_multiplies_the_task_count():
|
| 22 |
+
base = evaluate([PACK], levels=["easy"], seeds=[1, 2],
|
| 23 |
+
repeats=1, dry_run=True)
|
| 24 |
+
rep3 = evaluate([PACK], levels=["easy"], seeds=[1, 2],
|
| 25 |
+
repeats=3, dry_run=True)
|
| 26 |
+
assert rep3["tasks"] == base["tasks"] * 3
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def test_records_carry_repeat_index():
|
| 30 |
+
stats = evaluate([PACK], levels=["easy"], seeds=[1], repeats=3)
|
| 31 |
+
eps = stats.get("episodes", [])
|
| 32 |
+
assert len(eps) == 3
|
| 33 |
+
assert {e["repeat"] for e in eps} == {0, 1, 2}
|
| 34 |
+
# all repeats stay at the same (cell, seed) — same key, different rep
|
| 35 |
+
assert len({(e["cell"], e["seed"]) for e in eps}) == 1
|