# OpenRA-Bench — Scenario Brainstorm & Economy Family Spec Design document. No engine or existing-pack changes. Two runnable economy packs were drafted and validate OK (see end). All win conditions below use ONLY leaves/composites that exist in `win_conditions.py`: `explored_pct_gte, enemies_discovered_gte, buildings_discovered_gte, units_killed_gte, units_lost_lte, within_ticks, after_ticks, reach_region, all_units_in_region, own_units_gte, cash_gte, harvesters_gte, power_surplus_gte, has_building, buildings_owned_gte, building_total_gte, building_count_gte{type,n}, building_in_region{type?,x,y,radius,count}`; composites `all_of / any_of / not`. --- ## Verified ground-truth constraints (this drove the design) 1. **No schema-valid ore source.** The Rust engine seeds an ore patch in a ~4-cell radius around any actor of type `mine` (`openra-sim/src/world.rs` ~L4014, `map_actor.actor_type == "mine"` at ~L3908). **But** the Python `ScenarioDefinition` actor whitelist (`openra_rl_training.scenario.VALID_ACTOR_TYPES`) has **no `mine` / `gmine`**, so a pack that spawns one fails validation. The 5 `.oramap` files (`map.yaml` inspected) embed **no resource tiles** either — only `mpspawn` actors. ⇒ **Harvest income is 0 in any runnable pack today.** `harv` + `proc` spawn fine but find no ore. `cash_gte` is only satisfiable from `starting_cash`, and `harvesters_gte` only counts the auto-spawned harvester (it never earns). 2. **`silo` is schema-valid but inert.** `silo` is in `VALID_ACTOR_TYPES`, but `PlayerResources.resource_capacity` is hardcoded `0` (`openra-sim/src/traits/mod.rs:76,172`, `world.rs:3609`) and the deposit path converts ore straight to cash ignoring capacity (`world.rs:1467-1473`). Building silos has **no economic effect**. 3. **`starting_cash` works** (per-level / pack-wide, schema confirmed), build/train with tech-tree prerequisites works, `proc` auto-spawns one `harv` on creation (`world.rs:1154`). 4. Only `rush-hour-arena` is Rust-loadable today (CONTRIBUTING §5); the four `singles-*` maps are schema-valid but Phase-3-gated. **Design consequence:** runnable economy scenarios test the **spend / allocation** decision over a fixed budget (the part the lead identified as the real test). Harvest-throughput and silo-storage variants are fully specced but tagged engine-prereq. --- # 1. Requested family — Economy-Time-Box (RUNNABLE TODAY) **Pack:** `packs/economy-time-box.yaml` — validates **OK**. **Capability:** reasoning. **Decision:** within a fixed tick budget, convert a fixed cash budget into BOTH deployed units AND a standing economy; idle cash and all-in-on-units both fail. real_world_meaning — Time-bounded capital deployment: a finite, non-replenishing budget must become mission capability *and* persistent infrastructure before a hard deadline. robotics_analogue — A field robot on a finite energy budget scheduling expenditure between mission work and standing up persistent infrastructure before a deadline. | Level | starting_cash | win_condition (real grammar) | scaling axis | |---|---|---|---| | easy | 4000 | `all_of[own_units_gte:4, building_total_gte:4, within_ticks:22000]` | loose clock & budget | | medium | 2400 | `all_of[own_units_gte:5, has_building:tent, building_total_gte:4, within_ticks:16000]` | adds tech-dep building, tighter | | hard | 1500 | `all_of[own_units_gte:6, building_total_gte:5, units_lost_lte:0, within_ticks:12000]` | lean budget, short clock, attrition cap | Difficulty comes from the decision getting harder (less budget slack, a tech-dependency constraint, an attrition cap + short clock), not bigger target numbers. Predicates: `own_units_gte`, `building_total_gte`, `has_building`, `units_lost_lte`, `within_ticks`. Commands: `build`, `place_building`, `move_units`, `deploy`, `stop`. Map: `rush-hour-arena`. **Engine status: runnable now.** ### Follow-up (leaderboard scoring, not a predicate today) A *final-economy-value* score = `cash + Σ asset_value(owned units & buildings)` at the deadline would reward "spent it well" beyond the binary `cash_gte`. No such predicate exists; propose it as a scoring / leaderboard metric computed from the render state at episode end (sum buildable costs of `own_buildings` + `units_summary`, add `signals.cash`). Do **not** add it as a win leaf — pass/fail stays in the grammar above. ### Harvest-income variant — **needs S0 + S1** (see §Engine-prereqs) Same shape but income comes from harvesting and `cash_gte` rises over time: `all_of[cash_gte:N, own_units_gte:K, harvesters_gte:1, within_ticks:T]`. Requires S0 (schema-valid ore source so harvesting actually earns) — and S1 only if silo storage is part of the test. --- # 2. Requested family — Economy-Investment (allocation decision) **Pack:** `packs/economy-investment.yaml` — validates **OK** (runnable variant). real_world_meaning — Capital allocation under one indivisible budget: the same cash buys either a *wide* economy (more depot/throughput capacity) or a *deep* one (more collectors/force); the budget covers exactly one coherent path, so splitting builds neither. robotics_analogue — Fleet capital allocation: a fixed budget buying either added depot/processing capacity or added collector agents — the throughput-vs-collection trade, where indecision yields neither. ### (a) Runnable-today variant — RUNNABLE NOW Income is the fixed `starting_cash`; the allocation is verified by build predicates (no silo, no harvest dependency). | Level | starting_cash | win_condition (real grammar) | path tested | |---|---|---|---| | easy | 4500 | `all_of[building_count_gte{proc,2}, building_count_gte{powr,2}, within_ticks:22000]` | wide, loose | | medium | 3200 | `all_of[building_count_gte{proc,2}, building_count_gte{powr,2}, own_units_gte:3, within_ticks:16000]` | wide + utilisation, tighter | | hard | 2600 | `all_of[own_units_gte:6, building_count_gte{proc,1}, units_lost_lte:0, within_ticks:14000]` | deep (force) under lean budget + attrition | Difficulty escalates by removing budget slack so the allocation must be committed (a split budget clears no bar), tightening the clock, and adding an attrition cap. Predicates: `building_count_gte{type,n}`, `own_units_gte`, `units_lost_lte`, `within_ticks`. Commands as above. Map `rush-hour-arena`. ### (b) Full version — **ENGINE-PREREQ (needs S0 + S1)** The faithful test the lead asked for: enough cash for **two refineries OR one refinery + extra harvesters**, scored by **harvested cash at the deadline**, plus **silos that raise the harvest cap**. - Levels: easy = budget covers either path comfortably (commit to one); medium = budget covers exactly one (the bottleneck differs: 2×proc = unload throughput, 1×proc+harv = gather rate); hard = lean budget + short clock + silo decision (must build `silo` to avoid capping the resource pool before a return trip). - Win grammar (valid leaves, *meaningful only after S0/S1*): `all_of[cash_gte:N, harvesters_gte:H, within_ticks:T]` for the allocation; the silo sub-goal is observable as `building_count_gte{type:silo,n:K}` but only *matters* after S1. - **Engine status: needs S0 (income) + S1 (storage).** Without them the two allocations are economically identical (income 0) so the scenario is not discriminating — hence variant (a) ships instead. --- ## Engine-prerequisite specs ### S0 — schema-valid ore source (smaller, unblocks all harvest income) Today the only ore mechanism is `actor_type == "mine"`, which the Python schema rejects. Two non-engine-code options the project could take (documented here, not implemented): - Add `mine`/`gmine` to `VALID_ACTOR_TYPES` in `openra_rl_training/scenario.py` (one list entry; the Rust seed loop at `world.rs:4014` already handles it), **or** - Add a `resource_fields: [{x,y,radius,density}]` field to `ScenarioDefinition` plumbed to `terrain.set_resource(...)` (`openra-sim/src/terrain.rs:193`) at world init. Either makes harvest income real and unblocks the §1 harvest variant and §2(b) allocation scoring (without silos). ### S1 — silo / resource-storage modeling (the part to flag) Goal: harvested resource enters a **capped pool**; cash is realised from the pool gradually (or on cap pressure), and `silo` buildings raise the cap. Precise change (Rust): 1. **`openra-sim/src/traits/mod.rs`** — `TraitState::PlayerResources { cash, resources, resource_capacity }` already has the fields; stop hardcoding `resource_capacity: 0` at the two constructor sites (`mod.rs:172`, `world.rs:3609`). Give players a small base capacity (e.g. from `fact`/`proc`). 2. **`openra-sim/src/world.rs` deposit path (~L1467-1473, `Unloading` arm of `tick_harvesters`)** — instead of `player.set_cash(current + value)`, add to `PlayerResources.resources`, clamped to `resource_capacity`; overflow is lost (matches OpenRA). Add a per-tick drain that converts `resources → cash` at a fixed rate (the realisation of stored ore). 3. **Silo capacity contribution** — when an actor of type `silo` (and `proc`, in real OpenRA) becomes owned/active, add its `Storage` amount to the owning player's `resource_capacity`; subtract on loss. Implement as a recompute over owned buildings in the building add/remove path (mirror how `PowerManager` provided/drained is recomputed), reading a per-building storage constant (silo ≈ 3000, proc ≈ 2000 in RA defaults). 4. **(optional) win/score** — no `resources_gte` predicate exists; either add one to `_PREDICATES` in `win_conditions.py` (one entry, pure: `c.signals.resources >= int(v)`) and expose `resources` on `EpisodeSignals`, or keep pass/fail on `cash_gte` and surface stored-value only in the leaderboard score. With S1, building silos genuinely changes optimal play (don't cap; don't over-invest in storage you can't fill), making §2(b) discriminating. --- ## 3. Broader brainstorm — 13 scenario ideas across capabilities Each: capability · real_world_meaning / robotics_analogue · win (real grammar) · predicates+commands · map · engine status. None duplicate the existing 13 packs. ### Perception **P1 — Resource-Patch Discovery Under Fog** - perception. RWM: locating *where* to forage before committing collectors is the real foraging problem, not the path to a known patch. RA: SLAM-time frontier selection for a resource map. - win: `all_of[explored_pct_gte:55, within_ticks:9000]` - uses `explored_pct_gte`,`within_ticks`; `move_units`,`observe`,`stop`. Map `rush-hour-arena`. - engine status: runnable now. **P2 — Count-the-Threats Before Acting** - perception. RWM: estimating force size from partial sightings before engaging is a recon-quality problem distinct from combat. RA: multi-target detection/enumeration under occlusion. - win: `all_of[enemies_discovered_gte:N, units_lost_lte:0, within_ticks:T]` (levels raise N, add decoy idle units, tighten T). - uses `enemies_discovered_gte`,`units_lost_lte`,`within_ticks`; `move_units`,`observe`. Map `rush-hour-arena`. - engine status: runnable now. **P3 — Identify the Production Building (signature ID)** - perception. RWM: classifying an installation by observable signature before acting on it. RA: semantic recognition of a target structure from sensor returns. - win: `all_of[buildings_discovered_gte:1, within_ticks:T]`; hard adds decoy buildings + tighter clock. - uses `buildings_discovered_gte`,`within_ticks`; `move_units`,`observe`. Map `rush-hour-arena`. - engine status: runnable now. ### Reasoning **R1 — Tech-Tree Critical Path** - reasoning. RWM: ordering prerequisite-coupled tasks (B needs power needs ...) to hit a deadline is scheduling, not building. RA: task-graph scheduling with hard precedence + a makespan deadline. - win: `all_of[has_building:tent, building_total_gte:4, power_surplus_gte:0, within_ticks:T]`; levels shorten T, raise depth. - uses `has_building`,`building_total_gte`,`power_surplus_gte`, `within_ticks`; `build`,`place_building`,`deploy`. `rush-hour-arena`. - engine status: runnable now. **R2 — Power-Budget Brownout Avoidance** - reasoning. RWM: keeping a power budget non-negative while expanding — an online resource-constraint problem. RA: a robot keeping net power ≥ 0 while bringing subsystems online. - win: `all_of[power_surplus_gte:0, building_total_gte:N, within_ticks:T]`; hard raises N with scarce power slack. - uses `power_surplus_gte`,`building_total_gte`,`within_ticks`; `build`,`place_building`,`power_down`. `rush-hour-arena`. - engine status: runnable now. **R3 — Defensive-Direction Commitment** - reasoning. RWM: committing limited defenses to the *right* approach under directional uncertainty. RA: sensor/effector placement toward the most likely intrusion bearing. - win: `all_of[building_in_region{type:pbox,x:40,y:20,radius:9, count:2}, within_ticks:T]`; levels move/duplicate the threat axis. - uses `building_in_region`,`within_ticks`; `build`,`place_building`. `rush-hour-arena`. - engine status: runnable now. **R4 — Economy-Time-Box** (§1) — reasoning — engine status: runnable now. **R5 — Economy-Investment (a)** (§2a) — reasoning — runnable now; full (b) **needs S0 + S1**. ### Action **A1 — Synchronised Multi-Squad Arrival** - action. RWM: executing a plan so dispersed agents *converge* simultaneously — coordination/execution, not planning. RA: multi-robot rendezvous with a timing constraint. - win: `all_of[all_units_in_region{x:90,y:20,radius:6}, units_lost_lte:0, within_ticks:T]`; levels split start positions wider, tighten T. - uses `all_units_in_region`,`units_lost_lte`,`within_ticks`; `move_units`,`attack_move`,`stop`. `rush-hour-arena`. - engine status: runnable now. **A2 — Sequenced Build-then-Relocate** - action. RWM: executing an ordered build→deploy→reposition sequence without dropping a step under a clock. RA: a fixed manipulation sequence (assemble → carry → place) executed to deadline. - win: `all_of[building_in_region{x:60,y:20,radius:10,count:2}, units_lost_lte:1, within_ticks:T]`. - uses `building_in_region`,`units_lost_lte`,`within_ticks`; `move_units`,`deploy`,`build`,`place_building`. `rush-hour-arena`. - engine status: runnable now. **A3 — Harvester Re-tasking Under Interruption** - action. RWM: keeping a foraging loop alive by promptly re-tasking collectors when the route is disrupted. RA: closed-loop recovery of a collection cycle after an interruption. - win: `all_of[harvesters_gte:1, cash_gte:N, within_ticks:T]` — only meaningful once harvesting earns. - uses `harvesters_gte`,`cash_gte`,`within_ticks`; `harvest`,`move_units`,`stop`. `rush-hour-arena`. - engine status: **needs S0** (no schema-valid ore source today). ### Economy / Tech / Multi-objective **E1 — Economy-Time-Box harvest variant** — see §1 — **needs S0** (+ S1 if silos are in the test). **E2 — Economy-Investment full** — see §2(b) — **needs S0 + S1**. **M1 — Earn-and-Survive (multi-objective)** - reasoning (multi-objective). RWM: holding an income loop *and* a defensive constraint simultaneously — competing objectives under one budget. RA: a robot foraging while maintaining a safety constraint. - win: `all_of[cash_gte:N, units_lost_lte:0, building_total_gte:K, within_ticks:T]` (the `cash_gte` arm is real income only post-S0; pre-S0 it degenerates to a spend test like §1). - uses `cash_gte`,`units_lost_lte`,`building_total_gte`,`within_ticks`; `harvest`,`build`,`move_units`. `rush-hour-arena`. - engine status: spend-only form runnable now; income form **needs S0**. --- ## Summary - **Runnable now:** P1, P2, P3, R1, R2, R3, R4 (economy-time-box), R5/§2a (economy-investment runnable variant), A1, A2, and the spend-only form of M1 — all expressible in the existing grammar on `rush-hour-arena`. - **Engine-prereq S0** (schema-valid ore source — a one-line whitelist add or a `resource_fields` scenario field; the Rust seed loop already exists): unblocks any *harvest income* scenario — A3, E1, the income form of M1, and the §2(b) allocation scoring. - **Engine-prereq S1** (silo storage: stop hardcoding `resource_capacity`, route deposits through a capped `resources` pool in `tick_harvesters`, recompute capacity from owned `silo`/`proc` buildings, optional `resources_gte` predicate): required only for the *silo* mechanic in §2(b). - **Final-economy-value scoring** (cash + asset value at deadline) is a desirable leaderboard metric, NOT a win predicate — there is no such leaf and one should not be assumed. ### Files written - `openra_bench/scenarios/packs/economy-time-box.yaml` — validator: `OK` - `openra_bench/scenarios/packs/economy-investment.yaml` — validator: `OK` - `SCENARIO_BRAINSTORM.md` (this file) --- ## Tech-Tree & Production Planning pack (design-doc Family 2) Pack: `openra_bench/scenarios/packs/tech-production-planning.yaml`. Maps the Benchmark Design Proposal's **Task Family 2 — Sequential Dependency Planning** (Scenarios 2A/2B/2C) directly into one bench pack. **Why a new pack rather than extending existing ones.** Two existing packs touch this territory but ladder on different axes: | Pack | Easy axis | Medium axis | Hard axis | Ladder shape | |---|---|---|---|---| | `building-and-planning` | 1-hop prereq (powr→tent) + building count | spatial direction (place pbox east) | MCV relocate / new base | building → spatial → mobility (changes axis per level) | | `strict-production-bom` | exact 3×e1 (no overproduction) | + 2×e3 | + tsla + tight budget | spec-fidelity (BFCL/τ²-bench style; orthogonal to dependency planning) | | **`tech-production-planning`** (new) | 2-hop chain `weap→fix→3tnk` | parallel branches (barr ‖ weap+fix) + `power_surplus_gte:0` forces 2nd powr | mid-game infantry tech committed → scout fog → pivot to anti-armor | **dependency depth → parallel breadth → adaptive replanning** (same axis at increasing complexity) | `building-and-planning` is mixed-axis (Family 2-ish at easy, drifts into Family 3/spatial later). `strict-production-bom` tests a different capability entirely (instruction-following fidelity, not dependency planning). The new pack is the first time Family 2 is ladded canonically on one axis end-to-end. **Real tech tree (engine ground truth from `openra_env/game_data.py`):** | Building | Prereqs | Power | |---|---|---| | `powr` | — | +100 | | `proc` (refinery) | `powr` | −30 | | `weap` (war factory) | `proc` | −30 | | `fix` (service depot) | `weap` | −30 | | `barr` (soviet barracks) | `powr` | −20 | | `3tnk` (Heavy Tank) | `weap` + `fix` | unit | | `e1` / `e3` (infantry) | `barr|tent` | unit | → Heavy Tank's real chain is `powr → proc → weap → fix → 3tnk` (4 prereqs deep). Building all of `proc + weap + fix + barr` drains to −10 power against a 100-power base — a SECOND powr is mandatory for medium. This is the real `power_surplus_gte:0` teeth at medium. **Difficulty calibration (one notch under doc spec).** Design doc 2A ("Construction Yard + 5000 credits → produce one Heavy Tank from scratch") is harder than every existing easy in the repo (~3× the work). Easy here pre-places `fact + powr + proc` so the chain becomes `weap → fix → 3tnk` (2-hop), aligning with `strict-production-bom` easy's "1 build + N train" workload. Medium and hard are calibrated similarly downward. Hard (adaptive transition under fog) is left at the design-doc difficulty because no existing pack tests online plan revision — this is the pack's core new contribution. ### Follow-ups (Design Proposal Families 4 / 5 / 7) — separate PRs Each likely needs new engine infrastructure that isn't yet wired into the bench: - **Family 4 — Adversarial Reasoning** (Beginner/Normal/Hard AI match). Existing `adversarial-*.yaml` packs are duels/sieges (combat encounters), not full RTS matches; `bot_type: ''` is empty across them, so a scripted opponent AI tier doesn't appear to be exposed yet. Needs `bot_type` integration + a "win full match" win predicate. - **Family 5 — Reactive Replanning** (Early threat / Multi-raid / Catastrophic). The `interrupts` field exists (`adversarial-duel` uses `enemy_unit_spotted` + `own_unit_destroyed`), but raid-generation hooks (mid-game enemy attack scripts) and an "infrastructure destroyed" trigger aren't represented in any pack. - **Family 7 — Holistic Strategic Intelligence** (full RTS match at 3 difficulties). Needs Family 4's infra (full-match predicate + AI opponent) plus long-horizon scoring beyond the existing rubric.