Spaces:
Running
Running
Hard-tier curation: seed-driven spawn variation (exemplar + invariant)
Browse filesDesign 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:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 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"
|