Spaces:
Running
Running
| """mcv-deploy-second-base — multi-site ramp-up via 2nd MCV. | |
| The bar: the intended drive-MCV-to-eastern-target-and-deploy policy | |
| WINS on every level and every hard seed; stall (only observe), | |
| deploy-next-to-existing-base (just deploy in place at the center, | |
| keeping the new fact OUTSIDE the eastern region), and on hard, | |
| deploy-in-the-wrong-corner (cross the map to the FAR candidate | |
| region — fails the tick budget and/or eats the wrong-corner patrol) | |
| all LOSE on every level. Non-win is a real reachable timeout LOSS. | |
| Validation is scripted (no model / network). | |
| """ | |
| from __future__ import annotations | |
| import types | |
| 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 / "mcv-deploy-second-base.yaml" | |
| # ── unit-level predicate checks ────────────────────────────────────── | |
| def _ctx(own_buildings=(), tick=1000): | |
| """Synthesize a WinContext from a building list. | |
| own_buildings: iterable of (type, cell_x, cell_y). | |
| """ | |
| sig = types.SimpleNamespace( | |
| game_tick=tick, | |
| units_killed=0, | |
| units_lost=0, | |
| own_buildings=list(own_buildings), | |
| own_building_types={str(t).lower() for (t, _, _) in own_buildings}, | |
| enemies_seen_ids=set(), | |
| enemy_buildings_seen_ids=set(), | |
| ) | |
| return WinContext(signals=sig, render_state={"units_summary": []}) | |
| def test_predicates_easy(): | |
| c = compile_level(load_pack(PACK_PATH), "easy") | |
| base1 = ("fact", 15, 20) | |
| # WIN: 2 facts, one at east (90,20) within radius 8 — in time. | |
| east = ("fact", 90, 20) | |
| assert evaluate(c.win_condition, _ctx([base1, east], tick=3000)) | |
| # FAIL: only 1 fact (never deployed) | |
| assert not evaluate(c.win_condition, _ctx([base1], tick=3000)) | |
| # FAIL: 2 facts but second one is in the CENTER (next to MCV start | |
| # ~ (60,20)) — well outside the east region radius-8. | |
| next_to_mcv = ("fact", 60, 20) | |
| assert not evaluate(c.win_condition, _ctx([base1, next_to_mcv], tick=3000)) | |
| # FAIL: 2 facts but second one is in NW corner (wrong region). | |
| nw = ("fact", 20, 5) | |
| assert not evaluate(c.win_condition, _ctx([base1, nw], tick=3000)) | |
| # Past deadline ⇒ fail. | |
| assert evaluate(c.fail_condition, _ctx([base1, east], tick=5402)) | |
| # No fact at all ⇒ fail (lost base #1 AND never deployed). | |
| assert evaluate(c.fail_condition, _ctx([], tick=3000)) | |
| # Deadline reachable inside max_turns (∼90 ticks/turn). | |
| assert 5401 <= 93 + 90 * (c.max_turns - 1), ( | |
| "after_ticks 5401 must be reachable within max_turns" | |
| ) | |
| def test_predicates_medium(): | |
| c = compile_level(load_pack(PACK_PATH), "medium") | |
| base1 = ("fact", 15, 20) | |
| east = ("fact", 92, 18) # inside radius-8 around (90,20) | |
| assert evaluate(c.win_condition, _ctx([base1, east], tick=2500)) | |
| # Just outside the radius | |
| outside = ("fact", 99, 20) # distance 9 > 8 | |
| assert not evaluate(c.win_condition, _ctx([base1, outside], tick=2500)) | |
| # Timeout reachable | |
| assert 4501 <= 93 + 90 * (c.max_turns - 1) | |
| def test_predicates_hard_either_corner_wins(): | |
| c = compile_level(load_pack(PACK_PATH), "hard") | |
| base1 = ("fact", 15, 20) | |
| ne = ("fact", 95, 10) | |
| se = ("fact", 95, 30) | |
| # WIN: deploy at NE candidate region | |
| assert evaluate(c.win_condition, _ctx([base1, ne], tick=2000)) | |
| # WIN: deploy at SE candidate region | |
| assert evaluate(c.win_condition, _ctx([base1, se], tick=2000)) | |
| # FAIL: deploy at center (neither candidate) | |
| center = ("fact", 60, 20) | |
| assert not evaluate(c.win_condition, _ctx([base1, center], tick=2000)) | |
| # FAIL: deploy next to base #1 (still outside both candidate regions) | |
| near_base = ("fact", 22, 20) | |
| assert not evaluate(c.win_condition, _ctx([base1, near_base], tick=2000)) | |
| # Past deadline ⇒ fail. | |
| assert evaluate(c.fail_condition, _ctx([base1, ne], tick=3802)) | |
| # Deadline reachable | |
| assert 3801 <= 93 + 90 * (c.max_turns - 1) | |
| def test_hard_has_two_spawn_point_groups(): | |
| """Hard-tier curation contract: ≥2 distinct agent spawn_point | |
| groups so the seed round-robins the staging latitude (NE vs SE | |
| MCV).""" | |
| 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_pack_compiles_and_meta_fields_populated(): | |
| pack = load_pack(PACK_PATH) | |
| assert pack.meta.capability == "reasoning" | |
| assert pack.meta.id == "mcv-deploy-second-base" | |
| anchors = pack.meta.benchmark_anchor | |
| assert isinstance(anchors, list) and anchors, "benchmark_anchor required" | |
| joined = " ".join(anchors).lower() | |
| assert "sc2le" in joined and "microrts" in joined | |
| assert "geographic" in joined or "multi-site" in joined | |
| for lvl in ("easy", "medium", "hard"): | |
| c = compile_level(pack, lvl) | |
| assert c.map_supported | |
| assert c.win_condition is not None and c.fail_condition is not None | |
| # ── engine-driven scripted policies ────────────────────────────────── | |
| def _find_mcv(rs): | |
| for u in rs.get("units_summary", []) or []: | |
| if str(u.get("type", "")).lower() == "mcv": | |
| return u | |
| return None | |
| def _nearest_target(mcv, level): | |
| """Return (tx, ty) the deploy-region center the MCV is nearer to.""" | |
| if level != "hard": | |
| return (90, 20) | |
| if mcv is None: | |
| return (95, 10) | |
| # On hard, NE (95,10) vs SE (95,30); pick nearer to MCV start. | |
| dx_n = (mcv["cell_x"] - 95) ** 2 + (mcv["cell_y"] - 10) ** 2 | |
| dx_s = (mcv["cell_x"] - 95) ** 2 + (mcv["cell_y"] - 30) ** 2 | |
| return (95, 10) if dx_n <= dx_s else (95, 30) | |
| def _intended_policy_for(level): | |
| """Drive the MCV to the (nearest) target region; deploy when in | |
| range. After deploy the MCV is consumed and the episode just | |
| observes.""" | |
| def _policy(rs, Command): | |
| mcv = _find_mcv(rs) | |
| if mcv is None: | |
| return [Command.observe()] | |
| tx, ty = _nearest_target(mcv, level) | |
| dx = mcv["cell_x"] - tx | |
| dy = mcv["cell_y"] - ty | |
| dist2 = dx * dx + dy * dy | |
| # Within deploy region (radius 8 in YAML; deploy a bit inside | |
| # to be safe). | |
| if dist2 <= 36: | |
| return [Command.deploy([str(mcv["id"])])] | |
| return [Command.move_units([str(mcv["id"])], target_x=tx, target_y=ty)] | |
| return _policy | |
| def _stall_policy(rs, Command): | |
| return [Command.observe()] | |
| def _deploy_in_place_policy(rs, Command): | |
| """Deploy MCV immediately wherever it stands (~(60,20) for | |
| easy/medium, ~(60,10) or (60,30) for hard). The new fact ends up | |
| well outside the east target region → win predicate fails.""" | |
| mcv = _find_mcv(rs) | |
| if mcv is None: | |
| return [Command.observe()] | |
| return [Command.deploy([str(mcv["id"])])] | |
| def _deploy_next_to_existing_policy(rs, Command): | |
| """Move MCV BACK toward base #1 (west) and deploy there. The new | |
| fact ends up near (18,20) — clearly outside the east region.""" | |
| mcv = _find_mcv(rs) | |
| if mcv is None: | |
| return [Command.observe()] | |
| # Once we're nearly on top of the existing west base, deploy. | |
| if abs(mcv["cell_x"] - 22) <= 1 and abs(mcv["cell_y"] - 20) <= 2: | |
| return [Command.deploy([str(mcv["id"])])] | |
| return [Command.move_units([str(mcv["id"])], target_x=22, target_y=20)] | |
| def _deploy_wrong_corner_policy(rs, Command): | |
| """Hard-only: drive MCV to the OPPOSITE corner (the candidate | |
| region that is NOT the nearest one) and deploy. The cross-map | |
| haul + wrong-corner patrol busts the tick budget or kills the | |
| MCV en route.""" | |
| def _policy(rs, Command): | |
| mcv = _find_mcv(rs) | |
| if mcv is None: | |
| return [Command.observe()] | |
| # Pick the FARTHER corner. | |
| dx_n = (mcv["cell_x"] - 95) ** 2 + (mcv["cell_y"] - 10) ** 2 | |
| dx_s = (mcv["cell_x"] - 95) ** 2 + (mcv["cell_y"] - 30) ** 2 | |
| tx, ty = (95, 10) if dx_n > dx_s else (95, 30) | |
| dx = mcv["cell_x"] - tx | |
| dy = mcv["cell_y"] - ty | |
| if dx * dx + dy * dy <= 36: | |
| return [Command.deploy([str(mcv["id"])])] | |
| return [Command.move_units([str(mcv["id"])], target_x=tx, target_y=ty)] | |
| return _policy(rs, Command) | |
| def test_intended_policy_wins(level): | |
| pytest.importorskip("openra_train") | |
| from openra_bench.eval_core import run_level | |
| c = compile_level(load_pack(PACK_PATH), level) | |
| seeds = (1, 2, 3, 4) if level == "hard" else (1,) | |
| for s in seeds: | |
| res = run_level(c, _intended_policy_for(level), seed=s) | |
| assert res.outcome == "win", ( | |
| f"{level} seed={s}: intended deploy-MCV-at-target-region " | |
| f"should WIN, got {res.outcome} after {res.turns} turns " | |
| f"(buildings={sorted(res.signals.own_building_types)}, " | |
| f"own_buildings={res.signals.own_buildings})" | |
| ) | |
| def test_stall_policy_loses(level): | |
| pytest.importorskip("openra_train") | |
| from openra_bench.eval_core import run_level | |
| c = compile_level(load_pack(PACK_PATH), level) | |
| seeds = (1, 2, 3, 4) if level == "hard" else (1,) | |
| for s in seeds: | |
| res = run_level(c, _stall_policy, seed=s) | |
| assert res.outcome == "loss", ( | |
| f"{level} seed={s}: stall must LOSE (no 2nd fact ever), " | |
| f"got {res.outcome} after {res.turns} turns; " | |
| f"buildings={sorted(res.signals.own_building_types)}" | |
| ) | |
| def test_deploy_next_to_existing_base_loses(level): | |
| pytest.importorskip("openra_train") | |
| from openra_bench.eval_core import run_level | |
| c = compile_level(load_pack(PACK_PATH), level) | |
| seeds = (1, 2, 3, 4) if level == "hard" else (1,) | |
| for s in seeds: | |
| res = run_level(c, _deploy_next_to_existing_policy, seed=s) | |
| assert res.outcome == "loss", ( | |
| f"{level} seed={s}: deploy-next-to-existing-base must " | |
| f"LOSE (2nd fact not in east region), got {res.outcome} " | |
| f"after {res.turns} turns; " | |
| f"buildings={res.signals.own_buildings}" | |
| ) | |
| def test_deploy_wrong_corner_loses_hard(): | |
| """Hard-only: deploying at the FAR candidate region (cross-map | |
| diagonal) must LOSE — tick budget + wrong-corner patrol attrition | |
| is the discrimination axis vs the intended NEAREST-corner play.""" | |
| pytest.importorskip("openra_train") | |
| from openra_bench.eval_core import run_level | |
| c = compile_level(load_pack(PACK_PATH), "hard") | |
| for s in (1, 2, 3, 4): | |
| res = run_level(c, _deploy_wrong_corner_policy, seed=s) | |
| assert res.outcome == "loss", ( | |
| f"hard seed={s}: deploy-in-wrong-corner must LOSE " | |
| f"(tick budget blown or MCV killed by far-corner patrol), " | |
| f"got {res.outcome} after {res.turns} turns; " | |
| f"buildings={res.signals.own_buildings}" | |
| ) | |
| def test_timeout_loss_is_reachable_on_every_level(): | |
| """No draw degeneracy: the level's after_ticks fail must fit | |
| inside max_turns on every level (∼90 ticks/turn).""" | |
| pack = load_pack(PACK_PATH) | |
| bars = {"easy": 5401, "medium": 4501, "hard": 3801} | |
| for lvl, bar in bars.items(): | |
| c = compile_level(pack, lvl) | |
| assert bar <= 93 + 90 * (c.max_turns - 1), lvl | |