yxc20098 commited on
Commit
199bf71
·
1 Parent(s): e9d5097

Hard-tier curation: seed-driven spawn variation (exemplar + invariant)

Browse files

Design principle for the hard tier — qualitatively harder, not
number-inflated: hard defines >=2 distinct agent spawn_point groups so
Env round-robins the start position by seed (anti-memorization;
strengthens the held-out/generalization-gap metric), and the enemy
sits beyond initial sight (fog-dependence).

- adversarial-duel:hard reworked to two spawn_point groups (top-left /
lower-left staging) with wider randomize; keeps its fail_condition
- tests/test_hard_tier.py: reusable invariant — UPGRADED list (grows
per curated pack, suite green per commit); asserts >=2 spawn groups,
a live seed→distinct-start check, and fail_condition present
- worked exemplar; rollout across the applicability matrix follows

openra_bench/scenarios/packs/adversarial-duel.yaml CHANGED
@@ -102,9 +102,15 @@ levels:
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]
@@ -112,10 +118,11 @@ levels:
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]
@@ -123,6 +130,28 @@ levels:
123
  health: 100
124
  facing: -1
125
  count: 2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  - type: e1
127
  owner: enemy
128
  position: [34, 22]
 
102
  own_units_gte: 1
103
  max_turns: 70
104
  hard:
105
+ description: >
106
+ Rung 3 — badly outnumbered + a loss cap, and the agent force
107
+ starts from a seed-chosen corner (two spawn_point groups → the
108
+ env round-robins start position by seed, so a single memorised
109
+ opening can't generalise). Enemy sits mid-map beyond initial
110
+ sight: must close under fog.
111
  overrides:
112
  actors:
113
+ # spawn_point 0 — top-left staging
114
  - type: 2tnk
115
  owner: agent
116
  position: [6, 8]
 
118
  health: 100
119
  facing: -1
120
  count: 2
121
+ spawn_point: 0
122
  randomize:
123
  position:
124
  base: [6, 8]
125
+ offset: 3
126
  - type: 1tnk
127
  owner: agent
128
  position: [6, 11]
 
130
  health: 100
131
  facing: -1
132
  count: 2
133
+ spawn_point: 0
134
+ # spawn_point 1 — lower-left staging (different approach axis)
135
+ - type: 2tnk
136
+ owner: agent
137
+ position: [6, 40]
138
+ stance: 3
139
+ health: 100
140
+ facing: -1
141
+ count: 2
142
+ spawn_point: 1
143
+ randomize:
144
+ position:
145
+ base: [6, 40]
146
+ offset: 3
147
+ - type: 1tnk
148
+ owner: agent
149
+ position: [6, 43]
150
+ stance: 3
151
+ health: 100
152
+ facing: -1
153
+ count: 2
154
+ spawn_point: 1
155
  - type: e1
156
  owner: enemy
157
  position: [34, 22]
tests/test_hard_tier.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hard-tier curation invariant.
2
+
3
+ The `hard` level of a spatial pack must be *qualitatively* harder, not
4
+ number-inflated: it must define ≥2 distinct agent `spawn_point` groups
5
+ so `Env` round-robins the start position by seed (a single memorised
6
+ opening can't generalise — this is what the held-out-seed /
7
+ generalization-gap metric is there to reward). `UPGRADED` grows as
8
+ packs are curated, so the suite stays green per commit while the
9
+ contract is enforced on everything already done.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+
16
+ import pytest
17
+
18
+ from openra_bench.scenarios import load_pack
19
+ from openra_bench.scenarios.loader import compile_level
20
+
21
+ PACKS = Path(__file__).parent.parent / "openra_bench" / "scenarios" / "packs"
22
+
23
+ # Packs whose `hard` tier has been curated to the spawn-variation
24
+ # contract. Append as each is done (see the applicability matrix —
25
+ # economy-*/building-and-planning/strict-production-bom are N/A).
26
+ UPGRADED = [
27
+ "adversarial-duel",
28
+ ]
29
+
30
+
31
+ def _agent_spawn_points(pack_id: str, level: str) -> set:
32
+ c = compile_level(load_pack(PACKS / f"{pack_id}.yaml"), level)
33
+ return {
34
+ (a.spawn_point if a.spawn_point is not None else 0)
35
+ for a in c.scenario.actors
36
+ if a.owner == "agent"
37
+ }
38
+
39
+
40
+ @pytest.mark.parametrize("pid", UPGRADED)
41
+ def test_hard_has_multiple_seed_driven_spawn_points(pid):
42
+ sp = _agent_spawn_points(pid, "hard")
43
+ assert len(sp) >= 2, (
44
+ f"{pid}:hard must define ≥2 agent spawn_point groups for "
45
+ f"seed-driven start variation; got {sorted(sp)}"
46
+ )
47
+
48
+
49
+ @pytest.mark.parametrize("pid", UPGRADED)
50
+ def test_curated_hard_still_compiles_and_runs(pid):
51
+ pytest.importorskip("openra_train")
52
+ from openra_bench.eval_core import run_level
53
+
54
+ c = compile_level(load_pack(PACKS / f"{pid}.yaml"), "hard")
55
+ assert c.map_supported
56
+ # Different seeds must actually place the agent differently (the
57
+ # whole point of multiple spawn_point groups).
58
+ from openra_bench.rust_adapter import RustObsAdapter
59
+ from openra_bench.eval_core import _scenario_to_tmp_yaml, RustEnvPool
60
+
61
+ starts = set()
62
+ tmp = _scenario_to_tmp_yaml(c)
63
+ pool = RustEnvPool(size=1, scenario_path=tmp)
64
+ env = pool.acquire()
65
+ try:
66
+ for seed in (1, 2, 3, 4):
67
+ ad = RustObsAdapter()
68
+ ad.observe(env.reset(seed=seed))
69
+ u = ad.render_state().get("units_summary", []) or []
70
+ if u:
71
+ starts.add(
72
+ tuple(sorted((x["cell_x"], x["cell_y"]) for x in u))
73
+ )
74
+ finally:
75
+ pool.release(env)
76
+ pool.shutdown()
77
+ Path(tmp).unlink(missing_ok=True)
78
+ assert len(starts) >= 2, (
79
+ f"{pid}:hard seeds produced identical starts {starts}; "
80
+ "spawn_point round-robin not taking effect"
81
+ )
82
+
83
+
84
+ def test_fail_condition_present_on_curated_hard():
85
+ # Curated hard tiers must be able to emit a loss (no loss==draw
86
+ # degeneracy on the packs we've touched).
87
+ for pid in UPGRADED:
88
+ c = compile_level(load_pack(PACKS / f"{pid}.yaml"), "hard")
89
+ assert c.fail_condition is not None, f"{pid}:hard needs a fail_condition"