yiyu-tian commited on
Commit
7c62ead
·
unverified ·
2 Parent(s): 50fe83f9afe0cd

Merge pull request #5 from yiyu-tian/scenarios/tech-production-planning

Browse files
SCENARIO_BRAINSTORM.md CHANGED
@@ -341,3 +341,74 @@ full (b) **needs S0 + S1**.
341
  - `openra_bench/scenarios/packs/economy-time-box.yaml` — validator: `OK`
342
  - `openra_bench/scenarios/packs/economy-investment.yaml` — validator: `OK`
343
  - `SCENARIO_BRAINSTORM.md` (this file)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
  - `openra_bench/scenarios/packs/economy-time-box.yaml` — validator: `OK`
342
  - `openra_bench/scenarios/packs/economy-investment.yaml` — validator: `OK`
343
  - `SCENARIO_BRAINSTORM.md` (this file)
344
+
345
+ ---
346
+
347
+ ## Tech-Tree & Production Planning pack (design-doc Family 2)
348
+
349
+ Pack: `openra_bench/scenarios/packs/tech-production-planning.yaml`. Maps
350
+ the Benchmark Design Proposal's **Task Family 2 — Sequential Dependency
351
+ Planning** (Scenarios 2A/2B/2C) directly into one bench pack.
352
+
353
+ **Why a new pack rather than extending existing ones.** Two existing
354
+ packs touch this territory but ladder on different axes:
355
+
356
+ | Pack | Easy axis | Medium axis | Hard axis | Ladder shape |
357
+ |---|---|---|---|---|
358
+ | `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) |
359
+ | `strict-production-bom` | exact 3×e1 (no overproduction) | + 2×e3 | + tsla + tight budget | spec-fidelity (BFCL/τ²-bench style; orthogonal to dependency planning) |
360
+ | **`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) |
361
+
362
+ `building-and-planning` is mixed-axis (Family 2-ish at easy, drifts into
363
+ Family 3/spatial later). `strict-production-bom` tests a different
364
+ capability entirely (instruction-following fidelity, not dependency
365
+ planning). The new pack is the first time Family 2 is ladded canonically
366
+ on one axis end-to-end.
367
+
368
+ **Real tech tree (engine ground truth from `openra_env/game_data.py`):**
369
+
370
+ | Building | Prereqs | Power |
371
+ |---|---|---|
372
+ | `powr` | — | +100 |
373
+ | `proc` (refinery) | `powr` | −30 |
374
+ | `weap` (war factory) | `proc` | −30 |
375
+ | `fix` (service depot) | `weap` | −30 |
376
+ | `barr` (soviet barracks) | `powr` | −20 |
377
+ | `3tnk` (Heavy Tank) | `weap` + `fix` | unit |
378
+ | `e1` / `e3` (infantry) | `barr|tent` | unit |
379
+
380
+ → Heavy Tank's real chain is `powr → proc → weap → fix → 3tnk` (4
381
+ prereqs deep). Building all of `proc + weap + fix + barr` drains to −10
382
+ power against a 100-power base — a SECOND powr is mandatory for medium.
383
+ This is the real `power_surplus_gte:0` teeth at medium.
384
+
385
+ **Difficulty calibration (one notch under doc spec).** Design doc 2A
386
+ ("Construction Yard + 5000 credits → produce one Heavy Tank from
387
+ scratch") is harder than every existing easy in the repo (~3× the
388
+ work). Easy here pre-places `fact + powr + proc` so the chain becomes
389
+ `weap → fix → 3tnk` (2-hop), aligning with `strict-production-bom`
390
+ easy's "1 build + N train" workload. Medium and hard are calibrated
391
+ similarly downward. Hard (adaptive transition under fog) is left at the
392
+ design-doc difficulty because no existing pack tests online plan
393
+ revision — this is the pack's core new contribution.
394
+
395
+ ### Follow-ups (Design Proposal Families 4 / 5 / 7) — separate PRs
396
+
397
+ Each likely needs new engine infrastructure that isn't yet wired into
398
+ the bench:
399
+
400
+ - **Family 4 — Adversarial Reasoning** (Beginner/Normal/Hard AI
401
+ match). Existing `adversarial-*.yaml` packs are duels/sieges (combat
402
+ encounters), not full RTS matches; `bot_type: ''` is empty across
403
+ them, so a scripted opponent AI tier doesn't appear to be exposed
404
+ yet. Needs `bot_type` integration + a "win full match" win predicate.
405
+
406
+ - **Family 5 — Reactive Replanning** (Early threat / Multi-raid /
407
+ Catastrophic). The `interrupts` field exists (`adversarial-duel`
408
+ uses `enemy_unit_spotted` + `own_unit_destroyed`), but
409
+ raid-generation hooks (mid-game enemy attack scripts) and an
410
+ "infrastructure destroyed" trigger aren't represented in any pack.
411
+
412
+ - **Family 7 — Holistic Strategic Intelligence** (full RTS match at 3
413
+ difficulties). Needs Family 4's infra (full-match predicate + AI
414
+ opponent) plus long-horizon scoring beyond the existing rubric.
openra_bench/scenarios/packs/tech-production-planning.yaml ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ meta:
2
+ id: tech-production-planning
3
+ title: 'Tech-Tree & Production — Sequential Dependency Planning'
4
+ capability: reasoning
5
+ real_world_meaning: >
6
+ Sequential dependency planning under hard precedence + power + budget
7
+ constraints. Easy is a 2-hop prereq chain (build weap → fix → train
8
+ one Heavy Tank). Medium forces parallel branches (infantry queue at
9
+ barr AND vehicle queue at weap+fix) under a power brownout that
10
+ requires a second power plant. Hard requires online plan revision:
11
+ the agent starts mid-game with infantry tech committed, must scout
12
+ through fog to reveal that the enemy is heavy armor, and pivot
13
+ production to anti-armor (Rocket Soldier) before the deadline. The
14
+ decision being tested is build-order scheduling under precedence,
15
+ not motor control of any single build.
16
+ robotics_analogue: >
17
+ Task-graph scheduling with multi-hop precedence and resource
18
+ constraints, escalating to mid-execution replanning when discovered
19
+ facts invalidate the initial plan — PlanBench Mystery-Blocksworld
20
+ (precedence) plus an online-revision twist after partial observation.
21
+ author: openra-bench
22
+
23
+ base_map: rush-hour-arena
24
+
25
+ base:
26
+ agent:
27
+ faction: soviet
28
+ cash: 0
29
+ enemy:
30
+ faction: allies
31
+ cash: 0
32
+ bot_type: ''
33
+ tools:
34
+ - build
35
+ - place_building
36
+ - train
37
+ - cancel_production
38
+ - move_units
39
+ - deploy
40
+ - stop
41
+ - set_rally_point
42
+ planning: true
43
+ termination:
44
+ max_ticks: 40000
45
+ actors:
46
+ - {type: fact, owner: agent, position: [10, 18]}
47
+ - {type: powr, owner: agent, position: [14, 18]}
48
+
49
+ levels:
50
+ easy:
51
+ description: >
52
+ Simple Tech Chain — fact, powr, and proc are pre-placed; build
53
+ War Factory → Service Depot, then train one Heavy Tank. The
54
+ decision is recognising 3tnk needs weap + fix in that order; the
55
+ budget covers the chain with modest slack. Tests the agent can
56
+ sequence a deep enough prereq chain (proc→weap→fix→3tnk) without
57
+ attempting to train 3tnk before fix exists.
58
+ starting_cash: 5000
59
+ overrides:
60
+ actors:
61
+ - {type: fact, owner: agent, position: [10, 18]}
62
+ - {type: powr, owner: agent, position: [14, 18]}
63
+ - {type: proc, owner: agent, position: [16, 22]}
64
+ - {type: mine, owner: agent, position: [20, 24]}
65
+ - {type: e1, owner: enemy, position: [114, 34], stance: 2}
66
+ win_condition:
67
+ all_of:
68
+ - unit_type_count_gte: {type: 3tnk, n: 1}
69
+ - building_count_gte: {type: fix, n: 1}
70
+ - within_ticks: 18000
71
+ max_turns: 60
72
+
73
+ medium:
74
+ description: >
75
+ Parallel Dependency Optimization — only fact + powr pre-placed.
76
+ Produce one Heavy Tank PLUS one Rocket Soldier PLUS three Rifle
77
+ Infantry, with power_surplus ≥ 0 at the deadline. The combined
78
+ build (proc + barr + weap + fix = -110 power) drains net power
79
+ below zero against a 100-power base, so a SECOND power plant is
80
+ mandatory. Tight clock forces queueing the infantry branch (barr)
81
+ in parallel with the vehicle branch (weap+fix); serial production
82
+ blows the clock on idle time.
83
+ starting_cash: 6500
84
+ overrides:
85
+ actors:
86
+ - {type: fact, owner: agent, position: [10, 18]}
87
+ - {type: powr, owner: agent, position: [14, 18]}
88
+ - {type: mine, owner: agent, position: [20, 24]}
89
+ - {type: e1, owner: enemy, position: [114, 34], stance: 2}
90
+ win_condition:
91
+ all_of:
92
+ - unit_type_count_gte: {type: 3tnk, n: 1}
93
+ - unit_type_count_gte: {type: e3, n: 1}
94
+ - unit_type_count_gte: {type: e1, n: 3}
95
+ - power_surplus_gte: 0
96
+ - within_ticks: 22000
97
+ max_turns: 80
98
+
99
+ hard:
100
+ description: >
101
+ Adaptive Tech Transition — mid-game start: fact + powr + proc +
102
+ barr and three Rifle Infantry are already on the field (an
103
+ infantry build committed). Three enemy Heavy Tanks sit passive on
104
+ the east side, hidden by fog. The agent must scout, recognise
105
+ that infantry is the wrong counter to heavy armor, and pivot
106
+ production to FIVE Rocket Soldiers (e3, anti-armor) before the
107
+ deadline. Overproducing infantry past ten = fail (doubling down
108
+ on the wrong plan exhausts the budget). The transition — cancel
109
+ planned infantry, fund anti-armor, scout — is the test.
110
+ starting_cash: 3000
111
+ overrides:
112
+ actors:
113
+ - {type: fact, owner: agent, position: [10, 18]}
114
+ - {type: powr, owner: agent, position: [14, 18]}
115
+ - {type: proc, owner: agent, position: [16, 22]}
116
+ - {type: barr, owner: agent, position: [12, 21]}
117
+ - {type: e1, owner: agent, position: [8, 16], count: 3}
118
+ - {type: mine, owner: agent, position: [20, 24]}
119
+ - {type: 3tnk, owner: enemy, position: [110, 34], stance: 2, count: 3}
120
+ win_condition:
121
+ all_of:
122
+ - enemies_discovered_gte: 2
123
+ - unit_type_count_gte: {type: e3, n: 5}
124
+ - within_ticks: 14000
125
+ fail_condition:
126
+ unit_type_count_gte: {type: e1, n: 10}
127
+ max_turns: 90
tests/test_tech_production_planning.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """tech-production-planning pack — full loop on Rust.
2
+
3
+ Canonical Family 2 (Sequential Dependency Planning) from the Benchmark
4
+ Design Proposal: easy = 2-hop chain (weap -> fix -> 3tnk), medium =
5
+ parallel branches under power_surplus_gte:0, hard = adaptive transition.
6
+
7
+ A scripted "tech-planner" agent walks the easy chain (build War Factory
8
+ -> Service Depot -> train one Heavy Tank) and proves the declarative
9
+ win_condition fires; medium/hard run deterministically with the same
10
+ agent (they need richer play than a scripted depth-first chain to win,
11
+ so we assert determinism + budget bound, mirroring test_building_planning).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import pytest
17
+
18
+ pytest.importorskip("openra_train", reason="Rust env wheel not installed")
19
+
20
+ from openra_bench.eval_core import run_level
21
+ from openra_bench.scenarios import load_pack
22
+ from openra_bench.scenarios.loader import PACKS_DIR, compile_level
23
+ from openra_bench.scoring import score_episode
24
+
25
+ PACK = PACKS_DIR / "tech-production-planning.yaml"
26
+
27
+
28
+ def tech_planner_agent(render_state, Command):
29
+ """Walk the 3tnk prereq chain depth-first.
30
+
31
+ Pre-placed: fact, powr, proc. Build weap -> fix, then train one 3tnk.
32
+ `build` queues the item; `place_building` no-ops until the building
33
+ is ready to be placed.
34
+ """
35
+ btypes = [b["type"] for b in render_state.get("own_buildings", [])]
36
+ producing = render_state.get("production", []) or []
37
+ units = [u["type"] for u in render_state.get("units_summary", [])]
38
+
39
+ if "weap" not in btypes:
40
+ cmds = []
41
+ if "weap" not in producing:
42
+ cmds.append(Command.build("weap"))
43
+ cmds.append(Command.place_building("weap", 18, 22))
44
+ return cmds
45
+ if "fix" not in btypes:
46
+ cmds = []
47
+ if "fix" not in producing:
48
+ cmds.append(Command.build("fix"))
49
+ cmds.append(Command.place_building("fix", 22, 22))
50
+ return cmds
51
+ if "3tnk" not in units:
52
+ if "3tnk" not in producing:
53
+ return [Command.build("3tnk")]
54
+ return [Command.observe()]
55
+ return [Command.observe()]
56
+
57
+
58
+ def test_pack_compiles_with_three_levels():
59
+ pack = load_pack(PACK)
60
+ assert pack.meta.id == "tech-production-planning"
61
+ assert pack.meta.capability == "reasoning"
62
+ assert set(pack.levels) == {"easy", "medium", "hard"}
63
+
64
+
65
+ def test_easy_tech_chain_wins_and_scores():
66
+ pack = load_pack(PACK)
67
+ c = compile_level(pack, "easy")
68
+ assert c.map_supported
69
+ assert c.starting_cash == 5000
70
+
71
+ res = run_level(c, tech_planner_agent, seed=1)
72
+
73
+ # The chain went through both weap AND fix (the prereq for 3tnk).
74
+ btypes = res.signals.own_building_types
75
+ assert "weap" in btypes and "fix" in btypes, (
76
+ f"chain incomplete; own_building_types={btypes}"
77
+ )
78
+ # The agent actually trained a Heavy Tank.
79
+ unit_types = [u["type"] for u in res.signals.units_summary]
80
+ assert "3tnk" in unit_types, f"no 3tnk produced; units={unit_types}"
81
+ # Production debited the budget.
82
+ assert res.signals.cash < 5000, "construction must debit the budget"
83
+ assert res.outcome == "win", f"easy should be winnable, got {res.outcome}"
84
+
85
+ sc = score_episode(c, res)
86
+ assert sc.outcome == "win" and 0.0 <= sc.composite <= 1.0
87
+
88
+
89
+ def test_easy_run_is_deterministic():
90
+ c = compile_level(load_pack(PACK), "easy")
91
+ a = run_level(c, tech_planner_agent, seed=7)
92
+ b = run_level(c, tech_planner_agent, seed=7)
93
+ assert (a.outcome, a.turns, a.signals.cash) == (
94
+ b.outcome,
95
+ b.turns,
96
+ b.signals.cash,
97
+ ), "same seed must be deterministic"
98
+
99
+
100
+ @pytest.mark.parametrize("level", ["medium", "hard"])
101
+ def test_medium_hard_run_and_score_within_constraints(level):
102
+ """Harder levels need richer play than the scripted depth-first chain
103
+ (medium wants parallel queues + 2nd powr; hard wants scouting +
104
+ pivot to anti-armor). Assert they execute within declared constraints
105
+ and score deterministically — the contributor loop must be robust
106
+ even on a loss."""
107
+ pack = load_pack(PACK)
108
+ c = compile_level(pack, level)
109
+ a = run_level(c, tech_planner_agent, seed=3)
110
+ b = run_level(c, tech_planner_agent, seed=3)
111
+ assert a.outcome in {"win", "draw", "loss"}
112
+ assert (a.outcome, a.turns, a.signals.cash) == (
113
+ b.outcome,
114
+ b.turns,
115
+ b.signals.cash,
116
+ ), "same seed must yield identical outcome"
117
+ sc = score_episode(c, a)
118
+ assert 0.0 <= sc.composite <= 1.0
119
+ # Budget constraint enforced by the engine.
120
+ assert a.signals.cash <= c.starting_cash
121
+
122
+
123
+ def test_hard_fail_predicate_set():
124
+ """Hard declares overproducing infantry past 10 = fail. Confirm the
125
+ fail_condition is wired through compile (the engine actually checks
126
+ it). Schema-level check, no episode run required."""
127
+ c = compile_level(load_pack(PACK), "hard")
128
+ assert c.fail_condition is not None
129
+ # The fail leaf is unit_type_count_gte on e1 at n=10.
130
+ fc = c.fail_condition.model_dump(exclude_none=True)
131
+ assert fc == {"unit_type_count_gte": {"type": "e1", "n": 10}}, fc