Spaces:
Running
Running
| """combat-focus-fire-priority โ focus-fire the high-DPS target FIRST. | |
| The bar: intended focus-e3-first WINS on every level and every hard | |
| seed (1-4); STALL, BRUTE attack_move, and SPREAD-attack-e1-first all | |
| LOSE on every level and every hard seed. Every enemy squad is a STATIC | |
| stance:2 (Defend) shield-wall-plus-rocket-rear formation, so the | |
| focus-fire priority is a real decision rather than a melee. Non-win is | |
| a real LOSS: stall via the `after_ticks` timeout fail clause, brute / | |
| spread via the ZERO `units_lost_lte` attrition cap. | |
| Recalibrated after the engine movement fixes (moving units take fire | |
| en route; attack_unit on an out-of-sight target paths at real speed). | |
| The strike force now stages at x=60 โ just outside the rocket rear- | |
| rank's Dragon range โ so a stall policy is never engaged (clean | |
| timeout LOSS) and the focus-fire policy spots the squad and opens fire | |
| from turn 1 instead of a 64-cell blind march under fire. The rocket | |
| rear-rank dropped to 2 (3+ e3 out-damage a 4-tank focus post-fix), the | |
| hard squad is compact so both seed-driven spawn corridors can reach | |
| the whole squad (no out-of-reach DRAW), and the easy attrition cap | |
| tightened to 0. | |
| 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-focus-fire-priority.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-focus-fire-priority" | |
| 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", "microrts", "strike-package", "rps-counter"): | |
| 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 7 (whole squad), ZERO tanks lost, in time โ WIN | |
| assert evaluate(c.win_condition, _ctx(units=_alive(4), tick=2000, kills=7, lost=0)) | |
| # Kill bar unmet (only 6 kills) โ not a win | |
| assert not evaluate(c.win_condition, _ctx(units=_alive(4), tick=2000, kills=6, lost=0)) | |
| # Attrition cap busted (any tank lost, cap is 0) โ fail; also not a win | |
| assert evaluate(c.fail_condition, _ctx(units=_alive(3), tick=2000, kills=7, lost=1)) | |
| assert not evaluate(c.win_condition, _ctx(units=_alive(3), tick=2000, kills=7, lost=1)) | |
| # All tanks dead โ fail (own_units_gte:1 trips via fail clause) | |
| assert evaluate(c.fail_condition, _ctx(units=[], tick=2000, kills=3, lost=4)) | |
| # Timeout with bar unmet โ fail (after_ticks 2701) | |
| assert evaluate(c.fail_condition, _ctx(units=_alive(4), tick=2702, kills=6, lost=0)) | |
| def test_medium_predicates(): | |
| c = compile_level(load_pack(PACK_PATH), "medium") | |
| # Intended: kills 10, 0 lost โ WIN | |
| assert evaluate(c.win_condition, _ctx(units=_alive(4), tick=2000, kills=10, lost=0)) | |
| # Bar unmet (only 9 kills) โ not a win | |
| assert not evaluate(c.win_condition, _ctx(units=_alive(4), tick=2000, kills=9, lost=0)) | |
| # Any tank lost (cap is 0) โ fail | |
| assert evaluate(c.fail_condition, _ctx(units=_alive(3), tick=2000, kills=10, lost=1)) | |
| # Force wipe โ fail | |
| assert evaluate(c.fail_condition, _ctx(units=[], tick=2000, kills=10, lost=4)) | |
| # Timeout โ fail | |
| assert evaluate(c.fail_condition, _ctx(units=_alive(4), tick=2702, kills=9, lost=0)) | |
| def test_hard_predicates(): | |
| c = compile_level(load_pack(PACK_PATH), "hard") | |
| # Intended: kills 14, 0 lost โ WIN | |
| assert evaluate(c.win_condition, _ctx(units=_alive(4), tick=2000, kills=14, lost=0)) | |
| # Bar unmet โ not a win | |
| assert not evaluate(c.win_condition, _ctx(units=_alive(4), tick=2000, kills=13, lost=0)) | |
| # Any tank lost (cap is 0) โ fail | |
| assert evaluate(c.fail_condition, _ctx(units=_alive(3), tick=2000, kills=14, lost=1)) | |
| # Force wipe โ fail | |
| assert evaluate(c.fail_condition, _ctx(units=[], tick=2000, kills=14, lost=4)) | |
| # Timeout โ fail | |
| assert evaluate(c.fail_condition, _ctx(units=_alive(4), tick=2702, kills=13, lost=0)) | |
| def test_timeout_reachable_inside_max_turns(): | |
| """No draw degeneracy: after_ticks 2701 โค 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 2701 <= max_tick, ( | |
| f"{lvl}: after_ticks 2701 > max reachable tick {max_tick} " | |
| f"(max_turns={c.max_turns}); deadline never bites" | |
| ) | |
| assert 2700 <= max_tick, ( | |
| f"{lvl}: within_ticks 2700 > max reachable 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_enemy_squad_has_rocket_soldier_and_rifle_infantry(): | |
| """The priority threat (e3 = anti-tank rocket soldier) must be | |
| present on every level alongside rifle infantry (e1) escorts; | |
| that contrast is what makes target prioritization the | |
| load-bearing capability of the pack.""" | |
| pack = load_pack(PACK_PATH) | |
| for lvl in ("easy", "medium", "hard"): | |
| c = compile_level(pack, lvl) | |
| types = [a.type for a in c.scenario.actors if a.owner == "enemy"] | |
| n_e3 = sum(1 for t in types if t == "e3") | |
| n_e1 = sum(1 for t in types if t == "e1") | |
| assert n_e3 >= 1, f"{lvl}: must include โฅ1 e3 (priority threat); got {types}" | |
| assert n_e1 >= 2, f"{lvl}: must include โฅ2 e1 (escort); got {types}" | |
| # Persistent far enemy marker (engine auto-done mitigation). | |
| assert "fact" in types, f"{lvl}: needs a persistent enemy fact" | |
| # โโ 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 โ the enemy squad is static (stance:2 Defend, never | |
| advances), the tanks never engage โ kill bar unmet โ after_ticks | |
| LOSS.""" | |
| return [Command.observe()] | |
| def _brute_attack_move(rs, Command): | |
| """Brute: every tank attack_moves toward the enemy cluster centre. | |
| The engine auto-targets the NEAREST visible hostile, which spreads | |
| fire across the front-line e1s. The rocket-soldier e3 stays alive | |
| long enough to bust the attrition cap โ LOSS on medium/hard.""" | |
| cmds = [] | |
| for uid in _own_ids(rs): | |
| cmds.append(Command.attack_move([uid], 71, 20)) | |
| return cmds or [Command.observe()] | |
| def _spread_attack_e1_first(rs, Command): | |
| """Explicitly target an e1 first (priority inversion). Each turn | |
| re-target the closest visible e1; only fall through to the e3 if | |
| no e1s remain. The e3 keeps firing for the entire e1 mop-up and | |
| kills tanks โ busts attrition cap โ LOSS.""" | |
| own = _own_ids(rs) | |
| if not own: | |
| return [Command.observe()] | |
| e1s = _enemies_of_type(rs, {"e1"}) | |
| if e1s: | |
| rxs = [u["cell_x"] for u in rs.get("units_summary", [])] | |
| rys = [u["cell_y"] for u in rs.get("units_summary", [])] | |
| cx = sum(rxs) / len(rxs) | |
| cy = sum(rys) / len(rys) | |
| e1s.sort(key=lambda e: (e["cell_x"] - cx) ** 2 + (e["cell_y"] - cy) ** 2) | |
| tid = e1s[0].get("id") | |
| if tid is not None: | |
| return [Command.attack_unit(own, str(tid))] | |
| e3s = _enemies_of_type(rs, {"e3"}) | |
| if e3s: | |
| tid = e3s[0].get("id") | |
| if tid is not None: | |
| return [Command.attack_unit(own, str(tid))] | |
| # Nothing in sight โ advance to contact. | |
| return [Command.attack_move([uid], 71, 20) for uid in own] | |
| def _intended_focus_e3(rs, Command): | |
| """Focus-fire the visible rocket soldier (e3) FIRST with ALL tanks | |
| (4-vs-1 concentrated fire kills it in 1-2 decision ticks); once | |
| all e3s are down, mop up the rifle infantry. This is the focus- | |
| fire doctrine โ kill the high-DPS threat before it can score | |
| tank kills.""" | |
| own = _own_ids(rs) | |
| if not own: | |
| return [Command.observe()] | |
| e3s = _enemies_of_type(rs, {"e3"}) | |
| if e3s: | |
| rxs = [u["cell_x"] for u in rs.get("units_summary", [])] | |
| rys = [u["cell_y"] for u in rs.get("units_summary", [])] | |
| cx = sum(rxs) / len(rxs) | |
| cy = sum(rys) / len(rys) | |
| e3s.sort(key=lambda e: (e["cell_x"] - cx) ** 2 + (e["cell_y"] - cy) ** 2) | |
| tid = e3s[0].get("id") | |
| if tid is not None: | |
| return [Command.attack_unit(own, str(tid))] | |
| e1s = _enemies_of_type(rs, {"e1"}) | |
| if e1s: | |
| tid = e1s[0].get("id") | |
| if tid is not None: | |
| return [Command.attack_unit(own, str(tid))] | |
| # No targets in sight โ advance east toward the cluster centre. | |
| return [Command.attack_move([uid], 71, 20) for uid in own] | |
| def test_intended_focus_e3_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_focus_e3, seed=seed) | |
| assert r.outcome == "win", ( | |
| f"{level} seed={seed}: intended focus-e3-first should WIN, " | |
| f"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_brute_attack_move_loses(level, seed): | |
| """Brute attack_move (auto-target the nearest hostile) must LOSE | |
| on every level โ the engine spreads fire across the e1 shield | |
| line and the static rocket rear-rank picks off enough tanks to | |
| bust the attrition cap before the kill bar is met.""" | |
| pytest.importorskip("openra_train") | |
| from openra_bench.eval_core import run_level | |
| c = compile_level(load_pack(PACK_PATH), level) | |
| r = run_level(c, _brute_attack_move, seed=seed) | |
| assert r.outcome == "loss", ( | |
| f"{level} seed={seed}: brute attack_move must LOSE (e3 picks " | |
| f"off tanks via spread fire), got {r.outcome} " | |
| f"(kills={r.signals.units_killed}, losses={r.signals.units_lost})" | |
| ) | |
| def test_spread_attack_e1_first_loses(level, seed): | |
| """Spread / priority-inverted: attack the rifle infantry first | |
| (the close, soft targets). The static rocket soldiers keep firing | |
| through the entire e1 mop-up and kill enough tanks to bust the | |
| attrition cap before the kill bar is met โ LOSS on every level.""" | |
| pytest.importorskip("openra_train") | |
| from openra_bench.eval_core import run_level | |
| c = compile_level(load_pack(PACK_PATH), level) | |
| r = run_level(c, _spread_attack_e1_first, seed=seed) | |
| assert r.outcome == "loss", ( | |
| f"{level} seed={seed}: e1-first must LOSE (e3 alive too long " | |
| f"โ tank kills), got {r.outcome} " | |
| f"(kills={r.signals.units_killed}, losses={r.signals.units_lost})" | |
| ) | |