OpenRA-Bench / tests /test_building_planning.py
Yiyu Tian
tests: module-level importorskip on all 80 engine-dependent test files
5cfed54
Raw
History Blame Contribute Delete
7.54 kB
"""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
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
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"
)
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
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
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
@pytest.mark.parametrize(
"policy", ["idle", "spam_powr", "goal_first"]
)
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"