Spaces:
Running
Running
Merge pull request #5 from yiyu-tian/scenarios/tech-production-planning
Browse files
SCENARIO_BRAINSTORM.md
CHANGED
|
@@ -341,3 +341,74 @@ full (b) **needs S0 + S1**.
|
|
| 341 |
- `openra_bench/scenarios/packs/economy-time-box.yaml` — validator: `OK`
|
| 342 |
- `openra_bench/scenarios/packs/economy-investment.yaml` — validator: `OK`
|
| 343 |
- `SCENARIO_BRAINSTORM.md` (this file)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
- `openra_bench/scenarios/packs/economy-time-box.yaml` — validator: `OK`
|
| 342 |
- `openra_bench/scenarios/packs/economy-investment.yaml` — validator: `OK`
|
| 343 |
- `SCENARIO_BRAINSTORM.md` (this file)
|
| 344 |
+
|
| 345 |
+
---
|
| 346 |
+
|
| 347 |
+
## Tech-Tree & Production Planning pack (design-doc Family 2)
|
| 348 |
+
|
| 349 |
+
Pack: `openra_bench/scenarios/packs/tech-production-planning.yaml`. Maps
|
| 350 |
+
the Benchmark Design Proposal's **Task Family 2 — Sequential Dependency
|
| 351 |
+
Planning** (Scenarios 2A/2B/2C) directly into one bench pack.
|
| 352 |
+
|
| 353 |
+
**Why a new pack rather than extending existing ones.** Two existing
|
| 354 |
+
packs touch this territory but ladder on different axes:
|
| 355 |
+
|
| 356 |
+
| Pack | Easy axis | Medium axis | Hard axis | Ladder shape |
|
| 357 |
+
|---|---|---|---|---|
|
| 358 |
+
| `building-and-planning` | 1-hop prereq (powr→tent) + building count | spatial direction (place pbox east) | MCV relocate / new base | building → spatial → mobility (changes axis per level) |
|
| 359 |
+
| `strict-production-bom` | exact 3×e1 (no overproduction) | + 2×e3 | + tsla + tight budget | spec-fidelity (BFCL/τ²-bench style; orthogonal to dependency planning) |
|
| 360 |
+
| **`tech-production-planning`** (new) | 2-hop chain `weap→fix→3tnk` | parallel branches (barr ‖ weap+fix) + `power_surplus_gte:0` forces 2nd powr | mid-game infantry tech committed → scout fog → pivot to anti-armor | **dependency depth → parallel breadth → adaptive replanning** (same axis at increasing complexity) |
|
| 361 |
+
|
| 362 |
+
`building-and-planning` is mixed-axis (Family 2-ish at easy, drifts into
|
| 363 |
+
Family 3/spatial later). `strict-production-bom` tests a different
|
| 364 |
+
capability entirely (instruction-following fidelity, not dependency
|
| 365 |
+
planning). The new pack is the first time Family 2 is ladded canonically
|
| 366 |
+
on one axis end-to-end.
|
| 367 |
+
|
| 368 |
+
**Real tech tree (engine ground truth from `openra_env/game_data.py`):**
|
| 369 |
+
|
| 370 |
+
| Building | Prereqs | Power |
|
| 371 |
+
|---|---|---|
|
| 372 |
+
| `powr` | — | +100 |
|
| 373 |
+
| `proc` (refinery) | `powr` | −30 |
|
| 374 |
+
| `weap` (war factory) | `proc` | −30 |
|
| 375 |
+
| `fix` (service depot) | `weap` | −30 |
|
| 376 |
+
| `barr` (soviet barracks) | `powr` | −20 |
|
| 377 |
+
| `3tnk` (Heavy Tank) | `weap` + `fix` | unit |
|
| 378 |
+
| `e1` / `e3` (infantry) | `barr|tent` | unit |
|
| 379 |
+
|
| 380 |
+
→ Heavy Tank's real chain is `powr → proc → weap → fix → 3tnk` (4
|
| 381 |
+
prereqs deep). Building all of `proc + weap + fix + barr` drains to −10
|
| 382 |
+
power against a 100-power base — a SECOND powr is mandatory for medium.
|
| 383 |
+
This is the real `power_surplus_gte:0` teeth at medium.
|
| 384 |
+
|
| 385 |
+
**Difficulty calibration (one notch under doc spec).** Design doc 2A
|
| 386 |
+
("Construction Yard + 5000 credits → produce one Heavy Tank from
|
| 387 |
+
scratch") is harder than every existing easy in the repo (~3× the
|
| 388 |
+
work). Easy here pre-places `fact + powr + proc` so the chain becomes
|
| 389 |
+
`weap → fix → 3tnk` (2-hop), aligning with `strict-production-bom`
|
| 390 |
+
easy's "1 build + N train" workload. Medium and hard are calibrated
|
| 391 |
+
similarly downward. Hard (adaptive transition under fog) is left at the
|
| 392 |
+
design-doc difficulty because no existing pack tests online plan
|
| 393 |
+
revision — this is the pack's core new contribution.
|
| 394 |
+
|
| 395 |
+
### Follow-ups (Design Proposal Families 4 / 5 / 7) — separate PRs
|
| 396 |
+
|
| 397 |
+
Each likely needs new engine infrastructure that isn't yet wired into
|
| 398 |
+
the bench:
|
| 399 |
+
|
| 400 |
+
- **Family 4 — Adversarial Reasoning** (Beginner/Normal/Hard AI
|
| 401 |
+
match). Existing `adversarial-*.yaml` packs are duels/sieges (combat
|
| 402 |
+
encounters), not full RTS matches; `bot_type: ''` is empty across
|
| 403 |
+
them, so a scripted opponent AI tier doesn't appear to be exposed
|
| 404 |
+
yet. Needs `bot_type` integration + a "win full match" win predicate.
|
| 405 |
+
|
| 406 |
+
- **Family 5 — Reactive Replanning** (Early threat / Multi-raid /
|
| 407 |
+
Catastrophic). The `interrupts` field exists (`adversarial-duel`
|
| 408 |
+
uses `enemy_unit_spotted` + `own_unit_destroyed`), but
|
| 409 |
+
raid-generation hooks (mid-game enemy attack scripts) and an
|
| 410 |
+
"infrastructure destroyed" trigger aren't represented in any pack.
|
| 411 |
+
|
| 412 |
+
- **Family 7 — Holistic Strategic Intelligence** (full RTS match at 3
|
| 413 |
+
difficulties). Needs Family 4's infra (full-match predicate + AI
|
| 414 |
+
opponent) plus long-horizon scoring beyond the existing rubric.
|
openra_bench/scenarios/packs/tech-production-planning.yaml
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
meta:
|
| 2 |
+
id: tech-production-planning
|
| 3 |
+
title: 'Tech-Tree & Production — Sequential Dependency Planning'
|
| 4 |
+
capability: reasoning
|
| 5 |
+
real_world_meaning: >
|
| 6 |
+
Sequential dependency planning under hard precedence + power + budget
|
| 7 |
+
constraints. Easy is a 2-hop prereq chain (build weap → fix → train
|
| 8 |
+
one Heavy Tank). Medium forces parallel branches (infantry queue at
|
| 9 |
+
barr AND vehicle queue at weap+fix) under a power brownout that
|
| 10 |
+
requires a second power plant. Hard requires online plan revision:
|
| 11 |
+
the agent starts mid-game with infantry tech committed, must scout
|
| 12 |
+
through fog to reveal that the enemy is heavy armor, and pivot
|
| 13 |
+
production to anti-armor (Rocket Soldier) before the deadline. The
|
| 14 |
+
decision being tested is build-order scheduling under precedence,
|
| 15 |
+
not motor control of any single build.
|
| 16 |
+
robotics_analogue: >
|
| 17 |
+
Task-graph scheduling with multi-hop precedence and resource
|
| 18 |
+
constraints, escalating to mid-execution replanning when discovered
|
| 19 |
+
facts invalidate the initial plan — PlanBench Mystery-Blocksworld
|
| 20 |
+
(precedence) plus an online-revision twist after partial observation.
|
| 21 |
+
author: openra-bench
|
| 22 |
+
|
| 23 |
+
base_map: rush-hour-arena
|
| 24 |
+
|
| 25 |
+
base:
|
| 26 |
+
agent:
|
| 27 |
+
faction: soviet
|
| 28 |
+
cash: 0
|
| 29 |
+
enemy:
|
| 30 |
+
faction: allies
|
| 31 |
+
cash: 0
|
| 32 |
+
bot_type: ''
|
| 33 |
+
tools:
|
| 34 |
+
- build
|
| 35 |
+
- place_building
|
| 36 |
+
- train
|
| 37 |
+
- cancel_production
|
| 38 |
+
- move_units
|
| 39 |
+
- deploy
|
| 40 |
+
- stop
|
| 41 |
+
- set_rally_point
|
| 42 |
+
planning: true
|
| 43 |
+
termination:
|
| 44 |
+
max_ticks: 40000
|
| 45 |
+
actors:
|
| 46 |
+
- {type: fact, owner: agent, position: [10, 18]}
|
| 47 |
+
- {type: powr, owner: agent, position: [14, 18]}
|
| 48 |
+
|
| 49 |
+
levels:
|
| 50 |
+
easy:
|
| 51 |
+
description: >
|
| 52 |
+
Simple Tech Chain — fact, powr, and proc are pre-placed; build
|
| 53 |
+
War Factory → Service Depot, then train one Heavy Tank. The
|
| 54 |
+
decision is recognising 3tnk needs weap + fix in that order; the
|
| 55 |
+
budget covers the chain with modest slack. Tests the agent can
|
| 56 |
+
sequence a deep enough prereq chain (proc→weap→fix→3tnk) without
|
| 57 |
+
attempting to train 3tnk before fix exists.
|
| 58 |
+
starting_cash: 5000
|
| 59 |
+
overrides:
|
| 60 |
+
actors:
|
| 61 |
+
- {type: fact, owner: agent, position: [10, 18]}
|
| 62 |
+
- {type: powr, owner: agent, position: [14, 18]}
|
| 63 |
+
- {type: proc, owner: agent, position: [16, 22]}
|
| 64 |
+
- {type: mine, owner: agent, position: [20, 24]}
|
| 65 |
+
- {type: e1, owner: enemy, position: [114, 34], stance: 2}
|
| 66 |
+
win_condition:
|
| 67 |
+
all_of:
|
| 68 |
+
- unit_type_count_gte: {type: 3tnk, n: 1}
|
| 69 |
+
- building_count_gte: {type: fix, n: 1}
|
| 70 |
+
- within_ticks: 18000
|
| 71 |
+
max_turns: 60
|
| 72 |
+
|
| 73 |
+
medium:
|
| 74 |
+
description: >
|
| 75 |
+
Parallel Dependency Optimization — only fact + powr pre-placed.
|
| 76 |
+
Produce one Heavy Tank PLUS one Rocket Soldier PLUS three Rifle
|
| 77 |
+
Infantry, with power_surplus ≥ 0 at the deadline. The combined
|
| 78 |
+
build (proc + barr + weap + fix = -110 power) drains net power
|
| 79 |
+
below zero against a 100-power base, so a SECOND power plant is
|
| 80 |
+
mandatory. Tight clock forces queueing the infantry branch (barr)
|
| 81 |
+
in parallel with the vehicle branch (weap+fix); serial production
|
| 82 |
+
blows the clock on idle time.
|
| 83 |
+
starting_cash: 6500
|
| 84 |
+
overrides:
|
| 85 |
+
actors:
|
| 86 |
+
- {type: fact, owner: agent, position: [10, 18]}
|
| 87 |
+
- {type: powr, owner: agent, position: [14, 18]}
|
| 88 |
+
- {type: mine, owner: agent, position: [20, 24]}
|
| 89 |
+
- {type: e1, owner: enemy, position: [114, 34], stance: 2}
|
| 90 |
+
win_condition:
|
| 91 |
+
all_of:
|
| 92 |
+
- unit_type_count_gte: {type: 3tnk, n: 1}
|
| 93 |
+
- unit_type_count_gte: {type: e3, n: 1}
|
| 94 |
+
- unit_type_count_gte: {type: e1, n: 3}
|
| 95 |
+
- power_surplus_gte: 0
|
| 96 |
+
- within_ticks: 22000
|
| 97 |
+
max_turns: 80
|
| 98 |
+
|
| 99 |
+
hard:
|
| 100 |
+
description: >
|
| 101 |
+
Adaptive Tech Transition — mid-game start: fact + powr + proc +
|
| 102 |
+
barr and three Rifle Infantry are already on the field (an
|
| 103 |
+
infantry build committed). Three enemy Heavy Tanks sit passive on
|
| 104 |
+
the east side, hidden by fog. The agent must scout, recognise
|
| 105 |
+
that infantry is the wrong counter to heavy armor, and pivot
|
| 106 |
+
production to FIVE Rocket Soldiers (e3, anti-armor) before the
|
| 107 |
+
deadline. Overproducing infantry past ten = fail (doubling down
|
| 108 |
+
on the wrong plan exhausts the budget). The transition — cancel
|
| 109 |
+
planned infantry, fund anti-armor, scout — is the test.
|
| 110 |
+
starting_cash: 3000
|
| 111 |
+
overrides:
|
| 112 |
+
actors:
|
| 113 |
+
- {type: fact, owner: agent, position: [10, 18]}
|
| 114 |
+
- {type: powr, owner: agent, position: [14, 18]}
|
| 115 |
+
- {type: proc, owner: agent, position: [16, 22]}
|
| 116 |
+
- {type: barr, owner: agent, position: [12, 21]}
|
| 117 |
+
- {type: e1, owner: agent, position: [8, 16], count: 3}
|
| 118 |
+
- {type: mine, owner: agent, position: [20, 24]}
|
| 119 |
+
- {type: 3tnk, owner: enemy, position: [110, 34], stance: 2, count: 3}
|
| 120 |
+
win_condition:
|
| 121 |
+
all_of:
|
| 122 |
+
- enemies_discovered_gte: 2
|
| 123 |
+
- unit_type_count_gte: {type: e3, n: 5}
|
| 124 |
+
- within_ticks: 14000
|
| 125 |
+
fail_condition:
|
| 126 |
+
unit_type_count_gte: {type: e1, n: 10}
|
| 127 |
+
max_turns: 90
|
tests/test_tech_production_planning.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""tech-production-planning pack — full loop on Rust.
|
| 2 |
+
|
| 3 |
+
Canonical Family 2 (Sequential Dependency Planning) from the Benchmark
|
| 4 |
+
Design Proposal: easy = 2-hop chain (weap -> fix -> 3tnk), medium =
|
| 5 |
+
parallel branches under power_surplus_gte:0, hard = adaptive transition.
|
| 6 |
+
|
| 7 |
+
A scripted "tech-planner" agent walks the easy chain (build War Factory
|
| 8 |
+
-> Service Depot -> train one Heavy Tank) and proves the declarative
|
| 9 |
+
win_condition fires; medium/hard run deterministically with the same
|
| 10 |
+
agent (they need richer play than a scripted depth-first chain to win,
|
| 11 |
+
so we assert determinism + budget bound, mirroring test_building_planning).
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import pytest
|
| 17 |
+
|
| 18 |
+
pytest.importorskip("openra_train", reason="Rust env wheel not installed")
|
| 19 |
+
|
| 20 |
+
from openra_bench.eval_core import run_level
|
| 21 |
+
from openra_bench.scenarios import load_pack
|
| 22 |
+
from openra_bench.scenarios.loader import PACKS_DIR, compile_level
|
| 23 |
+
from openra_bench.scoring import score_episode
|
| 24 |
+
|
| 25 |
+
PACK = PACKS_DIR / "tech-production-planning.yaml"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def tech_planner_agent(render_state, Command):
|
| 29 |
+
"""Walk the 3tnk prereq chain depth-first.
|
| 30 |
+
|
| 31 |
+
Pre-placed: fact, powr, proc. Build weap -> fix, then train one 3tnk.
|
| 32 |
+
`build` queues the item; `place_building` no-ops until the building
|
| 33 |
+
is ready to be placed.
|
| 34 |
+
"""
|
| 35 |
+
btypes = [b["type"] for b in render_state.get("own_buildings", [])]
|
| 36 |
+
producing = render_state.get("production", []) or []
|
| 37 |
+
units = [u["type"] for u in render_state.get("units_summary", [])]
|
| 38 |
+
|
| 39 |
+
if "weap" not in btypes:
|
| 40 |
+
cmds = []
|
| 41 |
+
if "weap" not in producing:
|
| 42 |
+
cmds.append(Command.build("weap"))
|
| 43 |
+
cmds.append(Command.place_building("weap", 18, 22))
|
| 44 |
+
return cmds
|
| 45 |
+
if "fix" not in btypes:
|
| 46 |
+
cmds = []
|
| 47 |
+
if "fix" not in producing:
|
| 48 |
+
cmds.append(Command.build("fix"))
|
| 49 |
+
cmds.append(Command.place_building("fix", 22, 22))
|
| 50 |
+
return cmds
|
| 51 |
+
if "3tnk" not in units:
|
| 52 |
+
if "3tnk" not in producing:
|
| 53 |
+
return [Command.build("3tnk")]
|
| 54 |
+
return [Command.observe()]
|
| 55 |
+
return [Command.observe()]
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def test_pack_compiles_with_three_levels():
|
| 59 |
+
pack = load_pack(PACK)
|
| 60 |
+
assert pack.meta.id == "tech-production-planning"
|
| 61 |
+
assert pack.meta.capability == "reasoning"
|
| 62 |
+
assert set(pack.levels) == {"easy", "medium", "hard"}
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def test_easy_tech_chain_wins_and_scores():
|
| 66 |
+
pack = load_pack(PACK)
|
| 67 |
+
c = compile_level(pack, "easy")
|
| 68 |
+
assert c.map_supported
|
| 69 |
+
assert c.starting_cash == 5000
|
| 70 |
+
|
| 71 |
+
res = run_level(c, tech_planner_agent, seed=1)
|
| 72 |
+
|
| 73 |
+
# The chain went through both weap AND fix (the prereq for 3tnk).
|
| 74 |
+
btypes = res.signals.own_building_types
|
| 75 |
+
assert "weap" in btypes and "fix" in btypes, (
|
| 76 |
+
f"chain incomplete; own_building_types={btypes}"
|
| 77 |
+
)
|
| 78 |
+
# The agent actually trained a Heavy Tank.
|
| 79 |
+
unit_types = [u["type"] for u in res.signals.units_summary]
|
| 80 |
+
assert "3tnk" in unit_types, f"no 3tnk produced; units={unit_types}"
|
| 81 |
+
# Production debited the budget.
|
| 82 |
+
assert res.signals.cash < 5000, "construction must debit the budget"
|
| 83 |
+
assert res.outcome == "win", f"easy should be winnable, got {res.outcome}"
|
| 84 |
+
|
| 85 |
+
sc = score_episode(c, res)
|
| 86 |
+
assert sc.outcome == "win" and 0.0 <= sc.composite <= 1.0
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def test_easy_run_is_deterministic():
|
| 90 |
+
c = compile_level(load_pack(PACK), "easy")
|
| 91 |
+
a = run_level(c, tech_planner_agent, seed=7)
|
| 92 |
+
b = run_level(c, tech_planner_agent, seed=7)
|
| 93 |
+
assert (a.outcome, a.turns, a.signals.cash) == (
|
| 94 |
+
b.outcome,
|
| 95 |
+
b.turns,
|
| 96 |
+
b.signals.cash,
|
| 97 |
+
), "same seed must be deterministic"
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@pytest.mark.parametrize("level", ["medium", "hard"])
|
| 101 |
+
def test_medium_hard_run_and_score_within_constraints(level):
|
| 102 |
+
"""Harder levels need richer play than the scripted depth-first chain
|
| 103 |
+
(medium wants parallel queues + 2nd powr; hard wants scouting +
|
| 104 |
+
pivot to anti-armor). Assert they execute within declared constraints
|
| 105 |
+
and score deterministically — the contributor loop must be robust
|
| 106 |
+
even on a loss."""
|
| 107 |
+
pack = load_pack(PACK)
|
| 108 |
+
c = compile_level(pack, level)
|
| 109 |
+
a = run_level(c, tech_planner_agent, seed=3)
|
| 110 |
+
b = run_level(c, tech_planner_agent, seed=3)
|
| 111 |
+
assert a.outcome in {"win", "draw", "loss"}
|
| 112 |
+
assert (a.outcome, a.turns, a.signals.cash) == (
|
| 113 |
+
b.outcome,
|
| 114 |
+
b.turns,
|
| 115 |
+
b.signals.cash,
|
| 116 |
+
), "same seed must yield identical outcome"
|
| 117 |
+
sc = score_episode(c, a)
|
| 118 |
+
assert 0.0 <= sc.composite <= 1.0
|
| 119 |
+
# Budget constraint enforced by the engine.
|
| 120 |
+
assert a.signals.cash <= c.starting_cash
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def test_hard_fail_predicate_set():
|
| 124 |
+
"""Hard declares overproducing infantry past 10 = fail. Confirm the
|
| 125 |
+
fail_condition is wired through compile (the engine actually checks
|
| 126 |
+
it). Schema-level check, no episode run required."""
|
| 127 |
+
c = compile_level(load_pack(PACK), "hard")
|
| 128 |
+
assert c.fail_condition is not None
|
| 129 |
+
# The fail leaf is unit_type_count_gte on e1 at n=10.
|
| 130 |
+
fc = c.fail_condition.model_dump(exclude_none=True)
|
| 131 |
+
assert fc == {"unit_type_count_gte": {"type": "e1", "n": 10}}, fc
|