Spaces:
Running
Running
| """combat-harass-aggro-commit โ AGGRO variant of the harvester-harass | |
| triple. | |
| The bar: intended focus-defender-then-harv WINS on every level and | |
| every hard seed (1-4); STALL (only observe), RETREAT-ONLY (drive | |
| raiders back west), and ATTACK-HARV-IGNORE-DEFENDER (rush the harvs | |
| while standing in the 3tnk defender's range) all LOSE on every level | |
| and every hard seed โ with one documented exception: EASY allows | |
| attack-harv-only to squeak by (forgiving bare-skill tier with 4 | |
| raiders and a kill bar of 3). Non-win is a real reachable timeout | |
| LOSS via the `after_ticks` fail clause. | |
| Validation is scripted (no model / network). | |
| """ | |
| from __future__ import annotations | |
| from pathlib import Path | |
| import pytest | |
| pytest.importorskip("openra_rl_training", reason="Rust env wheel not installed") | |
| from openra_bench.scenarios import load_pack | |
| from openra_bench.scenarios.loader import compile_level | |
| from openra_bench.scenarios.win_conditions import WinContext, evaluate | |
| PACKS = Path(__file__).parent.parent / "openra_bench" / "scenarios" / "packs" | |
| PACK_PATH = PACKS / "combat-harass-aggro-commit.yaml" | |
| # โโ unit-level predicate / metadata checks (no engine) โโโโโโโโโโโโโโ | |
| def test_pack_compiles_and_meta_fields_populated(): | |
| pack = load_pack(PACK_PATH) | |
| assert pack.meta.id == "combat-harass-aggro-commit" | |
| assert pack.meta.capability == "action" | |
| assert pack.meta.real_world_meaning, "real_world_meaning required" | |
| assert pack.meta.robotics_analogue, "robotics_analogue required" | |
| anchors = pack.meta.benchmark_anchor | |
| assert isinstance(anchors, list) and len(anchors) == 4, ( | |
| f"benchmark_anchor must list all 4 anchors, got {anchors!r}" | |
| ) | |
| joined = " ".join(anchors).lower() | |
| for needle in ("sc2", "attrition", "aggro", "guerrilla"): | |
| assert needle in joined, f"missing anchor keyword: {needle}" | |
| for lvl in ("easy", "medium", "hard"): | |
| c = compile_level(pack, lvl) | |
| assert c.map_supported | |
| assert c.win_condition is not None | |
| assert c.fail_condition is not None | |
| def _ctx(*, units=(), tick=1000, kills=0, lost=0): | |
| """Synthesize a WinContext for predicate-level checks.""" | |
| import types | |
| sig = types.SimpleNamespace( | |
| game_tick=tick, | |
| units_killed=kills, | |
| units_lost=lost, | |
| cash=0, | |
| resources=0, | |
| own_buildings=[], | |
| own_building_types=set(), | |
| enemies_seen_ids=set(), | |
| enemy_buildings_seen_ids=set(), | |
| ) | |
| return WinContext( | |
| signals=sig, | |
| render_state={"units_summary": list(units)}, | |
| ) | |
| def _alive(n): | |
| return [{"cell_x": 10, "cell_y": 20, "type": "2tnk", "id": str(1000 + i)} for i in range(n)] | |
| def test_easy_predicates(): | |
| c = compile_level(load_pack(PACK_PATH), "easy") | |
| # Intended: kills 3, 1 raider alive, in time โ WIN | |
| assert evaluate(c.win_condition, _ctx(units=_alive(1), tick=2500, kills=3)) | |
| # Kill bar unmet (only 2 kills) โ not a win | |
| assert not evaluate(c.win_condition, _ctx(units=_alive(2), tick=2500, kills=2)) | |
| # All raiders dead โ fail (own_units_gte:1 trips via fail clause) | |
| assert evaluate(c.fail_condition, _ctx(units=[], tick=2500, kills=3)) | |
| # Timeout with bar unmet โ fail (after_ticks 4501) | |
| assert evaluate(c.fail_condition, _ctx(units=_alive(2), tick=4502, kills=2)) | |
| def test_medium_predicates(): | |
| c = compile_level(load_pack(PACK_PATH), "medium") | |
| # Intended: kills 4 (1 def + 3 harv), 1 raider alive โ WIN | |
| assert evaluate(c.win_condition, _ctx(units=_alive(1), tick=2500, kills=4)) | |
| # Bar unmet (only 3 kills โ partial sweep) โ not a win | |
| assert not evaluate(c.win_condition, _ctx(units=_alive(1), tick=2500, kills=3)) | |
| # Force wipe โ fail | |
| assert evaluate(c.fail_condition, _ctx(units=[], tick=2500, kills=4)) | |
| # Timeout with bar unmet โ fail | |
| assert evaluate(c.fail_condition, _ctx(units=_alive(2), tick=4502, kills=3)) | |
| def test_hard_predicates(): | |
| c = compile_level(load_pack(PACK_PATH), "hard") | |
| # Intended: kills 6 (2 def + 4 harv), 1 raider alive โ WIN | |
| assert evaluate(c.win_condition, _ctx(units=_alive(1), tick=3500, kills=6)) | |
| # Bar unmet โ not a win | |
| assert not evaluate(c.win_condition, _ctx(units=_alive(2), tick=3500, kills=5)) | |
| # Force wipe โ fail | |
| assert evaluate(c.fail_condition, _ctx(units=[], tick=3500, kills=6)) | |
| # Timeout โ fail | |
| assert evaluate(c.fail_condition, _ctx(units=_alive(1), tick=4502, kills=5)) | |
| def test_timeout_reachable_inside_max_turns(): | |
| """No draw degeneracy: after_ticks 4501 โค 93 + 90ยท(max_turns-1).""" | |
| pack = load_pack(PACK_PATH) | |
| for lvl in ("easy", "medium", "hard"): | |
| c = compile_level(pack, lvl) | |
| max_tick = 93 + 90 * (c.max_turns - 1) | |
| assert 4501 <= max_tick, ( | |
| f"{lvl}: after_ticks 4501 > max reachable tick {max_tick} " | |
| f"(max_turns={c.max_turns}); deadline never bites" | |
| ) | |
| assert 4500 <= max_tick, f"{lvl}: within_ticks 4500 > max tick {max_tick}" | |
| def test_hard_has_two_spawn_point_groups(): | |
| """Hard-tier curation: โฅ2 distinct agent spawn_point groups so the | |
| seed round-robins the west-edge corridor (north / south). Engine- | |
| roundtrip is asserted by tests/test_hard_tier.py.""" | |
| c = compile_level(load_pack(PACK_PATH), "hard") | |
| groups = { | |
| (a.spawn_point if a.spawn_point is not None else 0) | |
| for a in c.scenario.actors | |
| if a.owner == "agent" | |
| } | |
| assert len(groups) >= 2, f"hard needs โฅ2 spawn_point groups, got {groups}" | |
| def test_defender_is_3tnk_and_no_bot(): | |
| """The defender must be a 3tnk (heavier than 2tnk so a head-on | |
| 1-vs-1 trade loses); no bot is declared so retreat is safe but | |
| scoreless (the defender holds its post).""" | |
| pack = load_pack(PACK_PATH) | |
| enemy = pack.base.get("enemy") if isinstance(pack.base, dict) else None | |
| assert enemy is not None | |
| bot = enemy.get("bot_type") or enemy.get("bot") or "" | |
| assert bot == "", f"expected no bot (stationary defender), got {bot!r}" | |
| for lvl in ("easy", "medium", "hard"): | |
| c = compile_level(pack, lvl) | |
| types = [a.type for a in c.scenario.actors if a.owner == "enemy"] | |
| assert "3tnk" in types, f"{lvl}: defender must be 3tnk" | |
| assert "harv" in types, f"{lvl}: payload must include harv" | |
| # โโ engine-driven scripted policies โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _own_ids(rs): | |
| return [str(u["id"]) for u in (rs.get("units_summary", []) or [])] | |
| def _enemies_of_type(rs, want_types): | |
| out = [] | |
| for e in (rs.get("enemy_summary") or []): | |
| t = (e.get("type") or e.get("actor_type") or "").lower() | |
| if t in want_types: | |
| out.append(e) | |
| return out | |
| def _stall(rs, Command): | |
| """Pure observe โ defenders never engage, raiders never engage โ | |
| kill bar unmet โ after_ticks LOSS.""" | |
| return [Command.observe()] | |
| def _retreat(rs, Command): | |
| """Drive every raider back to the west spawn โ no engagement, no | |
| kills โ after_ticks LOSS. The defender is non-mobile so retreat | |
| is SAFE but scoreless (this is the discriminating LOSS: a model | |
| that picks survival over commitment fails the aggro bar).""" | |
| cmds = [] | |
| for uid in _own_ids(rs): | |
| cmds.append(Command.move_units([uid], 6, 20)) | |
| return cmds or [Command.observe()] | |
| def _attack_harv_only(rs, Command): | |
| """Rush the harvester cluster while ignoring the defender. With a | |
| heavier 3tnk in range, the raiders take fire on the approach and | |
| while engaging the soft targets โ kill ratio collapses before | |
| the bar is met โ LOSS on medium/hard. Easy is a forgiving tier | |
| where this play may squeak by.""" | |
| cmds = [] | |
| raiders = _own_ids(rs) | |
| for rid in raiders: | |
| cmds.append(Command.attack_move([rid], 80, 20)) | |
| return cmds or [Command.observe()] | |
| def _intended(rs, Command): | |
| """Focus-fire the visible defender 3tnk first with ALL raiders; | |
| once it's down, attack-move into the harv cluster. This is the | |
| aggro doctrine โ commit and trade favourably (3-vs-1 tank trade) | |
| on the high-value target before mopping up the payload.""" | |
| raiders = _own_ids(rs) | |
| if not raiders: | |
| return [Command.observe()] | |
| defenders = _enemies_of_type(rs, {"3tnk"}) | |
| if defenders: | |
| rxs = [u["cell_x"] for u in rs.get("units_summary", [])] | |
| rys = [u["cell_y"] for u in rs.get("units_summary", [])] | |
| cx, cy = sum(rxs) / len(rxs), sum(rys) / len(rys) | |
| defenders.sort( | |
| key=lambda e: (e["cell_x"] - cx) ** 2 + (e["cell_y"] - cy) ** 2 | |
| ) | |
| tid = defenders[0].get("id") | |
| if tid is not None: | |
| return [Command.attack_unit(raiders, str(tid))] | |
| harvs = _enemies_of_type(rs, {"harv"}) | |
| if harvs: | |
| tid = harvs[0].get("id") | |
| if tid is not None: | |
| return [Command.attack_unit(raiders, str(tid))] | |
| # No defenders / harvs in sight โ attack-move east into the cluster. | |
| return [Command.attack_move([rid], 80, 20) for rid in raiders] | |
| def test_intended_focus_defender_wins(level, seed): | |
| pytest.importorskip("openra_train") | |
| from openra_bench.eval_core import run_level | |
| c = compile_level(load_pack(PACK_PATH), level) | |
| r = run_level(c, _intended, seed=seed) | |
| assert r.outcome == "win", ( | |
| f"{level} seed={seed}: intended focus-defender-then-harv should " | |
| f"WIN, got {r.outcome} after {r.turns} turns " | |
| f"(kills={r.signals.units_killed}, losses={r.signals.units_lost})" | |
| ) | |
| def test_stall_loses(level, seed): | |
| pytest.importorskip("openra_train") | |
| from openra_bench.eval_core import run_level | |
| c = compile_level(load_pack(PACK_PATH), level) | |
| r = run_level(c, _stall, seed=seed) | |
| assert r.outcome == "loss", ( | |
| f"{level} seed={seed}: stall must be a real timeout LOSS " | |
| f"(no engagement โ kill bar unmet), got {r.outcome}" | |
| ) | |
| def test_retreat_only_loses(level, seed): | |
| """Pure retreat (drive all raiders back west) must LOSE on every | |
| tier โ the AGGRO doctrine specifically penalises survival-only | |
| play. The defender holds its post (no bot), so retreat is SAFE | |
| but scoreless โ after_ticks LOSS.""" | |
| pytest.importorskip("openra_train") | |
| from openra_bench.eval_core import run_level | |
| c = compile_level(load_pack(PACK_PATH), level) | |
| r = run_level(c, _retreat, seed=seed) | |
| assert r.outcome == "loss", ( | |
| f"{level} seed={seed}: retreat-only must LOSE (no kills โ bar " | |
| f"unmet), got {r.outcome} (kills={r.signals.units_killed})" | |
| ) | |
| def test_attack_harv_only_loses(level, seed): | |
| """Attack-harv-only (ignore the defender, rush harvs) must LOSE | |
| on medium and hard โ the 3tnk picks off the raiders while they | |
| engage the soft targets. Easy is excluded as the bare-skill tier | |
| (4 raiders + kill bar 3 is forgiving enough for this brute play | |
| to squeak by; documented in the pack's design comment, matches | |
| SCENARIO_REVIEW_CHECKLIST.md note that inert anti-cheat teeth | |
| are acceptable on easy).""" | |
| pytest.importorskip("openra_train") | |
| from openra_bench.eval_core import run_level | |
| c = compile_level(load_pack(PACK_PATH), level) | |
| r = run_level(c, _attack_harv_only, seed=seed) | |
| assert r.outcome == "loss", ( | |
| f"{level} seed={seed}: attack-harv-only must LOSE (defender " | |
| f"picks off raiders), got {r.outcome} " | |
| f"(kills={r.signals.units_killed}, losses={r.signals.units_lost})" | |
| ) | |