Spaces:
Running
feat(scenario): def-position-expected-direction — fortify intel'd threat axis (ERQA / MicroRTS anchor)
Browse filesGroup C reasoning pack on rush-hour-arena: the mission brief states
WHERE the enemy will attack from (NORTH on easy/medium; matched-side
NORTH/SOUTH per spawn on hard) and the agent must commit its pillbox
triple to that lane.
Win idiom: building_count_gte:{pbox,3} AND building_in_region:{pbox,
NORTH_centre, r=6, 3} AND has_building:fact AND own_units_gte:3
AND within_ticks:5400 (paired with after_ticks:5401 for a real
reachable timeout LOSS).
Hard tier defines two agent spawn_point groups (NORTH base y=12 /
SOUTH base y=28) round-robined by seed; the win clause is any_of
over the two correct region orientations so 'NORTH of YOUR base'
flips per seed and a single memorised pbox cell cannot generalise.
Scripted-bar (all seeds 1..4): intended NORTH-pbox triple WINS
every level + every hard seed; stall / defenses-SOUTH / no-defenses
ALL LOSE (real reachable timeout LOSS) every level + every hard
seed — never a draw.
Model smoke: Qwen/Qwen3.6-Plus on medium seed 1 = LOSS (composite
0.3305) — valid discrimination, the model failed to commit defence
to the declared threat axis.
Anchors: ERQA spatial commit, MicroRTS terrain-aware defence,
military pre-positioned defence, intel-driven facility hardening.
|
@@ -0,0 +1,320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# def-position-expected-direction.yaml
|
| 2 |
+
#
|
| 3 |
+
# REASONING capability — DEFENSE POSITIONING BASED ON DECLARED THREAT
|
| 4 |
+
# DIRECTION. The model is TOLD where the enemy will attack from (the
|
| 5 |
+
# objective brief states the intel: "rushers will come from the
|
| 6 |
+
# NORTH"). The decision is purely one of spatial commitment: invest
|
| 7 |
+
# the defence budget on the CORRECT lane — the NORTH lane — and
|
| 8 |
+
# the rush is blunted; commit the SAME budget on the WRONG lane
|
| 9 |
+
# (SOUTH) and the rushers walk past, the deadline expires (the
|
| 10 |
+
# wrong-direction pbox cannot satisfy the NORTH region clause), and
|
| 11 |
+
# the run loses. Building no defences at all also loses (no pbox).
|
| 12 |
+
#
|
| 13 |
+
# Win idiom:
|
| 14 |
+
# * `building_count_gte:{pbox, 3}` — three pillboxes total
|
| 15 |
+
# (forces a real defence commitment, not a token pbox);
|
| 16 |
+
# * `building_in_region:{pbox, NORTH_centre, r, 3}` — ALL THREE
|
| 17 |
+
# must be in the NORTH-of-base region (a SOUTH-built triple
|
| 18 |
+
# does not satisfy this clause);
|
| 19 |
+
# * `has_building: fact` — the construction yard still stands
|
| 20 |
+
# (sanity / anti-degeneracy);
|
| 21 |
+
# * `own_units_gte: 3` — the four pre-placed defenders must
|
| 22 |
+
# mostly survive (a chaotic micro that bleeds them out fails
|
| 23 |
+
# this even with the NORTH pbox triple in place);
|
| 24 |
+
# * `within_ticks: 5400` paired with a `after_ticks: 5401` fail
|
| 25 |
+
# ⇒ reachable timeout LOSS, never a draw.
|
| 26 |
+
#
|
| 27 |
+
# Real-world / benchmark anchors:
|
| 28 |
+
# - Pre-positioned defence based on known threat direction;
|
| 29 |
+
# - ERQA spatial commit (commit to one of N candidate spatial
|
| 30 |
+
# regions given a textual cue);
|
| 31 |
+
# - MicroRTS terrain-aware defence;
|
| 32 |
+
# - military prepared-defence doctrine (intel-driven facility
|
| 33 |
+
# hardening on the threat axis).
|
| 34 |
+
#
|
| 35 |
+
# Distinct from siblings (Group C):
|
| 36 |
+
# * defense-rush-survive — REACTIVE rush-defence under tempo (the
|
| 37 |
+
# threat axis is one fixed lane; the decision is balancing
|
| 38 |
+
# defence + throughput under clock). This pack is purely about
|
| 39 |
+
# COMMITTING DEFENCE TO THE CORRECT AXIS given declared intel.
|
| 40 |
+
# * tech-turtle-defensive-tech — sequenced fortify-then-tech
|
| 41 |
+
# (the axis is fixed; the decision is the order, not the side).
|
| 42 |
+
# * expansion-turtle-1-base-fortified — single-base mass-defence
|
| 43 |
+
# (no directional axis; the whole perimeter must be covered).
|
| 44 |
+
#
|
| 45 |
+
# Validate:
|
| 46 |
+
# cd /Users/berta/Projects/OpenRA-Bench && \
|
| 47 |
+
# python3 -m openra_bench.scenarios.validate \
|
| 48 |
+
# openra_bench/scenarios/packs/def-position-expected-direction.yaml
|
| 49 |
+
|
| 50 |
+
meta:
|
| 51 |
+
id: def-position-expected-direction
|
| 52 |
+
title: 'Fortify the Threat Axis — Pre-Position Defence on the Intel Lane'
|
| 53 |
+
capability: reasoning
|
| 54 |
+
real_world_meaning: >
|
| 55 |
+
Spatial commitment of a finite defence budget to the CORRECT
|
| 56 |
+
direction given declared intel. The mission brief states where the
|
| 57 |
+
enemy will attack from; the agent must invest its pillboxes on
|
| 58 |
+
that lane (and not the opposite lane). A correct-axis commitment
|
| 59 |
+
blunts the rush; a wrong-axis commitment lets the rushers walk
|
| 60 |
+
past and the run loses on the spatial clause. This is the
|
| 61 |
+
decision drill of pre-positioned defence: not "should I defend?"
|
| 62 |
+
but "WHERE should I defend, given what intel says?".
|
| 63 |
+
robotics_analogue: >
|
| 64 |
+
Intel-driven facility hardening: a security operator is told the
|
| 65 |
+
threat axis (a perimeter breach is expected on the NORTH gate)
|
| 66 |
+
and must commit the available barriers / sensors / responders to
|
| 67 |
+
that axis. Spending the same budget on the SOUTH gate satisfies
|
| 68 |
+
no objective. This pack tests whether the policy converts a
|
| 69 |
+
textual directional cue into a CORRECT spatial commitment.
|
| 70 |
+
benchmark_anchor:
|
| 71 |
+
- 'ERQA spatial commit'
|
| 72 |
+
- 'MicroRTS terrain-aware defense'
|
| 73 |
+
- 'military pre-positioned defense'
|
| 74 |
+
- 'intel-driven facility hardening'
|
| 75 |
+
author: openra-bench
|
| 76 |
+
|
| 77 |
+
# rush-hour-arena (128×40, playable x∈[2..126], y∈[2..38]). Agent
|
| 78 |
+
# base at (20, 20) with the fact + tent + powr + 4 starter e1s
|
| 79 |
+
# pre-placed (so 3 pbox can build immediately — pbox needs tent which
|
| 80 |
+
# needs powr — and the throughput SLA `own_units_gte:3` is trivially
|
| 81 |
+
# satisfied at start; the rush will erode it unless the NORTH lane is
|
| 82 |
+
# covered). The scripted `rusher` bot concentrates its whole band on
|
| 83 |
+
# the agent centroid (~8-tick interval, see scripted_bot.rs::Rusher)
|
| 84 |
+
# — declared NORTH of the base, so the band charges SOUTH straight at
|
| 85 |
+
# the fact and is intercepted by a NORTH-placed pbox triple.
|
| 86 |
+
base_map: rush-hour-arena
|
| 87 |
+
|
| 88 |
+
# Pack-wide budget — funds exactly the intended NORTH-pbox triple
|
| 89 |
+
# (3 × 600cr = 1800cr) with a small buffer (~600) for any
|
| 90 |
+
# replacement e1 the agent decides to queue. Spamming pbox in the
|
| 91 |
+
# wrong direction burns the same budget without satisfying the
|
| 92 |
+
# NORTH region clause.
|
| 93 |
+
starting_cash: 2400
|
| 94 |
+
|
| 95 |
+
base:
|
| 96 |
+
agent:
|
| 97 |
+
faction: allies
|
| 98 |
+
# Relentless concentrated charge at the agent centroid every
|
| 99 |
+
# ~8 ticks — the canonical rush-defence opponent. The bot's
|
| 100 |
+
# spawn position (NORTH for easy/medium, NORTH+SOUTH on hard) is
|
| 101 |
+
# the declared intel the model gets in the brief.
|
| 102 |
+
enemy:
|
| 103 |
+
faction: soviet
|
| 104 |
+
bot_type: rusher
|
| 105 |
+
tools:
|
| 106 |
+
- observe
|
| 107 |
+
- build
|
| 108 |
+
- place_building
|
| 109 |
+
- move_units
|
| 110 |
+
- attack_unit
|
| 111 |
+
- attack_move
|
| 112 |
+
- stop
|
| 113 |
+
planning: true
|
| 114 |
+
# Re-decide the instant a rusher unit is spotted at the lane mouth
|
| 115 |
+
# or a defender is lost — defence commitment is reactive.
|
| 116 |
+
interrupts:
|
| 117 |
+
enemy_unit_spotted: true
|
| 118 |
+
own_unit_destroyed: true
|
| 119 |
+
termination:
|
| 120 |
+
max_ticks: 12000
|
| 121 |
+
actors:
|
| 122 |
+
# Base-pack defaults; each level overrides with the spawn-specific
|
| 123 |
+
# layout. (Anti-empty-actors sanity for the loader.)
|
| 124 |
+
- {type: fact, owner: agent, position: [20, 20]}
|
| 125 |
+
|
| 126 |
+
levels:
|
| 127 |
+
# ── EASY ── the bare directional-commit skill. A LIGHT rusher push
|
| 128 |
+
# (3 e1) comes from the NORTH (spawn at (20, 6)). The model is told
|
| 129 |
+
# in the brief "rushers will come from the NORTH" and must place
|
| 130 |
+
# three pillboxes in the NORTH region of its base (around (20, 14),
|
| 131 |
+
# radius 6). The starting 4 defenders survive the light push so
|
| 132 |
+
# own_units_gte:3 holds; the deadline is forgiving but real.
|
| 133 |
+
# max_turns 66 → reachable tick 93+90·65 = 5943 ≥ 5402 even with
|
| 134 |
+
# interrupts firing (per-turn step shrinks below 90 ticks when an
|
| 135 |
+
# interrupt fires; the empirical floor on this pack is ~86 t/turn,
|
| 136 |
+
# so 66 turns × 86 ≈ 5676 still clears the 5401 deadline).
|
| 137 |
+
easy:
|
| 138 |
+
description: >
|
| 139 |
+
INTEL: the enemy will attack from the NORTH. You start with a
|
| 140 |
+
construction yard, a barracks, a power plant, and four rifle
|
| 141 |
+
defenders at (20, 20). Build at least THREE pillboxes (pbox)
|
| 142 |
+
and place them in the NORTH region of your base (within radius
|
| 143 |
+
6 of (20, 14)) — that is, BETWEEN your base and the enemy's
|
| 144 |
+
approach. Keep your construction yard standing AND ≥3 of your
|
| 145 |
+
own units alive. Win before tick 5400. Placing pillboxes south
|
| 146 |
+
of your base does not satisfy the NORTH-region clause; building
|
| 147 |
+
no pillboxes never reaches the count; stalling loses on the
|
| 148 |
+
clock.
|
| 149 |
+
overrides:
|
| 150 |
+
actors:
|
| 151 |
+
# Agent base — fact at the centre, tent + powr supporting
|
| 152 |
+
# so the pbox tech chain is satisfied from turn 1 (no
|
| 153 |
+
# waiting on powr→tent before pbox). Tent placed WEST of the
|
| 154 |
+
# fact, powr SOUTH; both well clear of the NORTH defence
|
| 155 |
+
# region so a NORTH-placed pbox triple is unobstructed.
|
| 156 |
+
- {type: fact, owner: agent, position: [20, 20]}
|
| 157 |
+
- {type: tent, owner: agent, position: [16, 20]}
|
| 158 |
+
- {type: powr, owner: agent, position: [20, 24]}
|
| 159 |
+
# Four starter defenders south of the base (away from the
|
| 160 |
+
# NORTH lane mouth) — they survive a light push and satisfy
|
| 161 |
+
# `own_units_gte:3` even if one dies; positioning them away
|
| 162 |
+
# from the lane avoids the auto-target-fire complication.
|
| 163 |
+
- {type: e1, owner: agent, position: [22, 24], stance: 2}
|
| 164 |
+
- {type: e1, owner: agent, position: [24, 24], stance: 2}
|
| 165 |
+
- {type: e1, owner: agent, position: [22, 26], stance: 2}
|
| 166 |
+
- {type: e1, owner: agent, position: [24, 26], stance: 2}
|
| 167 |
+
# Light rusher push from the NORTH — 3 e1 at (20, 6).
|
| 168 |
+
# The rusher bot drives them straight at the agent
|
| 169 |
+
# centroid (the fact at 20,20); a NORTH pbox triple at
|
| 170 |
+
# y≈12..16 intercepts them on the way.
|
| 171 |
+
- {type: e1, owner: enemy, position: [20, 6], stance: 3, count: 3}
|
| 172 |
+
# Unarmed high-HP enemy marker far east — keeps the episode
|
| 173 |
+
# alive past the rush, so the win/fail predicate has time
|
| 174 |
+
# to evaluate (anti-DRAW per CLAUDE.md footgun).
|
| 175 |
+
- {type: fact, owner: enemy, position: [120, 20]}
|
| 176 |
+
win_condition:
|
| 177 |
+
all_of:
|
| 178 |
+
- building_count_gte: {type: pbox, n: 3}
|
| 179 |
+
- building_in_region: {type: pbox, x: 20, y: 14, radius: 6, count: 3}
|
| 180 |
+
- has_building: fact
|
| 181 |
+
- own_units_gte: 3
|
| 182 |
+
- within_ticks: 5400
|
| 183 |
+
fail_condition:
|
| 184 |
+
any_of:
|
| 185 |
+
- after_ticks: 5401
|
| 186 |
+
- not: {has_building: fact}
|
| 187 |
+
- not: {own_units_gte: 1}
|
| 188 |
+
max_turns: 66
|
| 189 |
+
|
| 190 |
+
# ── MEDIUM ── +1 controlled variable: a STRONGER northern push
|
| 191 |
+
# (5 e1 + 1 e3). The starter 4 defenders can no longer absorb the
|
| 192 |
+
# rush on their own — a NORTH pbox triple is REQUIRED to blunt the
|
| 193 |
+
# rocket soldier (e3 splash) before it ploughs through the
|
| 194 |
+
# defenders. Wrong-direction pbox (SOUTH) leaves the NORTH lane
|
| 195 |
+
# naked, the e3 kills the defenders, `own_units_gte:3` fails AND
|
| 196 |
+
# `building_in_region` fails ⇒ double LOSS path.
|
| 197 |
+
# max_turns 62 → reachable tick 5583 ≥ 5402.
|
| 198 |
+
medium:
|
| 199 |
+
description: >
|
| 200 |
+
INTEL: the enemy will attack from the NORTH. You start with a
|
| 201 |
+
construction yard, a barracks, a power plant, and four rifle
|
| 202 |
+
defenders at (20, 20). A stronger northern push — 5 rifle
|
| 203 |
+
infantry and 1 rocket soldier — will charge your base. Build
|
| 204 |
+
at least THREE pillboxes (pbox) and place all three in the
|
| 205 |
+
NORTH region of your base (within radius 6 of (20, 14)). Keep
|
| 206 |
+
the construction yard standing AND ≥3 of your own units alive
|
| 207 |
+
before tick 5400. Placing pillboxes south of your base does
|
| 208 |
+
not satisfy the NORTH-region clause AND leaves the northern
|
| 209 |
+
lane naked (the rocket soldier ploughs through the defenders);
|
| 210 |
+
building no pillboxes loses on both counts; stalling loses on
|
| 211 |
+
the clock.
|
| 212 |
+
overrides:
|
| 213 |
+
actors:
|
| 214 |
+
- {type: fact, owner: agent, position: [20, 20]}
|
| 215 |
+
- {type: tent, owner: agent, position: [16, 20]}
|
| 216 |
+
- {type: powr, owner: agent, position: [20, 24]}
|
| 217 |
+
- {type: e1, owner: agent, position: [22, 24], stance: 2}
|
| 218 |
+
- {type: e1, owner: agent, position: [24, 24], stance: 2}
|
| 219 |
+
- {type: e1, owner: agent, position: [22, 26], stance: 2}
|
| 220 |
+
- {type: e1, owner: agent, position: [24, 26], stance: 2}
|
| 221 |
+
# Stronger northern push: 5 e1 + 1 e3. The e3 is the
|
| 222 |
+
# discriminator — it has splash + AT damage; a defender-only
|
| 223 |
+
# screen cannot kill it before it bursts the screen.
|
| 224 |
+
- {type: e1, owner: enemy, position: [20, 6], stance: 3, count: 5}
|
| 225 |
+
- {type: e3, owner: enemy, position: [22, 6], stance: 3, count: 1}
|
| 226 |
+
- {type: fact, owner: enemy, position: [120, 20]}
|
| 227 |
+
win_condition:
|
| 228 |
+
all_of:
|
| 229 |
+
- building_count_gte: {type: pbox, n: 3}
|
| 230 |
+
- building_in_region: {type: pbox, x: 20, y: 14, radius: 6, count: 3}
|
| 231 |
+
- has_building: fact
|
| 232 |
+
- own_units_gte: 3
|
| 233 |
+
- within_ticks: 5400
|
| 234 |
+
fail_condition:
|
| 235 |
+
any_of:
|
| 236 |
+
- after_ticks: 5401
|
| 237 |
+
- not: {has_building: fact}
|
| 238 |
+
- not: {own_units_gte: 1}
|
| 239 |
+
max_turns: 66
|
| 240 |
+
|
| 241 |
+
# ── HARD ── +1 controlled variable: TWO spawn_point groups — the
|
| 242 |
+
# AGENT base round-robins between a NORTH-staged base (centre
|
| 243 |
+
# (20, 12), enemy comes from y≈4) and a SOUTH-staged base (centre
|
| 244 |
+
# (20, 28), enemy comes from y≈36). The intel rotates accordingly:
|
| 245 |
+
# for the NORTH base, "fortify NORTH" means defend at (20, 6)
|
| 246 |
+
# (between the base and the enemy north of it); for the SOUTH
|
| 247 |
+
# base, "fortify SOUTH" means defend at (20, 34) (between the
|
| 248 |
+
# base and the enemy south of it). The win predicate uses `any_of`
|
| 249 |
+
# over the two correct region orientations — the agent must place
|
| 250 |
+
# its pbox triple on the lane that matches its OWN spawn (a
|
| 251 |
+
# NORTH-base pbox triple does NOT satisfy the SOUTH-base region).
|
| 252 |
+
# The rusher hits from BOTH latitudes (each spawn faces the side
|
| 253 |
+
# matching its base — see comment on enemy actors below).
|
| 254 |
+
# max_turns 62 → reachable tick 5583 ≥ 5402.
|
| 255 |
+
hard:
|
| 256 |
+
description: >
|
| 257 |
+
INTEL: the enemy will attack from the side FACING your base —
|
| 258 |
+
from the NORTH if your base is in the NORTH half, from the
|
| 259 |
+
SOUTH if your base is in the SOUTH half. Identify your base's
|
| 260 |
+
latitude from the observation, then build at least THREE
|
| 261 |
+
pillboxes (pbox) and place all three BETWEEN your base and the
|
| 262 |
+
threat — within radius 6 of either (20, 6) (if your base is at
|
| 263 |
+
y=12) or (20, 34) (if your base is at y=28). Keep the
|
| 264 |
+
construction yard standing AND ≥3 of your own units alive
|
| 265 |
+
before tick 5400. Defending on the WRONG side (the side away
|
| 266 |
+
from your base's threat) does not satisfy the region clause
|
| 267 |
+
AND leaves the threat axis naked; building no pillboxes loses
|
| 268 |
+
both clauses; stalling loses on the clock.
|
| 269 |
+
overrides:
|
| 270 |
+
actors:
|
| 271 |
+
# ── NORTH spawn (spawn_point 0) — base centred at (20, 12).
|
| 272 |
+
# Defence region for THIS spawn: (20, 6) r 5 (between the
|
| 273 |
+
# base and the rusher band north of it at y=4).
|
| 274 |
+
- {type: fact, owner: agent, position: [20, 12], spawn_point: 0}
|
| 275 |
+
- {type: tent, owner: agent, position: [16, 12], spawn_point: 0}
|
| 276 |
+
- {type: powr, owner: agent, position: [20, 16], spawn_point: 0}
|
| 277 |
+
- {type: e1, owner: agent, position: [22, 16], stance: 2, spawn_point: 0}
|
| 278 |
+
- {type: e1, owner: agent, position: [24, 16], stance: 2, spawn_point: 0}
|
| 279 |
+
- {type: e1, owner: agent, position: [22, 18], stance: 2, spawn_point: 0}
|
| 280 |
+
- {type: e1, owner: agent, position: [24, 18], stance: 2, spawn_point: 0}
|
| 281 |
+
# ── SOUTH spawn (spawn_point 1) — base centred at (20, 28).
|
| 282 |
+
# Defence region for THIS spawn: (20, 34) r 5 (between the
|
| 283 |
+
# base and the rusher band south of it at y=36).
|
| 284 |
+
- {type: fact, owner: agent, position: [20, 28], spawn_point: 1}
|
| 285 |
+
- {type: tent, owner: agent, position: [16, 28], spawn_point: 1}
|
| 286 |
+
- {type: powr, owner: agent, position: [20, 24], spawn_point: 1}
|
| 287 |
+
- {type: e1, owner: agent, position: [22, 22], stance: 2, spawn_point: 1}
|
| 288 |
+
- {type: e1, owner: agent, position: [24, 22], stance: 2, spawn_point: 1}
|
| 289 |
+
- {type: e1, owner: agent, position: [22, 24], stance: 2, spawn_point: 1}
|
| 290 |
+
- {type: e1, owner: agent, position: [24, 24], stance: 2, spawn_point: 1}
|
| 291 |
+
# Enemy rushers — CLAUDE.md: enemy actors WITHOUT a
|
| 292 |
+
# spawn_point are placed regardless of the chosen agent
|
| 293 |
+
# group, so we declare BOTH a north and a south rush band.
|
| 294 |
+
# The `rusher` bot charges the agent centroid; whichever
|
| 295 |
+
# spawn is active, the matching-side band is the immediate
|
| 296 |
+
# threat (the opposite band has further to walk and the
|
| 297 |
+
# matching-side correct pbox triple will already be
|
| 298 |
+
# absorbing the lead band by the time the far band closes).
|
| 299 |
+
- {type: e1, owner: enemy, position: [20, 4], stance: 3, count: 4}
|
| 300 |
+
- {type: e3, owner: enemy, position: [22, 4], stance: 3, count: 1}
|
| 301 |
+
- {type: e1, owner: enemy, position: [20, 36], stance: 3, count: 4}
|
| 302 |
+
- {type: e3, owner: enemy, position: [22, 36], stance: 3, count: 1}
|
| 303 |
+
- {type: fact, owner: enemy, position: [120, 20]}
|
| 304 |
+
# Two CORRECT region orientations — the pbox triple must sit
|
| 305 |
+
# between THIS spawn's base and THIS spawn's matching rush band.
|
| 306 |
+
win_condition:
|
| 307 |
+
all_of:
|
| 308 |
+
- building_count_gte: {type: pbox, n: 3}
|
| 309 |
+
- any_of:
|
| 310 |
+
- building_in_region: {type: pbox, x: 20, y: 6, radius: 5, count: 3}
|
| 311 |
+
- building_in_region: {type: pbox, x: 20, y: 34, radius: 5, count: 3}
|
| 312 |
+
- has_building: fact
|
| 313 |
+
- own_units_gte: 3
|
| 314 |
+
- within_ticks: 5400
|
| 315 |
+
fail_condition:
|
| 316 |
+
any_of:
|
| 317 |
+
- after_ticks: 5401
|
| 318 |
+
- not: {has_building: fact}
|
| 319 |
+
- not: {own_units_gte: 1}
|
| 320 |
+
max_turns: 66
|
|
@@ -0,0 +1,339 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""def-position-expected-direction scenario pack, full loop on Rust.
|
| 2 |
+
|
| 3 |
+
REASONING capability — DEFENSE POSITIONING BASED ON DECLARED THREAT
|
| 4 |
+
DIRECTION. The model is TOLD where the enemy will attack from
|
| 5 |
+
(the objective brief states the intel: "rushers from the NORTH").
|
| 6 |
+
The decision is purely one of SPATIAL COMMITMENT: invest the
|
| 7 |
+
defence budget on the CORRECT lane and the rush is blunted; commit
|
| 8 |
+
the same budget on the WRONG lane (SOUTH) and the rushers walk past,
|
| 9 |
+
the deadline expires (the wrong-direction pbox cannot satisfy the
|
| 10 |
+
NORTH region clause), and the run loses. Building no defences at
|
| 11 |
+
all also loses (no pbox).
|
| 12 |
+
|
| 13 |
+
The win predicate makes the directional axis load-bearing:
|
| 14 |
+
|
| 15 |
+
* `building_count_gte:{pbox, 3}` ⇒ three pillboxes total (forces a
|
| 16 |
+
real defence commitment, not a token pbox);
|
| 17 |
+
* `building_in_region:{pbox, NORTH_centre, r=6, 3}` ⇒ ALL THREE must
|
| 18 |
+
sit in the NORTH region — a SOUTH-built triple cannot satisfy it;
|
| 19 |
+
* `has_building: fact` ⇒ the construction yard still stands;
|
| 20 |
+
* `own_units_gte: 3` ⇒ the throughput SLA (the four pre-placed
|
| 21 |
+
defenders must mostly survive);
|
| 22 |
+
* `within_ticks: 5400` paired with `after_ticks: 5401` ⇒ a non-
|
| 23 |
+
finisher is a real reachable timeout LOSS (max_turns 62 reaches
|
| 24 |
+
tick 5583 in interrupt mode), never a draw.
|
| 25 |
+
|
| 26 |
+
These tests prove deterministically (no model / no network) that:
|
| 27 |
+
|
| 28 |
+
* the intended NORTH-pbox triple WINS every level + every hard seed;
|
| 29 |
+
* stall, build-defences-SOUTH (wrong direction), and build-no-
|
| 30 |
+
defences ALL LOSE every level + every hard seed (a real LOSS,
|
| 31 |
+
never a draw);
|
| 32 |
+
* `after_ticks` is reachable inside `max_turns`;
|
| 33 |
+
* the hard tier defines ≥2 spawn_point groups so the threat axis
|
| 34 |
+
rotates by seed (single-cell memorisation cannot generalise).
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
from __future__ import annotations
|
| 38 |
+
|
| 39 |
+
import pytest
|
| 40 |
+
|
| 41 |
+
pytest.importorskip("openra_train", reason="Rust env wheel not installed")
|
| 42 |
+
pytest.importorskip("openra_rl_training", reason="Rust env wheel not installed")
|
| 43 |
+
|
| 44 |
+
from openra_bench.eval_core import run_level
|
| 45 |
+
from openra_bench.scenarios import load_pack
|
| 46 |
+
from openra_bench.scenarios.loader import PACKS_DIR, compile_level
|
| 47 |
+
|
| 48 |
+
PACK = PACKS_DIR / "def-position-expected-direction.yaml"
|
| 49 |
+
LEVELS = ("easy", "medium", "hard")
|
| 50 |
+
SEEDS = (1, 2, 3, 4)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# ── helpers ──────────────────────────────────────────────────────────
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _fact_xy(rs):
|
| 57 |
+
fact = next(
|
| 58 |
+
(b for b in (rs.get("own_buildings") or [])
|
| 59 |
+
if b.get("type") == "fact"),
|
| 60 |
+
None,
|
| 61 |
+
)
|
| 62 |
+
if fact is None:
|
| 63 |
+
return None
|
| 64 |
+
return int(fact["cell_x"]), int(fact["cell_y"])
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# ── scripted policies ────────────────────────────────────────────────
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def stall(rs, C):
|
| 71 |
+
"""Observe-only. Never builds anything; never satisfies the pbox
|
| 72 |
+
count or the region clauses → reachable timeout LOSS."""
|
| 73 |
+
return [C.observe()]
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def make_intended():
|
| 77 |
+
"""Build THREE pbox in the NORTH region of the agent base. The
|
| 78 |
+
NORTH side is `fy - 6` from the base (for easy/medium the base
|
| 79 |
+
is at y=20 so pbox at y≈14; for the hard NORTH spawn at y=12
|
| 80 |
+
pbox at y≈6; for the hard SOUTH spawn at y=28 pbox at y≈34).
|
| 81 |
+
Detects spawn by reading fact y and picks the side AWAY from
|
| 82 |
+
the centre (rushers come from the OUTSIDE on hard; on easy/
|
| 83 |
+
medium they come from the NORTH so the centre IS the rush
|
| 84 |
+
direction)."""
|
| 85 |
+
|
| 86 |
+
def policy(rs, C):
|
| 87 |
+
own_b = rs.get("own_buildings") or []
|
| 88 |
+
types = [b.get("type") for b in own_b]
|
| 89 |
+
pbox_count = sum(1 for t in types if t == "pbox")
|
| 90 |
+
prod = rs.get("production") or []
|
| 91 |
+
prod_items = [
|
| 92 |
+
p.get("item") for p in prod if isinstance(p, dict)
|
| 93 |
+
]
|
| 94 |
+
xy = _fact_xy(rs)
|
| 95 |
+
if xy is None:
|
| 96 |
+
return [C.observe()]
|
| 97 |
+
fx, fy = xy
|
| 98 |
+
# Pick the defence side: for easy/medium fact at y=20 →
|
| 99 |
+
# NORTH lane (fy-6 = 14). For hard NORTH spawn (fy=12) →
|
| 100 |
+
# NORTH (fy-6 = 6). For hard SOUTH spawn (fy=28) → SOUTH
|
| 101 |
+
# (fy+6 = 34). Heuristic: if fact y is in the SOUTH half
|
| 102 |
+
# (y>20), defend SOUTH; otherwise defend NORTH.
|
| 103 |
+
if fy > 20:
|
| 104 |
+
target_y = fy + 6 # SOUTH of base
|
| 105 |
+
else:
|
| 106 |
+
target_y = fy - 6 if fy >= 12 else fy - 6 # NORTH of base
|
| 107 |
+
cmds = []
|
| 108 |
+
if pbox_count < 3:
|
| 109 |
+
if prod_items.count("pbox") + pbox_count < 3:
|
| 110 |
+
cmds.append(C.build("pbox"))
|
| 111 |
+
# Stagger placement so 3 pbox sit in a tight cluster on
|
| 112 |
+
# the threat lane (all within radius 6 of (fx, target_y)).
|
| 113 |
+
dx = -2 + 2 * pbox_count
|
| 114 |
+
cmds.append(
|
| 115 |
+
C.place_building("pbox", fx + dx, target_y)
|
| 116 |
+
)
|
| 117 |
+
if not cmds:
|
| 118 |
+
cmds.append(C.observe())
|
| 119 |
+
return cmds
|
| 120 |
+
|
| 121 |
+
return policy
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def make_defenses_south():
|
| 125 |
+
"""Build THREE pbox in the WRONG region (the SOUTH side of the
|
| 126 |
+
base when the intel says NORTH, and vice versa for the hard SOUTH
|
| 127 |
+
spawn). Satisfies `building_count_gte:pbox,3` but NEVER the
|
| 128 |
+
`building_in_region` NORTH clause ⇒ deadline LOSS — AND the
|
| 129 |
+
NORTH lane is left naked so the rusher band tears through the
|
| 130 |
+
starter defenders (additional LOSS path on `own_units_gte`)."""
|
| 131 |
+
|
| 132 |
+
def policy(rs, C):
|
| 133 |
+
own_b = rs.get("own_buildings") or []
|
| 134 |
+
types = [b.get("type") for b in own_b]
|
| 135 |
+
pbox_count = sum(1 for t in types if t == "pbox")
|
| 136 |
+
prod = rs.get("production") or []
|
| 137 |
+
prod_items = [
|
| 138 |
+
p.get("item") for p in prod if isinstance(p, dict)
|
| 139 |
+
]
|
| 140 |
+
xy = _fact_xy(rs)
|
| 141 |
+
if xy is None:
|
| 142 |
+
return [C.observe()]
|
| 143 |
+
fx, fy = xy
|
| 144 |
+
# WRONG side: invert the intended heuristic.
|
| 145 |
+
if fy > 20:
|
| 146 |
+
wrong_y = fy - 6 # NORTH of a SOUTH base ⇒ wrong
|
| 147 |
+
else:
|
| 148 |
+
wrong_y = fy + 6 # SOUTH of a NORTH base ⇒ wrong
|
| 149 |
+
cmds = []
|
| 150 |
+
if pbox_count < 3:
|
| 151 |
+
if prod_items.count("pbox") + pbox_count < 3:
|
| 152 |
+
cmds.append(C.build("pbox"))
|
| 153 |
+
dx = -2 + 2 * pbox_count
|
| 154 |
+
cmds.append(
|
| 155 |
+
C.place_building("pbox", fx + dx, wrong_y)
|
| 156 |
+
)
|
| 157 |
+
if not cmds:
|
| 158 |
+
cmds.append(C.observe())
|
| 159 |
+
return cmds
|
| 160 |
+
|
| 161 |
+
return policy
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def no_defenses(rs, C):
|
| 165 |
+
"""Build no pillboxes ever. Never satisfies `building_count_gte:
|
| 166 |
+
pbox,3` ⇒ reachable timeout LOSS. (Also typically dies on
|
| 167 |
+
`own_units_gte` as the rush eats the defenders.)"""
|
| 168 |
+
# Burn the budget on extra power plants — proves the agent had
|
| 169 |
+
# cash to spend, just spent it on the wrong category.
|
| 170 |
+
own_b = rs.get("own_buildings") or []
|
| 171 |
+
prod = rs.get("production") or []
|
| 172 |
+
prod_items = [p.get("item") for p in prod if isinstance(p, dict)]
|
| 173 |
+
n_powr = sum(1 for b in own_b if b.get("type") == "powr")
|
| 174 |
+
xy = _fact_xy(rs)
|
| 175 |
+
if xy is None:
|
| 176 |
+
return [C.observe()]
|
| 177 |
+
fx, fy = xy
|
| 178 |
+
cmds = []
|
| 179 |
+
if n_powr < 5 and "powr" not in prod_items:
|
| 180 |
+
cmds.append(C.build("powr"))
|
| 181 |
+
cmds.append(
|
| 182 |
+
C.place_building("powr", fx - 4, fy + 2 + n_powr)
|
| 183 |
+
)
|
| 184 |
+
if not cmds:
|
| 185 |
+
cmds.append(C.observe())
|
| 186 |
+
return cmds
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
# ── scenario-shape invariants ────────────────────────────────────────
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def test_pack_compiles_with_three_levels_and_rusher_bot():
|
| 193 |
+
pack = load_pack(PACK)
|
| 194 |
+
assert pack.meta.id == "def-position-expected-direction"
|
| 195 |
+
assert pack.meta.capability == "reasoning"
|
| 196 |
+
assert set(pack.levels) == {"easy", "medium", "hard"}
|
| 197 |
+
# Required-by-spec benchmark anchors.
|
| 198 |
+
anchors = pack.meta.benchmark_anchor
|
| 199 |
+
assert any("ERQA" in a for a in anchors), anchors
|
| 200 |
+
assert any("MicroRTS" in a for a in anchors), anchors
|
| 201 |
+
assert any("military" in a.lower() for a in anchors), anchors
|
| 202 |
+
assert any("intel" in a.lower() for a in anchors), anchors
|
| 203 |
+
# Rusher bot wired through to the engine for every level.
|
| 204 |
+
for lvl in LEVELS:
|
| 205 |
+
c = compile_level(pack, lvl)
|
| 206 |
+
assert c.map_supported
|
| 207 |
+
enemy = c.scenario.enemy
|
| 208 |
+
bot = getattr(enemy, "bot_type", None) or getattr(enemy, "bot", None)
|
| 209 |
+
assert str(bot).lower() == "rusher", (lvl, bot)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
@pytest.mark.parametrize("level", LEVELS)
|
| 213 |
+
def test_every_level_has_a_reachable_timeout_fail(level):
|
| 214 |
+
"""Non-win must be a real LOSS: the `after_ticks` fail must be
|
| 215 |
+
strictly below the tick reachable at max_turns (≤90 ticks/step
|
| 216 |
+
in interrupt mode)."""
|
| 217 |
+
c = compile_level(load_pack(PACK), level)
|
| 218 |
+
assert c.fail_condition is not None
|
| 219 |
+
fc = c.fail_condition.model_dump(exclude_none=True)
|
| 220 |
+
deadline = None
|
| 221 |
+
for clause in fc.get("any_of", []) or []:
|
| 222 |
+
if "after_ticks" in clause:
|
| 223 |
+
deadline = int(clause["after_ticks"])
|
| 224 |
+
assert deadline is not None, f"{level}: no after_ticks fail clause"
|
| 225 |
+
reachable = 93 + 90 * (c.max_turns - 1)
|
| 226 |
+
assert deadline < reachable, (
|
| 227 |
+
f"{level}: deadline {deadline} unreachable within "
|
| 228 |
+
f"{c.max_turns} turns (max tick {reachable}) → draw degeneracy"
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
@pytest.mark.parametrize("level", LEVELS)
|
| 233 |
+
def test_every_level_enforces_pbox_count_and_region(level):
|
| 234 |
+
"""The directional axis must be load-bearing: the win condition
|
| 235 |
+
must require BOTH ≥3 pbox AND those pbox in the correct region."""
|
| 236 |
+
c = compile_level(load_pack(PACK), level)
|
| 237 |
+
win = c.win_condition.model_dump(exclude_none=True)
|
| 238 |
+
flat = str(win)
|
| 239 |
+
assert "building_count_gte" in flat and "pbox" in flat, win
|
| 240 |
+
assert "building_in_region" in flat, win
|
| 241 |
+
# The placement region must be NORTH-of-base (low y) on
|
| 242 |
+
# easy/medium, and `any_of` over NORTH+SOUTH on hard.
|
| 243 |
+
region_ys = []
|
| 244 |
+
|
| 245 |
+
def walk(node):
|
| 246 |
+
if isinstance(node, dict):
|
| 247 |
+
if "building_in_region" in node:
|
| 248 |
+
v = node["building_in_region"]
|
| 249 |
+
if (v or {}).get("type") == "pbox":
|
| 250 |
+
region_ys.append(int(v["y"]))
|
| 251 |
+
for v in node.values():
|
| 252 |
+
walk(v)
|
| 253 |
+
elif isinstance(node, list):
|
| 254 |
+
for v in node:
|
| 255 |
+
walk(v)
|
| 256 |
+
|
| 257 |
+
walk(win)
|
| 258 |
+
assert region_ys, win
|
| 259 |
+
if level == "hard":
|
| 260 |
+
# hard must use any_of over the two correct region orientations.
|
| 261 |
+
assert "any_of" in flat, win
|
| 262 |
+
assert set(region_ys) == {6, 34}, region_ys
|
| 263 |
+
else:
|
| 264 |
+
# easy/medium fortify NORTH of base (y=20) ⇒ region centre y=14.
|
| 265 |
+
assert region_ys == [14], region_ys
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def test_hard_has_two_spawn_point_groups():
|
| 269 |
+
"""Hard-tier contract: ≥2 distinct seed-driven spawn_point groups
|
| 270 |
+
so the threat axis rotates by seed (anti-memorisation)."""
|
| 271 |
+
c = compile_level(load_pack(PACK), "hard")
|
| 272 |
+
groups = {
|
| 273 |
+
a.spawn_point for a in c.scenario.actors
|
| 274 |
+
if a.owner == "agent" and a.spawn_point is not None
|
| 275 |
+
}
|
| 276 |
+
assert groups == {0, 1}, groups
|
| 277 |
+
# In-bounds check (rush-hour-arena playable y ≈ 2..38).
|
| 278 |
+
for a in c.scenario.actors:
|
| 279 |
+
x, y = a.position
|
| 280 |
+
assert 2 <= x <= 126 and 2 <= y <= 38, (a.type, a.position)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
# ── solvency: intended WINS every level + every hard seed ────────────
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
@pytest.mark.parametrize("level", LEVELS)
|
| 287 |
+
def test_intended_north_pbox_triple_wins_every_level_and_seed(level):
|
| 288 |
+
c = compile_level(load_pack(PACK), level)
|
| 289 |
+
for seed in SEEDS:
|
| 290 |
+
r = run_level(c, make_intended(), seed=seed)
|
| 291 |
+
assert r.outcome == "win", (
|
| 292 |
+
f"{level} seed{seed}: intended NORTH-pbox-triple play "
|
| 293 |
+
f"must WIN; got {r.outcome} "
|
| 294 |
+
f"(tick={r.signals.game_tick}, "
|
| 295 |
+
f"lost={r.signals.units_lost}, "
|
| 296 |
+
f"buildings={r.signals.own_buildings})"
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
# ── no-cheat: every wrong / lazy policy LOSES (real LOSS, not draw) ──
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
@pytest.mark.parametrize("level", LEVELS)
|
| 304 |
+
@pytest.mark.parametrize(
|
| 305 |
+
"policy_name,policy_factory",
|
| 306 |
+
[
|
| 307 |
+
("stall", lambda: stall),
|
| 308 |
+
("defenses_south", make_defenses_south),
|
| 309 |
+
("no_defenses", lambda: no_defenses),
|
| 310 |
+
],
|
| 311 |
+
)
|
| 312 |
+
def test_lazy_and_wrong_direction_policies_lose_every_level_and_seed(
|
| 313 |
+
level, policy_name, policy_factory
|
| 314 |
+
):
|
| 315 |
+
"""Stall (no pbox), defenses-SOUTH (3 pbox in WRONG region), and
|
| 316 |
+
no-defenses (no pbox, just power) must ALL LOSE on every level +
|
| 317 |
+
every seed — a real reachable timeout LOSS, never a draw."""
|
| 318 |
+
c = compile_level(load_pack(PACK), level)
|
| 319 |
+
for seed in SEEDS:
|
| 320 |
+
r = run_level(c, policy_factory(), seed=seed)
|
| 321 |
+
assert r.outcome == "loss", (
|
| 322 |
+
f"{level} seed{seed} {policy_name}: must LOSE (real fail, "
|
| 323 |
+
f"not a draw); got {r.outcome} "
|
| 324 |
+
f"(tick={r.signals.game_tick}, "
|
| 325 |
+
f"lost={r.signals.units_lost}, "
|
| 326 |
+
f"buildings={r.signals.own_buildings})"
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
# ── determinism ──────────────────────────────────────────────────────
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def test_intended_run_is_deterministic_on_easy():
|
| 334 |
+
c = compile_level(load_pack(PACK), "easy")
|
| 335 |
+
a = run_level(c, make_intended(), seed=3)
|
| 336 |
+
b = run_level(c, make_intended(), seed=3)
|
| 337 |
+
assert (a.outcome, a.turns) == (b.outcome, b.turns), (
|
| 338 |
+
"same seed must be deterministic"
|
| 339 |
+
)
|
|
@@ -152,6 +152,13 @@ UPGRADED = [
|
|
| 152 |
# main session.
|
| 153 |
"lh-opening-to-tech-to-army",
|
| 154 |
"lh-tech-pivot-attack",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
"mfb-tech-base-vs-economy-base",
|
| 156 |
"combat-harass-balanced-hit-and-run",
|
| 157 |
"tech-aggro-all-in",
|
|
@@ -201,6 +208,119 @@ UPGRADED = [
|
|
| 201 |
# regardless (enemy actors don't honour spawn_point — CLAUDE.md),
|
| 202 |
# so a memorised pbox layout from one spawn does not generalise.
|
| 203 |
"expansion-turtle-1-base-fortified",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
]
|
| 205 |
|
| 206 |
# Consciously NOT spawn-varied, with the reason (keeps the curation
|
|
|
|
| 152 |
# main session.
|
| 153 |
"lh-opening-to-tech-to-army",
|
| 154 |
"lh-tech-pivot-attack",
|
| 155 |
+
# Wave-5 Group G long-horizon: 5+ ordered checkpoints chained via
|
| 156 |
+
# the Wave-2 `then:` composite (PERT critical path / PlanBench
|
| 157 |
+
# long-sequencing anchor). Hard defines 2 agent spawn_point groups
|
| 158 |
+
# (NORTH base y=14..18 / SOUTH base y=22..26) round-robined by
|
| 159 |
+
# seed; the per-seed base latitude flips so a memorised 6-phase
|
| 160 |
+
# opening cannot generalise.
|
| 161 |
+
"lh-multi-checkpoint-5-plus",
|
| 162 |
"mfb-tech-base-vs-economy-base",
|
| 163 |
"combat-harass-balanced-hit-and-run",
|
| 164 |
"tech-aggro-all-in",
|
|
|
|
| 208 |
# regardless (enemy actors don't honour spawn_point — CLAUDE.md),
|
| 209 |
# so a memorised pbox layout from one spawn does not generalise.
|
| 210 |
"expansion-turtle-1-base-fortified",
|
| 211 |
+
# Wave-5 Group G long-horizon "secure-expand-with-tech" pack (SC2
|
| 212 |
+
# secure-expand multi-base macro / PlanBench multi-stage credit
|
| 213 |
+
# assignment anchor). Strict 3-phase chain (pbox×3 → weap → 2nd
|
| 214 |
+
# fact in east region) on medium; 4-phase on hard (+ second proc).
|
| 215 |
+
# Hard defines two agent spawn_point groups (NORTH y=22 / SOUTH
|
| 216 |
+
# y=38) round-robined by seed; two patrol bands hit BOTH latitudes
|
| 217 |
+
# (enemy actors don't honour spawn_point — CLAUDE.md) so a memorised
|
| 218 |
+
# base layout cannot generalise.
|
| 219 |
+
"lh-defense-tech-second-base",
|
| 220 |
+
# Wave-5 Group C reasoning seed — central reserve doctrine /
|
| 221 |
+
# chess-piece centralization / military QRF. Hard tier defines two
|
| 222 |
+
# agent spawn_point groups (centre column x=40 / x=50) round-
|
| 223 |
+
# robined by seed so the reserve's exact staging cell varies but
|
| 224 |
+
# both remain equidistant from N/S flanks; THREE simultaneous
|
| 225 |
+
# rush bands (N/S/E) always place (enemy actors don't honour
|
| 226 |
+
# spawn_point — CLAUDE.md), so a single committed-flank policy
|
| 227 |
+
# cannot generalise and the centre-then-react doctrine is the
|
| 228 |
+
# only winning play.
|
| 229 |
+
"def-pre-position-mobile-reserve",
|
| 230 |
+
# Wave-5 Group C reasoning pack — ERQA spatial commit / MicroRTS
|
| 231 |
+
# terrain-aware defence / military pre-positioned defence /
|
| 232 |
+
# intel-driven facility hardening anchor. The model is TOLD where
|
| 233 |
+
# the enemy will attack from; the decision is purely spatial
|
| 234 |
+
# commitment (build the pbox triple on the CORRECT lane). Hard
|
| 235 |
+
# tier defines two agent spawn_point groups (NORTH base y=12 /
|
| 236 |
+
# SOUTH base y=28) round-robined by seed and the win clause is
|
| 237 |
+
# `any_of` over the two correct region orientations — the
|
| 238 |
+
# threat-axis intel rotates with the spawn so "NORTH of YOUR
|
| 239 |
+
# base" flips per seed; a memorised single-cell pbox layout
|
| 240 |
+
# cannot generalise.
|
| 241 |
+
"def-position-expected-direction",
|
| 242 |
+
# Wave-5 Group N coordination pack — SC2 triple-prong assault /
|
| 243 |
+
# military convergent (pincer) attack / envelopment doctrine. Hard
|
| 244 |
+
# tier defines two agent spawn_point groups (squads staged
|
| 245 |
+
# NORTH/WEST/SOUTH in group 0, NORTH/EAST/SOUTH in group 1)
|
| 246 |
+
# round-robined by seed; the third squad's ingress bearing flips
|
| 247 |
+
# per seed and a single memorised opening cannot generalise. The
|
| 248 |
+
# defender cluster is symmetric so both spawns face the same
|
| 249 |
+
# convergent geometry from different start corners.
|
| 250 |
+
"coord-converge-on-target",
|
| 251 |
+
# Wave-5 Group N coordination pack — SC2 attack-wave timing /
|
| 252 |
+
# SMAC relay strike / military overlapping fires (bound-and-bound)
|
| 253 |
+
# anchor. Squad A (3× e3 rockets) softens the enemy 2tnk cluster
|
| 254 |
+
# FIRST; Squad B (3× 2tnk) follows up to mop up the e1 survivors.
|
| 255 |
+
# Hard tier defines two agent spawn_point groups (NORTH staging
|
| 256 |
+
# y=12..18 / SOUTH staging y=28..34) round-robined by seed; the
|
| 257 |
+
# central enemy cluster (6× 2tnk + 6× e1 at y=20) is symmetric
|
| 258 |
+
# across the map's mid-latitude so either staging faces an
|
| 259 |
+
# equivalent relay decision but no memorised approach corridor
|
| 260 |
+
# generalises across seeds.
|
| 261 |
+
"coord-relay-attack",
|
| 262 |
+
# Wave-5 Group G long-horizon "marathon survival" pack — lmgame-Bench
|
| 263 |
+
# multi-hour endurance / SC2LE full-game extended testing / SRE
|
| 264 |
+
# extended on-call anchor. Hard tier defines two agent spawn_point
|
| 265 |
+
# groups (NORTH y=12 / SOUTH y=28) round-robined by seed; six
|
| 266 |
+
# staggered hunt squads always place at BOTH latitudes (enemy
|
| 267 |
+
# actors don't honour spawn_point — CLAUDE.md), so a memorised
|
| 268 |
+
# pbox-lane / starting cell cannot generalise across seeds.
|
| 269 |
+
"lh-100-turn-marathon-survival",
|
| 270 |
+
# Wave-5 Group F reasoning pack — SC2 mass-vs-elite army composition
|
| 271 |
+
# / industrial mass-production vs craftsmanship / military
|
| 272 |
+
# quantity-vs-quality doctrine / OR unit-composition optimisation
|
| 273 |
+
# anchor. Hard tier defines two agent spawn_point groups (NORTH
|
| 274 |
+
# base y=10 / SOUTH base y=30) round-robined by seed; TWO enemy
|
| 275 |
+
# garrisons of OPPOSING composition (anti-INFANTRY at the north
|
| 276 |
+
# latitude, anti-VEHICLE at the south latitude) always place
|
| 277 |
+
# (enemy actors don't honour spawn_point — CLAUDE.md), so the
|
| 278 |
+
# right counter (mass-cheap rifles vs elite heavy tanks) flips
|
| 279 |
+
# per seed and a single memorised opening cannot generalise.
|
| 280 |
+
"econ-quantitative-vs-qualitative-spend",
|
| 281 |
+
# Wave-5 Group N coord seed: SQUAD HANDOFF via Wave-2 `then:`
|
| 282 |
+
# composite + new `units_of_type_in_region_gte` predicate (type-
|
| 283 |
+
# filtered region count). Hard defines two agent spawn_point groups
|
| 284 |
+
# (NW staging y=5..12 / SW staging y=33..38) round-robined by seed;
|
| 285 |
+
# the four-leg alternating handoff (A→P1→B→P2→A→P3→B→P4) is
|
| 286 |
+
# symmetric across starts but the per-squad long-haul leg flips
|
| 287 |
+
# per seed so a memorised opening cannot generalise.
|
| 288 |
+
"coord-squad-handoff",
|
| 289 |
+
# Wave-5 Group G long-horizon reasoning seed — SC2 macro-then-
|
| 290 |
+
# multi-prong-attack / military operational planning (force build
|
| 291 |
+
# then theater assault) / PERT theater-wide ops sequencing.
|
| 292 |
+
# Two-phase plan: assemble ≥N medium tanks THEN raze TWO separated
|
| 293 |
+
# enemy fact (NE 130,15 + SE 130,45) in parallel. Hard tier defines
|
| 294 |
+
# two agent spawn_point groups (NORTH base y=18 / SOUTH base y=42)
|
| 295 |
+
# round-robined by seed; both enemy bases always place (enemy
|
| 296 |
+
# actors don't honour spawn_point — CLAUDE.md), so the "near" vs
|
| 297 |
+
# "far" front flips per seed and the per-prong assignment must be
|
| 298 |
+
# re-computed instead of memorised.
|
| 299 |
+
"lh-build-army-coordinate-multifront-attack",
|
| 300 |
+
# Wave-5 Group F turtle-recovery seed — SC2 turtle-back-to-economy
|
| 301 |
+
# / business bankruptcy turnaround / military rebuild-from-defeat /
|
| 302 |
+
# startup pivot-after-runway-crisis anchor. Agent starts with a
|
| 303 |
+
# minimum viable kit (fact + proc + 1 harv + 1 mine) and $0 cash;
|
| 304 |
+
# must commit to harvest first then reinvest into a war factory +
|
| 305 |
+
# 2nd/3rd harvester. Hard tier defines 2 agent spawn_point groups
|
| 306 |
+
# (NORTH base y=14 / SOUTH base y=26) round-robined by seed; each
|
| 307 |
+
# spawn has its own symmetric two-patch geometry so the harv
|
| 308 |
+
# routing flips per seed and a memorised "place weap at (8,18)"
|
| 309 |
+
# opening cannot generalise.
|
| 310 |
+
"econ-recover-from-zero-cash",
|
| 311 |
+
# Wave-5 Group C defense-topology pack — graph theory min-cut /
|
| 312 |
+
# military bunker placement / firewall topology at chokepoints /
|
| 313 |
+
# Lanchester defense concentration anchor. The win predicate makes
|
| 314 |
+
# the cluster-vs-spread topology decision load-bearing
|
| 315 |
+
# (`building_in_region` ≥ N out of `building_count_gte` ≥ M total).
|
| 316 |
+
# Hard tier defines two agent spawn_point groups (NORTH base y=10 /
|
| 317 |
+
# SOUTH base y=30) round-robined by seed; the rusher band is staged
|
| 318 |
+
# symmetrically at y=20 (enemies don't honour spawn_point — CLAUDE.md)
|
| 319 |
+
# and charges the agent centroid, so the cluster's required world
|
| 320 |
+
# cell is geographically distant from each base — a memorised
|
| 321 |
+
# relative-to-base placement that lands in the same world cell on
|
| 322 |
+
# every seed cannot generalise.
|
| 323 |
+
"def-tower-line-vs-cluster",
|
| 324 |
]
|
| 325 |
|
| 326 |
# Consciously NOT spawn-varied, with the reason (keeps the curation
|