Spaces:
Running
Running
feat(scenario): def-retreat-and-rebuild — strategic withdrawal + rebuild (CICERO retreat / replanning anchor)
Browse files
openra_bench/scenarios/packs/def-retreat-and-rebuild.yaml
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# def-retreat-and-rebuild — strategic withdrawal + rebuild at depth
|
| 2 |
+
#
|
| 3 |
+
# Wave-8 REASONING pack. The agent operates a FORWARD base that is
|
| 4 |
+
# being overrun by a heavy attack from the east; the FORWARD base
|
| 5 |
+
# (fact + proc + tent + 2 defenders at x≈40, y=20) WILL FALL under
|
| 6 |
+
# the inbound rush — it cannot be held inside the tick budget. The
|
| 7 |
+
# agent also has a parked MCV and a powr at a deeper SAFE zone in
|
| 8 |
+
# the WEST (x≈10..14, y=20), well outside the rusher's effective
|
| 9 |
+
# travel. The capability under test is to RECOGNISE the losing
|
| 10 |
+
# position, RETREAT (let the forward base fall — don't stake
|
| 11 |
+
# anything on it), DEPLOY the safe-zone MCV (creates a new fact in
|
| 12 |
+
# the safe region), and BUILD a `proc` next to it so the production
|
| 13 |
+
# economy stands up at depth. Win = a fact AND a proc are alive
|
| 14 |
+
# INSIDE the safe region before the deadline.
|
| 15 |
+
#
|
| 16 |
+
# Real-world anchors:
|
| 17 |
+
# - CICERO retreat doctrine / military strategic withdrawal:
|
| 18 |
+
# intentionally giving up indefensible forward ground to
|
| 19 |
+
# preserve force and re-establish a defensible posture at
|
| 20 |
+
# greater depth ("trading space for time").
|
| 21 |
+
# - PlanBench replanning: the original plan ("hold the forward
|
| 22 |
+
# base") becomes infeasible; the agent must re-plan toward a
|
| 23 |
+
# reachable goal at depth.
|
| 24 |
+
# - Business continuity / disaster recovery failover: the
|
| 25 |
+
# primary production site is being lost; standing up the
|
| 26 |
+
# replacement site at the cold-standby location IS the
|
| 27 |
+
# business-continuity goal, not heroic defence of the primary.
|
| 28 |
+
#
|
| 29 |
+
# DISCRIMINATION (the four-policy bar, every level + hard seed):
|
| 30 |
+
#
|
| 31 |
+
# • STALL (only observe): the rusher band razes the forward base
|
| 32 |
+
# ~turn 18-22; the MCV at the safe zone is never deployed and
|
| 33 |
+
# no proc is built; the safe-region win clauses are never
|
| 34 |
+
# satisfied; total fact count drops to 0 by the deadline →
|
| 35 |
+
# LOSS via `not building_count_gte:{type:fact,n:1}` (fact-loss
|
| 36 |
+
# fail clause) AND `after_ticks`.
|
| 37 |
+
#
|
| 38 |
+
# • HOLD-FORWARD-BASE (try to defend the forward base; e.g.
|
| 39 |
+
# attack_unit the rushers with the 2 defenders, build pbox at
|
| 40 |
+
# the forward perimeter, never touch the safe MCV): the rush
|
| 41 |
+
# is too heavy; the forward base is razed regardless; the
|
| 42 |
+
# defenders die in the trade; the safe MCV stays parked → no
|
| 43 |
+
# safe-region fact, no safe-region proc → LOSS.
|
| 44 |
+
#
|
| 45 |
+
# • DEPLOY-MCV-ONLY (deploy the safe-zone MCV, but never build
|
| 46 |
+
# proc): the safe-region fact clause IS satisfied (the deploy
|
| 47 |
+
# creates a fact at the MCV cell) but the safe-region proc
|
| 48 |
+
# clause is NEVER satisfied — proc requires `build('proc')` +
|
| 49 |
+
# `place_building('proc', ...)` inside the safe radius →
|
| 50 |
+
# LOSS via after_ticks (win unsatisfied) AND/OR
|
| 51 |
+
# `not building_count_gte:{type:proc,n:1}` once the forward
|
| 52 |
+
# proc is razed.
|
| 53 |
+
#
|
| 54 |
+
# • INTENDED RETREAT-AND-REBUILD: deploy the safe-zone MCV early
|
| 55 |
+
# (creates a fact at the safe radius), then `build('proc')` +
|
| 56 |
+
# `place_building('proc')` inside the safe region. WINS once
|
| 57 |
+
# both safe-region predicates fire (typically tick ~1000-1100
|
| 58 |
+
# for the deploy + proc cycle), well inside the deadline.
|
| 59 |
+
#
|
| 60 |
+
# WHY both safe-region clauses load-bear (no laziest-play
|
| 61 |
+
# inversion):
|
| 62 |
+
# - The win is `all_of`: fact-in-safe AND proc-in-safe AND
|
| 63 |
+
# within_ticks. Either clause alone is reachable by a half-task
|
| 64 |
+
# play; both together require BOTH the deploy AND the build.
|
| 65 |
+
# - The fail clause includes `not building_count_gte:{type:fact,n:1}`
|
| 66 |
+
# (canonical fact-loss check) so total fact extinction also
|
| 67 |
+
# triggers a real LOSS (no draw degeneracy).
|
| 68 |
+
# - The forward base falling is REQUIRED by design: the rush
|
| 69 |
+
# band is overwhelming, and the agent has no tools / no cash
|
| 70 |
+
# to mount a defence sufficient to keep the forward fact +
|
| 71 |
+
# forward proc alive. Trying to hold the forward base bleeds
|
| 72 |
+
# the only assets (the 2 defenders) that count toward
|
| 73 |
+
# own_units_gte. The agent must accept the loss.
|
| 74 |
+
#
|
| 75 |
+
# ENGINE FACTS APPLIED (CLAUDE.md / SCENARIO_QUALITY.md):
|
| 76 |
+
# - Map: rush-hour-arena (128 × 40, playable x in [2..126],
|
| 77 |
+
# y in [2..38]). Every actor coord inside.
|
| 78 |
+
# - `deploy` works for scenario-declared MCVs (Wave-2 fix in
|
| 79 |
+
# `openra-sim/src/gamerules.rs` + env.rs `kind_for_unit_type`).
|
| 80 |
+
# `Command.deploy([mcv_id])` removes the MCV and creates an
|
| 81 |
+
# agent `fact` at offset (mcv_x-1, mcv_y-1), re-enabling
|
| 82 |
+
# Building/Defense queues.
|
| 83 |
+
# - `place_building` does NOT enforce build-adjacency — `proc`
|
| 84 |
+
# can be placed at any safe-zone cell once the post-deploy
|
| 85 |
+
# fact is alive and the Building queue has the item.
|
| 86 |
+
# - `proc` costs 1400; `starting_cash: 1600` funds exactly one
|
| 87 |
+
# proc + ~200cr of slack. NO budget for an alternate "spam
|
| 88 |
+
# pbox to save the forward base" play.
|
| 89 |
+
# - `rusher` bot: concentrates the band on the agent CENTROID
|
| 90 |
+
# every ~8 ticks (scripted_bot.rs::Rusher); the centroid is
|
| 91 |
+
# dominated by the forward base (3 buildings + 2 defenders at
|
| 92 |
+
# x≈40), so the band drives west down y=20 straight into the
|
| 93 |
+
# forward base. The deep safe zone (x≈10) is far behind the
|
| 94 |
+
# centroid; even after the forward base falls, the rusher
|
| 95 |
+
# band continues toward the safe-zone units (but the MCV
|
| 96 |
+
# deploys + proc completes before the band can travel another
|
| 97 |
+
# ~30 cells west).
|
| 98 |
+
# - Persistent unarmed enemy `fact` marker far east keeps the
|
| 99 |
+
# episode alive past the last rusher death so the win/fail
|
| 100 |
+
# evaluator fires (anti auto-done on enemy-elimination).
|
| 101 |
+
# - `enemy_unit_spotted` + `own_unit_destroyed` interrupts so
|
| 102 |
+
# the agent re-decides as the rush closes AND when the
|
| 103 |
+
# forward fact / forward proc falls — the canonical
|
| 104 |
+
# "plan-changed, replan" trigger for CICERO retreat doctrine.
|
| 105 |
+
# - Tick budget: max_turns 60 → reachable tick ≈ 5400 in
|
| 106 |
+
# interrupt mode. `within_ticks: 4500` paired with
|
| 107 |
+
# `after_ticks: 4501` ⇒ a non-finisher is a real reachable
|
| 108 |
+
# timeout LOSS (no draw degeneracy).
|
| 109 |
+
# - Hard tier: TWO seed-driven agent spawn_point groups
|
| 110 |
+
# (NORTH safe zone at y=14 vs SOUTH safe zone at y=26). The
|
| 111 |
+
# safe-zone candidate that satisfies the win clause is an
|
| 112 |
+
# `any_of` so EITHER spawn satisfies the safe geometry —
|
| 113 |
+
# but the MCV + powr (and the matching forward base) move
|
| 114 |
+
# to the spawn latitude per seed, so a memorised "always
|
| 115 |
+
# deploy at (10,20)" cannot generalise.
|
| 116 |
+
|
| 117 |
+
meta:
|
| 118 |
+
id: def-retreat-and-rebuild
|
| 119 |
+
title: 'Strategic Withdrawal — Retreat from the Forward Base and Rebuild at Depth'
|
| 120 |
+
capability: reasoning
|
| 121 |
+
real_world_meaning: >
|
| 122 |
+
The forward base is being overrun by a heavy attack that cannot
|
| 123 |
+
be repulsed inside the tick / cash budget; the right move is to
|
| 124 |
+
ACCEPT THE LOSS of the forward position, fall back to a deeper
|
| 125 |
+
safe zone (pre-staged with an MCV and a power plant), DEPLOY
|
| 126 |
+
the MCV to spawn a new Construction Yard there, and BUILD a
|
| 127 |
+
refinery so production stands up at depth. Trying to hold the
|
| 128 |
+
forward base bleeds the defenders and the deadline expires
|
| 129 |
+
with neither base alive. Stalling, hold-forward, or deploy-
|
| 130 |
+
only-no-rebuild all lose: only the recognise-and-withdraw
|
| 131 |
+
policy reconstitutes a live fact + proc post-retreat.
|
| 132 |
+
robotics_analogue: >
|
| 133 |
+
Business-continuity failover and military strategic withdrawal:
|
| 134 |
+
the primary production site is being lost and heroic defence
|
| 135 |
+
is infeasible; standing up the cold-standby site (DR site,
|
| 136 |
+
backup datacentre, fall-back command post) BEFORE the primary
|
| 137 |
+
fully falls is the goal, not preserving the primary. The
|
| 138 |
+
policy must read the situation as "lost cause, withdraw" and
|
| 139 |
+
redirect resources to the standby instead of doubling down.
|
| 140 |
+
benchmark_anchor:
|
| 141 |
+
- "strategic withdrawal / CICERO retreat doctrine"
|
| 142 |
+
- "PlanBench replanning under exogenous loss"
|
| 143 |
+
- "business continuity / disaster recovery failover"
|
| 144 |
+
- "military trade-space-for-time fall-back"
|
| 145 |
+
author: openra-bench
|
| 146 |
+
|
| 147 |
+
base_map: rush-hour-arena
|
| 148 |
+
# 1600 cash funds exactly ONE proc (1400) + ~200 slack — enough to
|
| 149 |
+
# stand up the safe-zone economy, nowhere near enough to mount a
|
| 150 |
+
# meaningful forward-base defence (a pbox costs 600, 2 pbox = 1200
|
| 151 |
+
# still leaves the rush undamaged on the heavy band; the forward
|
| 152 |
+
# base WILL fall regardless of how the cash is spent there).
|
| 153 |
+
starting_cash: 1600
|
| 154 |
+
|
| 155 |
+
base:
|
| 156 |
+
agent: {faction: allies}
|
| 157 |
+
# `rusher` bot concentrates the band on the agent centroid every
|
| 158 |
+
# ~8 ticks (scripted_bot.rs::Rusher) — the band drives straight
|
| 159 |
+
# at the forward base mass and razes it.
|
| 160 |
+
enemy: {faction: soviet, bot_type: rusher}
|
| 161 |
+
# Minimal toolset for the capability: observe + move + deploy
|
| 162 |
+
# (the retreat / MCV-deploy primitive) + build/place_building
|
| 163 |
+
# (the rebuild-production primitive) + attack/stop so a
|
| 164 |
+
# hold-forward play is even attemptable (and demonstrably loses).
|
| 165 |
+
tools:
|
| 166 |
+
- observe
|
| 167 |
+
- build
|
| 168 |
+
- place_building
|
| 169 |
+
- deploy
|
| 170 |
+
- move_units
|
| 171 |
+
- attack_unit
|
| 172 |
+
- attack_move
|
| 173 |
+
- stop
|
| 174 |
+
planning: true
|
| 175 |
+
interrupts:
|
| 176 |
+
enemy_unit_spotted: true
|
| 177 |
+
own_unit_destroyed: true
|
| 178 |
+
termination:
|
| 179 |
+
max_ticks: 12000
|
| 180 |
+
actors: []
|
| 181 |
+
|
| 182 |
+
levels:
|
| 183 |
+
# ── EASY ─────────────────────────────────────────────────────────
|
| 184 |
+
# Bare retreat-and-rebuild skill: light rush band (4× e1 + 1× e3),
|
| 185 |
+
# generous clock (4500 ticks). The forward base still falls (the
|
| 186 |
+
# e3 rocket soldier ensures the fact dies in the budget) so the
|
| 187 |
+
# agent CANNOT avoid the retreat. Easy axis: forgiving clock + 1
|
| 188 |
+
# lone rocket so brute hold-forward also loses but slowly.
|
| 189 |
+
# max_turns 60 → reachable tick ≈ 5400 in interrupt mode ≥ 4501 ✓.
|
| 190 |
+
easy:
|
| 191 |
+
description: >
|
| 192 |
+
A rusher band (4 rifle infantry + 1 rocket soldier) is bearing
|
| 193 |
+
down on your FORWARD base (Construction Yard + refinery + 2
|
| 194 |
+
defenders at cells around x=40, y=20) from the east. The
|
| 195 |
+
forward base CANNOT be held inside the deadline — the rocket
|
| 196 |
+
soldier alone razes the fact, and your defenders are too few.
|
| 197 |
+
You also have a parked MCV at the deeper SAFE zone in the
|
| 198 |
+
west (cell 10, 20) plus a pre-placed power plant nearby. Win
|
| 199 |
+
by RETREATING — let the forward base fall — and standing up
|
| 200 |
+
the new base at depth: DEPLOY the MCV (creates a Construction
|
| 201 |
+
Yard at the safe zone) AND BUILD a refinery (`proc`) inside
|
| 202 |
+
the safe radius around (10,20). Win when a fact AND a proc
|
| 203 |
+
are alive inside the safe region (radius 8) AND you have
|
| 204 |
+
≥1 own unit alive AND before tick 4500. Stalling, trying to
|
| 205 |
+
hold the forward base, and deploying the MCV but not building
|
| 206 |
+
the refinery all LOSE.
|
| 207 |
+
overrides:
|
| 208 |
+
actors:
|
| 209 |
+
# ── FORWARD base (mid-map, will fall) — fact + proc + tent
|
| 210 |
+
# + 2 defenders. The forward proc is the canonical second
|
| 211 |
+
# high-value building that bleeds the agent if defended.
|
| 212 |
+
- {type: fact, owner: agent, position: [40, 20]}
|
| 213 |
+
- {type: proc, owner: agent, position: [44, 20]}
|
| 214 |
+
- {type: tent, owner: agent, position: [40, 23]}
|
| 215 |
+
- {type: e1, owner: agent, position: [42, 19], stance: 2}
|
| 216 |
+
- {type: e1, owner: agent, position: [42, 21], stance: 2}
|
| 217 |
+
# ── SAFE zone (deep west) — pre-placed MCV + powr +
|
| 218 |
+
# 1 garrison rifleman (satisfies own_units_gte:1 after the
|
| 219 |
+
# forward defenders die in the rush). The powr is required
|
| 220 |
+
# because proc has positive power draw and the post-deploy
|
| 221 |
+
# fact starts at 0/0 net; without a pre-placed powr a build
|
| 222 |
+
# of proc stalls on power.
|
| 223 |
+
- {type: mcv, owner: agent, position: [10, 20]}
|
| 224 |
+
- {type: powr, owner: agent, position: [6, 20]}
|
| 225 |
+
- {type: e1, owner: agent, position: [4, 20], stance: 0}
|
| 226 |
+
# ── rusher band: 4× e1 + 1× e3, starting at x=100 (≈60
|
| 227 |
+
# cells of travel to reach the forward fact at x=40 — fast
|
| 228 |
+
# enough that hold-forward cannot survive but slow enough
|
| 229 |
+
# that the retreat-and-rebuild policy has comfortable time).
|
| 230 |
+
- {type: e1, owner: enemy, position: [100, 20], stance: 3, count: 4}
|
| 231 |
+
- {type: e3, owner: enemy, position: [102, 20], stance: 3, count: 1}
|
| 232 |
+
# ── persistent unarmed enemy marker far east (anti DRAW —
|
| 233 |
+
# without this the engine auto-`done`s on enemy-elimination
|
| 234 |
+
# if the agent ever clears the rush, collapsing the run).
|
| 235 |
+
- {type: fact, owner: enemy, position: [125, 20]}
|
| 236 |
+
win_condition:
|
| 237 |
+
all_of:
|
| 238 |
+
# Safe-region fact (post-deploy MCV lands a fact ~(9,19)).
|
| 239 |
+
- building_in_region: {type: fact, x: 10, y: 20, radius: 8, count: 1}
|
| 240 |
+
# Safe-region proc (the rebuild — the load-bearing clause).
|
| 241 |
+
- building_in_region: {type: proc, x: 10, y: 20, radius: 8, count: 1}
|
| 242 |
+
- own_units_gte: 1
|
| 243 |
+
- within_ticks: 4500
|
| 244 |
+
fail_condition:
|
| 245 |
+
any_of:
|
| 246 |
+
- after_ticks: 4501
|
| 247 |
+
# Total fact extinction (both forward AND safe).
|
| 248 |
+
- not: {building_count_gte: {type: fact, n: 1}}
|
| 249 |
+
max_turns: 60
|
| 250 |
+
|
| 251 |
+
# ── MEDIUM ───────────────────────────────────────────────────────
|
| 252 |
+
# +1 controlled variable: HEAVIER rush band (6× e1 + 2× e3) so the
|
| 253 |
+
# forward base falls FASTER and the hold-forward attrition cap
|
| 254 |
+
# bites harder. Same clock (4500), same retreat-and-rebuild
|
| 255 |
+
# geometry. The intended play still wins comfortably (the deploy
|
| 256 |
+
# + proc-build cycle is ~30 turns wall-clock); brute hold-forward
|
| 257 |
+
# loses faster and more visibly.
|
| 258 |
+
medium:
|
| 259 |
+
description: >
|
| 260 |
+
A heavy rusher band (6 rifle infantry + 2 rocket soldiers) is
|
| 261 |
+
bearing down on your FORWARD base (fact + proc + 2 defenders
|
| 262 |
+
at x=40, y=20). The forward base CANNOT be held — the
|
| 263 |
+
rockets shred the fact in ~15 turns of fire. You have a
|
| 264 |
+
parked MCV at the deeper SAFE zone (cell 10, 20) and a
|
| 265 |
+
pre-placed power plant. Win by RETREATING (let the forward
|
| 266 |
+
base fall) and standing up the deeper base: DEPLOY the MCV
|
| 267 |
+
AND BUILD a refinery (`proc`) inside the safe radius around
|
| 268 |
+
(10,20). Win when a fact AND a proc are alive inside the
|
| 269 |
+
safe region (radius 8) AND you have ≥1 own unit alive AND
|
| 270 |
+
before tick 4500. Hold-forward, deploy-only-no-rebuild, and
|
| 271 |
+
stall all LOSE.
|
| 272 |
+
overrides:
|
| 273 |
+
actors:
|
| 274 |
+
- {type: fact, owner: agent, position: [40, 20]}
|
| 275 |
+
- {type: proc, owner: agent, position: [44, 20]}
|
| 276 |
+
- {type: tent, owner: agent, position: [40, 23]}
|
| 277 |
+
- {type: e1, owner: agent, position: [42, 19], stance: 2}
|
| 278 |
+
- {type: e1, owner: agent, position: [42, 21], stance: 2}
|
| 279 |
+
- {type: mcv, owner: agent, position: [10, 20]}
|
| 280 |
+
- {type: powr, owner: agent, position: [6, 20]}
|
| 281 |
+
- {type: e1, owner: agent, position: [4, 20], stance: 0}
|
| 282 |
+
# Heavier band: 6× e1 + 2× e3 from x=100 — same arrival
|
| 283 |
+
# window as easy but doubles the anti-building dps.
|
| 284 |
+
- {type: e1, owner: enemy, position: [100, 20], stance: 3, count: 6}
|
| 285 |
+
- {type: e3, owner: enemy, position: [102, 20], stance: 3, count: 2}
|
| 286 |
+
- {type: fact, owner: enemy, position: [125, 20]}
|
| 287 |
+
win_condition:
|
| 288 |
+
all_of:
|
| 289 |
+
- building_in_region: {type: fact, x: 10, y: 20, radius: 8, count: 1}
|
| 290 |
+
- building_in_region: {type: proc, x: 10, y: 20, radius: 8, count: 1}
|
| 291 |
+
- own_units_gte: 1
|
| 292 |
+
- within_ticks: 4500
|
| 293 |
+
fail_condition:
|
| 294 |
+
any_of:
|
| 295 |
+
- after_ticks: 4501
|
| 296 |
+
- not: {building_count_gte: {type: fact, n: 1}}
|
| 297 |
+
max_turns: 60
|
| 298 |
+
|
| 299 |
+
# ── HARD ─────────────────────────────────────────────────────────
|
| 300 |
+
# +1 controlled variable: TWO seed-driven agent spawn_point groups
|
| 301 |
+
# (NORTH layout y=14 vs SOUTH layout y=26). Both the FORWARD base
|
| 302 |
+
# AND the SAFE zone MCV move to the spawn latitude per seed; the
|
| 303 |
+
# win clause is `any_of` over the two candidate safe regions so
|
| 304 |
+
# EITHER landing satisfies, but the agent must DEPLOY+BUILD at
|
| 305 |
+
# the latitude that MATCHES its own spawn (the rusher commits to
|
| 306 |
+
# the active latitude's centroid, and the rebuilt base must be
|
| 307 |
+
# at that latitude to count toward the safe-region predicates).
|
| 308 |
+
# A memorised "always rebuild at y=20" play satisfies NEITHER
|
| 309 |
+
# safe region (the safe regions are at y=14 and y=26, NOT y=20).
|
| 310 |
+
# Per CLAUDE.md spawn_point contract: ALL agent actors carry an
|
| 311 |
+
# explicit spawn_point (else they'd be filtered out); enemy
|
| 312 |
+
# actors are unchanged and always place.
|
| 313 |
+
# Per-band rush intensity HALVED vs medium (3× e1 + 1× e3 per
|
| 314 |
+
# latitude) — both bands always place (enemy actors don't honour
|
| 315 |
+
# spawn_point — CLAUDE.md) and the rusher bot commits both to
|
| 316 |
+
# the active centroid; doubling per-band on top of the always-
|
| 317 |
+
# both-place doubling would crater the rebuild window before
|
| 318 |
+
# proc completes. The controlled axis vs medium is the spawn
|
| 319 |
+
# variation, NOT additional pressure.
|
| 320 |
+
# max_turns 60 → reachable tick ≈ 5400 ≥ 4501 ✓.
|
| 321 |
+
hard:
|
| 322 |
+
description: >
|
| 323 |
+
Your FORWARD base (fact + refinery + 2 defenders) and your
|
| 324 |
+
deeper SAFE-zone MCV are both at a SEED-CHOSEN latitude
|
| 325 |
+
(NORTH y=14 or SOUTH y=26). Rusher bands at BOTH latitudes
|
| 326 |
+
converge on your forward base centroid; the forward base
|
| 327 |
+
CANNOT be held inside the deadline. Win by retreating,
|
| 328 |
+
deploying the safe-zone MCV AND building a refinery — both
|
| 329 |
+
inside the safe radius around YOUR matched safe zone
|
| 330 |
+
(NORTH (10,14) OR SOUTH (10,26), radius 8). Win when a
|
| 331 |
+
fact AND a proc are alive inside EITHER safe region AND
|
| 332 |
+
you have ≥1 own unit alive AND before tick 4500. Rebuilding
|
| 333 |
+
at the off-latitude safe zone, hold-forward, deploy-only-
|
| 334 |
+
no-rebuild, and stall all LOSE.
|
| 335 |
+
overrides:
|
| 336 |
+
actors:
|
| 337 |
+
# spawn_point 0 — NORTH (y=14) forward base + safe zone.
|
| 338 |
+
- {type: fact, owner: agent, position: [40, 14], spawn_point: 0}
|
| 339 |
+
- {type: proc, owner: agent, position: [44, 14], spawn_point: 0}
|
| 340 |
+
- {type: tent, owner: agent, position: [40, 17], spawn_point: 0}
|
| 341 |
+
- {type: e1, owner: agent, position: [42, 13], stance: 2, spawn_point: 0}
|
| 342 |
+
- {type: e1, owner: agent, position: [42, 15], stance: 2, spawn_point: 0}
|
| 343 |
+
- {type: mcv, owner: agent, position: [10, 14], spawn_point: 0}
|
| 344 |
+
- {type: powr, owner: agent, position: [6, 14], spawn_point: 0}
|
| 345 |
+
- {type: e1, owner: agent, position: [4, 14], stance: 0, spawn_point: 0}
|
| 346 |
+
# spawn_point 1 — SOUTH (y=26) forward base + safe zone.
|
| 347 |
+
- {type: fact, owner: agent, position: [40, 26], spawn_point: 1}
|
| 348 |
+
- {type: proc, owner: agent, position: [44, 26], spawn_point: 1}
|
| 349 |
+
- {type: tent, owner: agent, position: [40, 29], spawn_point: 1}
|
| 350 |
+
- {type: e1, owner: agent, position: [42, 25], stance: 2, spawn_point: 1}
|
| 351 |
+
- {type: e1, owner: agent, position: [42, 27], stance: 2, spawn_point: 1}
|
| 352 |
+
- {type: mcv, owner: agent, position: [10, 26], spawn_point: 1}
|
| 353 |
+
- {type: powr, owner: agent, position: [6, 26], spawn_point: 1}
|
| 354 |
+
- {type: e1, owner: agent, position: [4, 26], stance: 0, spawn_point: 1}
|
| 355 |
+
# Two seed-symmetric rush bands (one per candidate latitude).
|
| 356 |
+
# Enemy actors don't honour spawn_point so BOTH bands always
|
| 357 |
+
# place; the rusher bot commits each band to the agent
|
| 358 |
+
# centroid. Each band is HALF the medium intensity (3× e1 +
|
| 359 |
+
# 1× e3) so the TOTAL on-map enemy firepower roughly matches
|
| 360 |
+
# medium (6 e1 + 2 e3) — the controlled axis vs medium is
|
| 361 |
+
# the spawn variation, NOT additional pressure (which would
|
| 362 |
+
# crater the rebuild window before the proc completes).
|
| 363 |
+
- {type: e1, owner: enemy, position: [100, 14], stance: 3, count: 3}
|
| 364 |
+
- {type: e3, owner: enemy, position: [102, 14], stance: 3, count: 1}
|
| 365 |
+
- {type: e1, owner: enemy, position: [100, 26], stance: 3, count: 3}
|
| 366 |
+
- {type: e3, owner: enemy, position: [102, 26], stance: 3, count: 1}
|
| 367 |
+
- {type: fact, owner: enemy, position: [125, 20]}
|
| 368 |
+
win_condition:
|
| 369 |
+
all_of:
|
| 370 |
+
- any_of:
|
| 371 |
+
# NORTH safe zone (matches spawn_point 0).
|
| 372 |
+
- all_of:
|
| 373 |
+
- building_in_region: {type: fact, x: 10, y: 14, radius: 8, count: 1}
|
| 374 |
+
- building_in_region: {type: proc, x: 10, y: 14, radius: 8, count: 1}
|
| 375 |
+
# SOUTH safe zone (matches spawn_point 1).
|
| 376 |
+
- all_of:
|
| 377 |
+
- building_in_region: {type: fact, x: 10, y: 26, radius: 8, count: 1}
|
| 378 |
+
- building_in_region: {type: proc, x: 10, y: 26, radius: 8, count: 1}
|
| 379 |
+
- own_units_gte: 1
|
| 380 |
+
- within_ticks: 4500
|
| 381 |
+
fail_condition:
|
| 382 |
+
any_of:
|
| 383 |
+
- after_ticks: 4501
|
| 384 |
+
- not: {building_count_gte: {type: fact, n: 1}}
|
| 385 |
+
max_turns: 60
|
tests/test_def_retreat_and_rebuild.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""def-retreat-and-rebuild — strategic withdrawal + rebuild at depth.
|
| 2 |
+
|
| 3 |
+
Wave-8 REASONING pack. The agent operates a FORWARD base that is
|
| 4 |
+
being overrun by a heavy attack from the east; the forward base
|
| 5 |
+
CANNOT be held inside the tick budget. A parked MCV + powr sits at
|
| 6 |
+
a deeper SAFE zone in the west. The intended play: retreat (let
|
| 7 |
+
the forward base fall), DEPLOY the safe-zone MCV (creates a fact
|
| 8 |
+
inside the safe radius), then BUILD a refinery (`proc`) at the safe
|
| 9 |
+
zone so production stands up at depth.
|
| 10 |
+
|
| 11 |
+
The bar (validated scripted at seed 1, plus seeds 1..4 on hard):
|
| 12 |
+
• STALL (observe only) — LOSS. Rusher band razes the forward
|
| 13 |
+
base; the MCV is never deployed; no safe-region fact ever
|
| 14 |
+
appears; the `not building_count_gte:{type:fact,n:1}` fail
|
| 15 |
+
clause fires once the forward fact dies, OR the deadline
|
| 16 |
+
expires with the safe-region win clauses unsatisfied.
|
| 17 |
+
• HOLD-FORWARD (defenders engage; MCV never touched) — LOSS.
|
| 18 |
+
Same forward-fact razing + no safe-region rebuild.
|
| 19 |
+
• DEPLOY-ONLY (deploy the MCV but never build proc) — LOSS.
|
| 20 |
+
Safe-region fact appears post-deploy but the safe-region
|
| 21 |
+
proc clause is never satisfied → deadline expires.
|
| 22 |
+
• INTENDED RETREAT-AND-REBUILD — WIN. Deploy MCV early →
|
| 23 |
+
fact at safe radius; build('proc') + place_building inside
|
| 24 |
+
safe radius → both win clauses fire ~tick 1000-1100.
|
| 25 |
+
|
| 26 |
+
Validation is scripted (no model / network) — uses
|
| 27 |
+
`openra_bench.eval_core.run_level`.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
from __future__ import annotations
|
| 31 |
+
|
| 32 |
+
import types
|
| 33 |
+
from pathlib import Path
|
| 34 |
+
|
| 35 |
+
import pytest
|
| 36 |
+
|
| 37 |
+
pytest.importorskip("openra_rl_training", reason="Rust env wheel not installed")
|
| 38 |
+
from openra_bench.scenarios import load_pack
|
| 39 |
+
from openra_bench.scenarios.loader import compile_level
|
| 40 |
+
from openra_bench.scenarios.win_conditions import WinContext, evaluate
|
| 41 |
+
|
| 42 |
+
PACKS = Path(__file__).parent.parent / "openra_bench" / "scenarios" / "packs"
|
| 43 |
+
PACK_PATH = PACKS / "def-retreat-and-rebuild.yaml"
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# ── unit-level predicate checks ──────────────────────────────────────
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _ctx(own_buildings=(), tick=1000, n_units=1):
|
| 50 |
+
sig = types.SimpleNamespace(
|
| 51 |
+
game_tick=tick,
|
| 52 |
+
units_killed=0,
|
| 53 |
+
units_lost=0,
|
| 54 |
+
own_buildings=list(own_buildings),
|
| 55 |
+
own_building_types={str(t).lower() for (t, _, _) in own_buildings},
|
| 56 |
+
enemies_seen_ids=set(),
|
| 57 |
+
enemy_buildings_seen_ids=set(),
|
| 58 |
+
)
|
| 59 |
+
units = [
|
| 60 |
+
{"id": str(i), "type": "e1", "cell_x": 4, "cell_y": 20, "owner": "agent"}
|
| 61 |
+
for i in range(n_units)
|
| 62 |
+
]
|
| 63 |
+
return WinContext(signals=sig, render_state={"units_summary": units})
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def test_predicates_easy_win_requires_both_fact_and_proc_in_safe_region():
|
| 67 |
+
c = compile_level(load_pack(PACK_PATH), "easy")
|
| 68 |
+
# WINNING state: safe-region fact (deploy lands at ~(9,19)) + safe-
|
| 69 |
+
# region proc (placed at (12,20)), unit alive, in time.
|
| 70 |
+
safe_fact = ("fact", 9, 19)
|
| 71 |
+
safe_proc = ("proc", 12, 20)
|
| 72 |
+
assert evaluate(c.win_condition, _ctx([safe_fact, safe_proc], tick=2000))
|
| 73 |
+
assert not evaluate(c.fail_condition, _ctx([safe_fact, safe_proc], tick=2000))
|
| 74 |
+
|
| 75 |
+
# FAIL clauses on losing states.
|
| 76 |
+
# Forward base still alive but no safe rebuild → win unsatisfied.
|
| 77 |
+
fwd_fact = ("fact", 40, 20)
|
| 78 |
+
fwd_proc = ("proc", 44, 20)
|
| 79 |
+
assert not evaluate(c.win_condition, _ctx([fwd_fact, fwd_proc], tick=2000))
|
| 80 |
+
# No buildings at all → fact-loss fail.
|
| 81 |
+
assert evaluate(c.fail_condition, _ctx([], tick=2000))
|
| 82 |
+
# Past deadline ⇒ fail.
|
| 83 |
+
assert evaluate(c.fail_condition, _ctx([safe_fact, safe_proc], tick=4502))
|
| 84 |
+
# Deploy only (fact at safe, no proc) → win unsatisfied.
|
| 85 |
+
assert not evaluate(c.win_condition, _ctx([safe_fact], tick=2000))
|
| 86 |
+
# Proc-only at safe (no safe fact) → win unsatisfied.
|
| 87 |
+
assert not evaluate(c.win_condition, _ctx([fwd_fact, safe_proc], tick=2000))
|
| 88 |
+
# The fact alone (forward-base survivor) but no safe rebuild → loss.
|
| 89 |
+
assert not evaluate(c.win_condition, _ctx([fwd_fact], tick=2000))
|
| 90 |
+
# Deadline reachable inside max_turns (∼90 ticks/turn).
|
| 91 |
+
assert 4501 <= 93 + 90 * (c.max_turns - 1)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def test_predicates_medium_radius_8_boundary():
|
| 95 |
+
c = compile_level(load_pack(PACK_PATH), "medium")
|
| 96 |
+
# Inside the r=8 safe region around (10,20).
|
| 97 |
+
safe_fact = ("fact", 9, 19)
|
| 98 |
+
inside_proc = ("proc", 14, 22) # dx=4,dy=2 → 20 ≤ 64 ✓
|
| 99 |
+
assert evaluate(c.win_condition, _ctx([safe_fact, inside_proc], tick=2000))
|
| 100 |
+
# Outside the r=8 safe region — the proc is too far east.
|
| 101 |
+
outside_proc = ("proc", 20, 20) # dx=10 → 100 > 64
|
| 102 |
+
assert not evaluate(c.win_condition, _ctx([safe_fact, outside_proc], tick=2000))
|
| 103 |
+
assert 4501 <= 93 + 90 * (c.max_turns - 1)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def test_predicates_hard_either_safe_zone_wins():
|
| 107 |
+
c = compile_level(load_pack(PACK_PATH), "hard")
|
| 108 |
+
# NORTH safe-region rebuild satisfies win.
|
| 109 |
+
n_fact = ("fact", 9, 13)
|
| 110 |
+
n_proc = ("proc", 12, 14)
|
| 111 |
+
assert evaluate(c.win_condition, _ctx([n_fact, n_proc], tick=2000))
|
| 112 |
+
# SOUTH safe-region rebuild satisfies win.
|
| 113 |
+
s_fact = ("fact", 9, 25)
|
| 114 |
+
s_proc = ("proc", 12, 26)
|
| 115 |
+
assert evaluate(c.win_condition, _ctx([s_fact, s_proc], tick=2000))
|
| 116 |
+
# Far-east mid-y placement misses BOTH safe regions (radius 8
|
| 117 |
+
# around (10,14) and (10,26); a build at (40,20) is dx=30 from
|
| 118 |
+
# BOTH centres → well outside).
|
| 119 |
+
far_fact = ("fact", 40, 20)
|
| 120 |
+
far_proc = ("proc", 44, 20)
|
| 121 |
+
assert not evaluate(c.win_condition, _ctx([far_fact, far_proc], tick=2000))
|
| 122 |
+
# Mixed latitude (north fact + south proc) — neither region
|
| 123 |
+
# satisfies both clauses simultaneously → win unsatisfied.
|
| 124 |
+
assert not evaluate(c.win_condition, _ctx([n_fact, s_proc], tick=2000))
|
| 125 |
+
assert evaluate(c.fail_condition, _ctx([n_fact, n_proc], tick=4502))
|
| 126 |
+
assert 4501 <= 93 + 90 * (c.max_turns - 1)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def test_hard_has_two_spawn_point_groups():
|
| 130 |
+
"""Hard-tier curation contract: ≥2 distinct agent spawn_point
|
| 131 |
+
groups so the seed round-robins the forward base + safe zone
|
| 132 |
+
latitude (NORTH y=14 vs SOUTH y=26)."""
|
| 133 |
+
c = compile_level(load_pack(PACK_PATH), "hard")
|
| 134 |
+
groups = {
|
| 135 |
+
(a.spawn_point if a.spawn_point is not None else 0)
|
| 136 |
+
for a in c.scenario.actors
|
| 137 |
+
if a.owner == "agent"
|
| 138 |
+
}
|
| 139 |
+
assert len(groups) >= 2, f"hard needs ≥2 spawn_point groups, got {groups}"
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def test_pack_meta_fields_populated():
|
| 143 |
+
pack = load_pack(PACK_PATH)
|
| 144 |
+
assert pack.meta.id == "def-retreat-and-rebuild"
|
| 145 |
+
assert pack.meta.capability == "reasoning"
|
| 146 |
+
anchors = pack.meta.benchmark_anchor
|
| 147 |
+
assert isinstance(anchors, list) and anchors, "benchmark_anchor required"
|
| 148 |
+
joined = " ".join(anchors).lower()
|
| 149 |
+
# Anchors must reference the strategic-withdrawal / CICERO retreat /
|
| 150 |
+
# PlanBench replanning doctrine that motivates the pack.
|
| 151 |
+
assert "withdrawal" in joined or "cicero" in joined or "retreat" in joined
|
| 152 |
+
assert "planbench" in joined or "replanning" in joined or "continuity" in joined
|
| 153 |
+
for lvl in ("easy", "medium", "hard"):
|
| 154 |
+
c = compile_level(pack, lvl)
|
| 155 |
+
assert c.map_supported
|
| 156 |
+
assert c.win_condition is not None and c.fail_condition is not None
|
| 157 |
+
# Tools required for the capability: observe + deploy + build +
|
| 158 |
+
# place_building (the rebuild primitive) plus move/attack for
|
| 159 |
+
# the hold-forward attempt to be expressible (and lose).
|
| 160 |
+
tools = set(c.scenario.tools or [])
|
| 161 |
+
for t in ("observe", "deploy", "build", "place_building",
|
| 162 |
+
"move_units", "attack_unit"):
|
| 163 |
+
assert t in tools, f"{lvl}: missing tool {t}"
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def test_timeout_loss_reachable_every_level():
|
| 167 |
+
"""No draw degeneracy: the after_ticks fail must fit inside
|
| 168 |
+
max_turns on every level."""
|
| 169 |
+
pack = load_pack(PACK_PATH)
|
| 170 |
+
for lvl in ("easy", "medium", "hard"):
|
| 171 |
+
c = compile_level(pack, lvl)
|
| 172 |
+
assert 4501 <= 93 + 90 * (c.max_turns - 1), lvl
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
# ── engine-driven scripted policies ──────────────────────────────────
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def _find_mcv(rs):
|
| 179 |
+
for u in rs.get("units_summary", []) or []:
|
| 180 |
+
if str(u.get("type", "")).lower() == "mcv":
|
| 181 |
+
return u
|
| 182 |
+
return None
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def _stall_policy(rs, Command):
|
| 186 |
+
return [Command.observe()]
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _hold_forward_policy(rs, Command):
|
| 190 |
+
"""Engage the rushers with the 2 forward defenders; never touch
|
| 191 |
+
the safe MCV. The rush is too heavy → forward base razed AND no
|
| 192 |
+
safe-zone rebuild → LOSS."""
|
| 193 |
+
units = rs.get("units_summary", []) or []
|
| 194 |
+
# Forward defenders are the e1s NEAR the forward base (x ≥ 40).
|
| 195 |
+
defenders = [
|
| 196 |
+
u for u in units
|
| 197 |
+
if str(u.get("type", "")).lower() == "e1"
|
| 198 |
+
and int(u.get("cell_x", 0)) >= 40
|
| 199 |
+
]
|
| 200 |
+
enemies = [
|
| 201 |
+
e for e in (rs.get("enemy_summary", []) or [])
|
| 202 |
+
if not e.get("is_building") and int(e.get("cell_x", 0)) >= 50
|
| 203 |
+
]
|
| 204 |
+
cmds = []
|
| 205 |
+
for u in defenders:
|
| 206 |
+
if enemies:
|
| 207 |
+
ux, uy = u["cell_x"], u["cell_y"]
|
| 208 |
+
target = min(
|
| 209 |
+
enemies,
|
| 210 |
+
key=lambda e: (e["cell_x"] - ux) ** 2 + (e["cell_y"] - uy) ** 2,
|
| 211 |
+
)
|
| 212 |
+
cmds.append(Command.attack_unit([str(u["id"])], str(target["id"])))
|
| 213 |
+
return cmds or [Command.observe()]
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def _deploy_only_policy(rs, Command):
|
| 217 |
+
"""Deploy the safe-zone MCV (satisfies the safe-region FACT
|
| 218 |
+
clause) but never build the safe-region proc → win unsatisfied
|
| 219 |
+
on the proc clause → after_ticks LOSS."""
|
| 220 |
+
mcv = _find_mcv(rs)
|
| 221 |
+
if mcv is not None:
|
| 222 |
+
return [Command.deploy([str(mcv["id"])])]
|
| 223 |
+
return [Command.observe()]
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def _intended_retreat_and_rebuild_policy():
|
| 227 |
+
"""Deploy the safe-zone MCV (creates a fact inside the safe
|
| 228 |
+
radius), then build('proc') + place_building('proc') inside the
|
| 229 |
+
safe radius. The forward base is allowed to fall — the safe
|
| 230 |
+
rebuild carries the win clauses by the deadline."""
|
| 231 |
+
state = {"deployed": False, "safe_y": None}
|
| 232 |
+
|
| 233 |
+
def policy(rs, Command):
|
| 234 |
+
bldgs = rs.get("own_buildings", []) or []
|
| 235 |
+
if not state["deployed"]:
|
| 236 |
+
mcv = _find_mcv(rs)
|
| 237 |
+
if mcv is not None:
|
| 238 |
+
state["safe_y"] = int(mcv["cell_y"])
|
| 239 |
+
state["deployed"] = True
|
| 240 |
+
return [Command.deploy([str(mcv["id"])])]
|
| 241 |
+
sy = state["safe_y"] or 20
|
| 242 |
+
n_proc_in_safe = sum(
|
| 243 |
+
1 for b in bldgs
|
| 244 |
+
if b.get("type") == "proc"
|
| 245 |
+
and (int(b.get("cell_x", 0)) - 10) ** 2
|
| 246 |
+
+ (int(b.get("cell_y", 0)) - sy) ** 2
|
| 247 |
+
<= 64
|
| 248 |
+
)
|
| 249 |
+
prod = rs.get("production", []) or []
|
| 250 |
+
n_in_q = sum(
|
| 251 |
+
1 for p in prod
|
| 252 |
+
if isinstance(p, dict) and p.get("item") == "proc"
|
| 253 |
+
)
|
| 254 |
+
cmds = []
|
| 255 |
+
if n_proc_in_safe < 1:
|
| 256 |
+
if n_in_q == 0:
|
| 257 |
+
cmds.append(Command.build("proc"))
|
| 258 |
+
# Place at (12, safe_y) — adjacent to the post-deploy
|
| 259 |
+
# fact at ~(9, safe_y-1) AND inside the radius-8 safe
|
| 260 |
+
# region around (10, safe_y).
|
| 261 |
+
cmds.append(Command.place_building("proc", 12, sy))
|
| 262 |
+
return cmds or [Command.observe()]
|
| 263 |
+
|
| 264 |
+
return policy
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
|
| 268 |
+
def test_intended_retreat_and_rebuild_wins(level):
|
| 269 |
+
pytest.importorskip("openra_train")
|
| 270 |
+
from openra_bench.eval_core import run_level
|
| 271 |
+
|
| 272 |
+
c = compile_level(load_pack(PACK_PATH), level)
|
| 273 |
+
seeds = (1, 2, 3, 4) if level == "hard" else (1,)
|
| 274 |
+
for s in seeds:
|
| 275 |
+
res = run_level(c, _intended_retreat_and_rebuild_policy(), seed=s)
|
| 276 |
+
assert res.outcome == "win", (
|
| 277 |
+
f"{level} seed={s}: intended retreat-and-rebuild should "
|
| 278 |
+
f"WIN, got {res.outcome} after {res.turns} turns "
|
| 279 |
+
f"(tick={res.signals.game_tick}); "
|
| 280 |
+
f"buildings={res.signals.own_buildings}"
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
|
| 285 |
+
def test_stall_loses(level):
|
| 286 |
+
pytest.importorskip("openra_train")
|
| 287 |
+
from openra_bench.eval_core import run_level
|
| 288 |
+
|
| 289 |
+
c = compile_level(load_pack(PACK_PATH), level)
|
| 290 |
+
seeds = (1, 2, 3, 4) if level == "hard" else (1,)
|
| 291 |
+
for s in seeds:
|
| 292 |
+
res = run_level(c, _stall_policy, seed=s)
|
| 293 |
+
assert res.outcome == "loss", (
|
| 294 |
+
f"{level} seed={s}: stall must LOSE, got {res.outcome} "
|
| 295 |
+
f"after {res.turns} turns; "
|
| 296 |
+
f"buildings={sorted(res.signals.own_building_types)}"
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
|
| 301 |
+
def test_hold_forward_loses(level):
|
| 302 |
+
pytest.importorskip("openra_train")
|
| 303 |
+
from openra_bench.eval_core import run_level
|
| 304 |
+
|
| 305 |
+
c = compile_level(load_pack(PACK_PATH), level)
|
| 306 |
+
seeds = (1, 2, 3, 4) if level == "hard" else (1,)
|
| 307 |
+
for s in seeds:
|
| 308 |
+
res = run_level(c, _hold_forward_policy, seed=s)
|
| 309 |
+
assert res.outcome == "loss", (
|
| 310 |
+
f"{level} seed={s}: hold-forward must LOSE (forward base "
|
| 311 |
+
f"razed regardless AND no safe-zone rebuild), got "
|
| 312 |
+
f"{res.outcome} after {res.turns} turns; "
|
| 313 |
+
f"buildings={res.signals.own_buildings}"
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
|
| 318 |
+
def test_deploy_only_loses(level):
|
| 319 |
+
pytest.importorskip("openra_train")
|
| 320 |
+
from openra_bench.eval_core import run_level
|
| 321 |
+
|
| 322 |
+
c = compile_level(load_pack(PACK_PATH), level)
|
| 323 |
+
seeds = (1, 2, 3, 4) if level == "hard" else (1,)
|
| 324 |
+
for s in seeds:
|
| 325 |
+
res = run_level(c, _deploy_only_policy, seed=s)
|
| 326 |
+
assert res.outcome == "loss", (
|
| 327 |
+
f"{level} seed={s}: deploy-only (no proc rebuild) must "
|
| 328 |
+
f"LOSE, got {res.outcome} after {res.turns} turns; "
|
| 329 |
+
f"buildings={res.signals.own_buildings}"
|
| 330 |
+
)
|
tests/test_hard_tier.py
CHANGED
|
@@ -253,6 +253,26 @@ UPGRADED = [
|
|
| 253 |
# dispatch) must run concurrently from t=0 at the spawn-matched
|
| 254 |
# latitude.
|
| 255 |
"def-while-building",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
# Wave-7 reasoning pack — distributed defence across three
|
| 257 |
# concurrent attack lanes (graph min-cut / military multi-front /
|
| 258 |
# distributed-systems load-balancing anchor). Hard tier defines
|
|
|
|
| 253 |
# dispatch) must run concurrently from t=0 at the spawn-matched
|
| 254 |
# latitude.
|
| 255 |
"def-while-building",
|
| 256 |
+
# Wave-8 REASONING pack — strategic withdrawal + rebuild at depth
|
| 257 |
+
# (CICERO retreat / PlanBench replanning / business-continuity
|
| 258 |
+
# failover anchor). The agent's forward base (fact + proc + tent +
|
| 259 |
+
# 2 defenders) is being overrun by a heavy rusher band and cannot
|
| 260 |
+
# be held in the budget; the intended play is to RETREAT (let the
|
| 261 |
+
# forward base fall), DEPLOY the safe-zone MCV (creates a fact at
|
| 262 |
+
# depth), and BUILD a proc inside the safe radius. Hard tier
|
| 263 |
+
# defines two agent spawn_point groups (NORTH base latitude y=14 /
|
| 264 |
+
# SOUTH base latitude y=26) round-robined by seed; both forward
|
| 265 |
+
# base AND safe zone move with the spawn so the agent must
|
| 266 |
+
# rebuild at the latitude that matches its OWN spawn (a memorised
|
| 267 |
+
# "always rebuild at y=20" satisfies NEITHER safe region). Per-
|
| 268 |
+
# band rush intensity is HALVED vs medium (3× e1 + 1× e3 per
|
| 269 |
+
# latitude) since both rush bands always place (enemy actors
|
| 270 |
+
# don't honour spawn_point — CLAUDE.md) and the rusher bot
|
| 271 |
+
# commits both to the active agent centroid; the controlled axis
|
| 272 |
+
# vs medium is the spawn variation, not additional pressure
|
| 273 |
+
# (which would crater the rebuild window before the safe-zone
|
| 274 |
+
# proc completes).
|
| 275 |
+
"def-retreat-and-rebuild",
|
| 276 |
# Wave-7 reasoning pack — distributed defence across three
|
| 277 |
# concurrent attack lanes (graph min-cut / military multi-front /
|
| 278 |
# distributed-systems load-balancing anchor). Hard tier defines
|