Spaces:
Running
Running
| """Building & Planning scenario family, full loop on Rust. | |
| No-cheat redesign (closer look): the pack tests construction planning | |
| under an ENGINE-ENFORCED tech-tree dependency (a power-less barracks | |
| never completes) plus spatial placement and base relocation. NO power | |
| is pre-placed, so respecting the prerequisite is the real decision. | |
| These tests prove, with deterministic scripted agents (no model): | |
| * the intended "architect" (power → tech-dependent barracks → placed | |
| defensive line) WINS every level; | |
| * every lazy/greedy/brute/stall policy LOSES every level (the timeout | |
| fail_condition makes non-win a real LOSS, not a draw), including the | |
| greedy "build the goal building first" policy — which proves the | |
| tech prerequisite is genuinely enforced by the engine. | |
| """ | |
| from __future__ import annotations | |
| import pytest | |
| pytest.importorskip("openra_train", reason="Rust env wheel not installed") | |
| pytest.importorskip("openra_rl_training", reason="Rust env wheel not installed") | |
| from openra_bench.eval_core import run_level | |
| from openra_bench.scenarios import load_pack | |
| from openra_bench.scenarios.loader import PACKS_DIR, compile_level | |
| from openra_bench.scoring import score_episode | |
| PACK = PACKS_DIR / "building-and-planning.yaml" | |
| # Region the defensive line must occupy (None on easy = placement-free). | |
| _REGION = {"easy": None, "medium": (40, 20), "hard": (60, 20)} | |
| def _architect_easy(render_state, Command): | |
| """Legal build order: power first (engine blocks a power-less | |
| barracks), then the power-dependent barracks.""" | |
| bt = [b["type"] for b in render_state.get("own_buildings", [])] | |
| prod = render_state.get("production", []) or [] | |
| if "powr" not in bt: | |
| cmds = [Command.build("powr")] if "powr" not in prod else [] | |
| return cmds + [Command.place_building("powr", 14, 18)] | |
| if "tent" not in bt: | |
| cmds = [Command.build("tent")] if "tent" not in prod else [] | |
| return cmds + [Command.place_building("tent", 18, 18)] | |
| return [Command.observe()] | |
| def _architect_chain(region): | |
| """powr→tent→pbox chain, creeping the build radius east until the | |
| two pillboxes can be placed inside `region`.""" | |
| rx, ry = region | |
| def fn(render_state, Command): | |
| bt = [ | |
| (b["type"], b["cell_x"], b["cell_y"]) | |
| for b in render_state.get("own_buildings", []) | |
| ] | |
| prod = render_state.get("production", []) or [] | |
| types = [b[0] for b in bt] | |
| eastmost = max([b[1] for b in bt if b[0] == "powr"], default=10) | |
| if eastmost < rx - 4: | |
| cmds = [Command.build("powr")] if "powr" not in prod else [] | |
| return cmds + [ | |
| Command.place_building("powr", min(eastmost + 8, rx - 2), 18) | |
| ] | |
| if "tent" not in types: | |
| cmds = [Command.build("tent")] if "tent" not in prod else [] | |
| return cmds + [Command.place_building("tent", rx - 2, ry + 2)] | |
| npb = sum(b[0] == "pbox" for b in bt) | |
| if npb < 2: | |
| cmds = [Command.build("pbox")] if "pbox" not in prod else [] | |
| return cmds + [Command.place_building("pbox", rx + npb * 2, ry)] | |
| return [Command.observe()] | |
| return fn | |
| def _idle(render_state, Command): | |
| return [Command.observe()] | |
| def _spam_powr(render_state, Command): | |
| """Greedy: only ever build power plants — never the dependent | |
| barracks/pillbox the win requires.""" | |
| prod = render_state.get("production", []) or [] | |
| bt = [(b["type"], b["cell_x"]) for b in render_state.get("own_buildings", [])] | |
| em = max([x for t, x in bt if t == "powr"], default=10) | |
| cmds = [Command.build("powr")] if "powr" not in prod else [] | |
| return cmds + [Command.place_building("powr", min(em + 6, 80), 18)] | |
| def _goal_first(render_state, Command): | |
| """Greedy: try to build the goal building (barracks/pillbox) FIRST | |
| with NO power — the engine blocks it (it never completes).""" | |
| bt = [b["type"] for b in render_state.get("own_buildings", [])] | |
| prod = render_state.get("production", []) or [] | |
| if "tent" not in bt and "pbox" not in bt: | |
| cmds = [] | |
| if "tent" not in prod and "pbox" not in prod: | |
| cmds.append(Command.build("tent")) | |
| cmds.append(Command.place_building("tent", 16, 18)) | |
| cmds.append(Command.place_building("pbox", 40, 20)) | |
| return cmds | |
| return [Command.observe()] | |
| def test_pack_compiles_with_three_levels_and_no_deploy(): | |
| pack = load_pack(PACK) | |
| assert pack.meta.id == "building-and-planning" | |
| assert pack.meta.capability == "reasoning" | |
| assert set(pack.levels) == {"easy", "medium", "hard"} | |
| # `deploy` must NOT be advertised: the installed engine wheel does | |
| # not transform an MCV into a `fact` (1:1 bench/engine parity). | |
| c = compile_level(pack, "hard") | |
| assert "deploy" not in (c.scenario.tools or []), c.scenario.tools | |
| def test_every_level_has_a_reachable_timeout_fail(level): | |
| """Non-win must be a real LOSS, not a draw: the fail deadline must | |
| be strictly below the tick the episode can reach at max_turns | |
| (tick ≈ 93 + 90·(max_turns-1)).""" | |
| c = compile_level(load_pack(PACK), level) | |
| assert c.fail_condition is not None | |
| fc = c.fail_condition.model_dump(exclude_none=True) | |
| inner = fc["not"] | |
| deadline = int(inner["within_ticks"]) | |
| reachable = 93 + 90 * (c.max_turns - 1) | |
| assert deadline < reachable, ( | |
| f"{level}: fail deadline {deadline} unreachable within " | |
| f"{c.max_turns} turns (max tick {reachable}) → draw degeneracy" | |
| ) | |
| def test_intended_architect_wins_every_level_and_seed(level): | |
| c = compile_level(load_pack(PACK), level) | |
| assert c.map_supported | |
| agent = ( | |
| _architect_easy if level == "easy" | |
| else _architect_chain(_REGION[level]) | |
| ) | |
| for seed in (1, 2, 3, 4): | |
| r = run_level(c, agent, seed=seed) | |
| assert r.outcome == "win", ( | |
| f"{level} seed{seed}: intended architect should win, got " | |
| f"{r.outcome}; buildings={r.signals.own_buildings}" | |
| ) | |
| # Construction debited the budget; the tech chain materialised. | |
| assert r.signals.cash < c.starting_cash | |
| sc = score_episode(c, r) | |
| assert sc.outcome == "win" and 0.0 <= sc.composite <= 1.0 | |
| def test_lazy_and_greedy_policies_lose_every_level_and_seed(level, policy): | |
| """No defect, no cheat: idle/stall, power-spam (ignores the | |
| dependent building), and goal-first (defies the engine-enforced | |
| prerequisite) must all LOSE on every level and hard seed.""" | |
| c = compile_level(load_pack(PACK), level) | |
| fn = {"idle": _idle, "spam_powr": _spam_powr, "goal_first": _goal_first}[ | |
| policy | |
| ] | |
| for seed in (1, 2, 3, 4): | |
| r = run_level(c, fn, seed=seed) | |
| assert r.outcome == "loss", ( | |
| f"{level} seed{seed} {policy}: must LOSE (real fail, not a " | |
| f"draw), got {r.outcome}; buildings={r.signals.own_buildings}" | |
| ) | |
| def test_easy_run_is_deterministic(): | |
| c = compile_level(load_pack(PACK), "easy") | |
| a = run_level(c, _architect_easy, seed=3) | |
| b = run_level(c, _architect_easy, seed=3) | |
| assert (a.outcome, a.turns, a.signals.cash) == ( | |
| b.outcome, | |
| b.turns, | |
| b.signals.cash, | |
| ), "same seed must be deterministic" | |