Spaces:
Running
feat(scenario): rob-multiple-simultaneous-pressures — compound-incident triage (SC2 multi-front / ops triage anchor)
Browse filesWave-6 REASONING pack: agent must hold THREE pressures simultaneously
— defence (hunt charge on the fact), economy (raider tanks on the
harv patch), and tech (war factory by hard deadline). No single-axis
play wins.
Design highlights
- bot_type: hunt (each enemy unit attacks its OWN nearest agent
target). Two threat axes emerge from geometry — centre-line
tank raiders hunt the nearest harv, flanking infantry hunt the
base buildings — without needing two bot types.
- starting_cash 2000 = exactly the weap cost. The intended play
queues weap on turn 1 and tops up cash from the harvest loop;
pulling harvs home strands the EV bar (economy_value_gte: 1000)
which makes the harvest loop load-bearing through the raid.
- Tools: full set. Pre-placed: fact + proc + powr + 2 harv +
2 mines + 4 1tnk defenders (powr added so the proc/weap chain
has power per gamerules.rs prereqs).
- Hard tier: 2 spawn_point groups (NORTH y=14 / SOUTH y=26)
round-robined by seed; centre-line raiders + hunt squad face
the active spawn with equal pressure on either latitude.
- Anti-DRAW: unarmed enemy fact marker far east per CLAUDE.md
auto-done footgun.
Discrimination (scripted, all seeds 1..4 on every tier)
- stall (only observe) → LOSS (clock + harv-loss)
- focus-defence-skip-tech → LOSS (weap never built)
- focus-tech-lose-econ (no harvest) → LOSS (EV bar never met)
- intended multi-handle → WIN
Registered in tests/test_hard_tier.py::UPGRADED.
Smoke (Qwen/Qwen3.6-Plus, medium s=1): loss composite=0.1894 —
valid model discrimination on the simultaneous-pressure axis.
|
@@ -0,0 +1,396 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# rob-multiple-simultaneous-pressures — compound-incident triage
|
| 2 |
+
#
|
| 3 |
+
# REASONING capability — Wave-6 "multi-front simultaneous pressure"
|
| 4 |
+
# pack. The agent must hold THREE pressures simultaneously, none of
|
| 5 |
+
# which can be sequenced or paused:
|
| 6 |
+
#
|
| 7 |
+
# 1. DEFENCE — the agent fact is under a `hunt` charge that always
|
| 8 |
+
# picks the nearest agent target. Even if the harvesters die, the
|
| 9 |
+
# hunters keep coming for the fact / proc.
|
| 10 |
+
# 2. ECONOMY — pre-placed harvesters (`harv`) work the patch east of
|
| 11 |
+
# the proc and are the closest agent target to the raider tanks
|
| 12 |
+
# lined up at the lane mouth. Stop harvesting and income stops;
|
| 13 |
+
# lose a harv and the win predicate breaks (harv≥1).
|
| 14 |
+
# 3. TECH — a war factory (`weap`, cost 2000, prereq `proc`) must be
|
| 15 |
+
# built and standing by tick 5400. starting_cash 2000 covers the
|
| 16 |
+
# base weap exactly; the chain only closes if the harvesters keep
|
| 17 |
+
# running through the raid (so cash is in the tin when defence
|
| 18 |
+
# tightens). Skipping the tech to focus on combat fails the win.
|
| 19 |
+
#
|
| 20 |
+
# Real-world anchor: ops triage during a compound incident — multiple
|
| 21 |
+
# system failures at once. SC2 multi-front simultaneous pressure. DR
|
| 22 |
+
# multi-system failure response. The discriminating policies:
|
| 23 |
+
#
|
| 24 |
+
# • stall (only observe): LOSS — no weap built, harvs don't deliver
|
| 25 |
+
# (`harvest` order needed each turn), eventually clock expires.
|
| 26 |
+
# • focus-defence-skip-tech (queue defenders, hold the line, never
|
| 27 |
+
# start weap): LOSS — weap never appears ⇒ the has_building:weap
|
| 28 |
+
# clause never latches ⇒ timeout LOSS via after_ticks.
|
| 29 |
+
# • focus-tech-lose-econ (queue weap immediately, pull defenders
|
| 30 |
+
# back to base, abandon the harvs / never issue harvest orders):
|
| 31 |
+
# LOSS — the post-weap EV bar (economy_value_gte: 1000) never
|
| 32 |
+
# latches because starting_cash 2000 is consumed by weap and
|
| 33 |
+
# the idle harvs add no income ⇒ timeout LOSS via after_ticks.
|
| 34 |
+
# (Lose-the-harv mode is also possible if the raiders reach
|
| 35 |
+
# the unprotected harvs, but the EV teeth bite first.)
|
| 36 |
+
# • intended multi-handle (keep harv ring on the patch, defenders
|
| 37 |
+
# intercept raiders, queue weap from turn 1, fund it with the
|
| 38 |
+
# harv income that keeps flowing): WIN.
|
| 39 |
+
#
|
| 40 |
+
# DESIGN
|
| 41 |
+
# ──────
|
| 42 |
+
# - bot_type: `hunt`. Each enemy unit attacks its OWN nearest agent
|
| 43 |
+
# target (Infantry / Vehicle / MCV / Building per scripted_bot.rs
|
| 44 |
+
# `foes()`). The TANK raiders are lined up directly east of the
|
| 45 |
+
# harv lane (their nearest agent target is the harv at the patch
|
| 46 |
+
# ⇒ economy harassment); the INFANTRY hunters are staged on
|
| 47 |
+
# FLANKING latitudes where the nearest agent target is the fact /
|
| 48 |
+
# proc / powr (⇒ base-hunt pressure). Same bot, two threat axes
|
| 49 |
+
# driven by geometry — the "raider for econ + hunt for base" of
|
| 50 |
+
# the spec is realised by positioning, not by two bot types.
|
| 51 |
+
# - starting_cash: 2000 — exactly the weap cost. The intended play
|
| 52 |
+
# queues weap immediately and tops up cash from the running harv
|
| 53 |
+
# loop while the defenders intercept the raiders. Stalling lets
|
| 54 |
+
# the cash sit unused; spending it on defenders pre-empts weap.
|
| 55 |
+
# - Tools: full set (observe + build + place_building + harvest +
|
| 56 |
+
# move_units + attack_unit + attack_move + stop). The pack tests
|
| 57 |
+
# simultaneous management, not tool restriction.
|
| 58 |
+
# - Pre-placed: fact + proc + powr (power for the queue) + 2 harv +
|
| 59 |
+
# 2 mines + 4 1tnk defenders. Powr is included because weap is
|
| 60 |
+
# queued from the Building list — without power the build is
|
| 61 |
+
# silently slow / blocked (CLAUDE.md underpowered-queue footgun).
|
| 62 |
+
# The spec lists "fact + proc + 2 harv + 2 mines + 4 defenders" as
|
| 63 |
+
# the core seed; powr is treated as core infrastructure (proc
|
| 64 |
+
# itself needs powr per gamerules.rs).
|
| 65 |
+
# - Anti-DRAW: unarmed enemy `fact` marker far east — the engine
|
| 66 |
+
# auto-`done`s on enemy-MustBeDestroyed wipe before the win/fail
|
| 67 |
+
# check otherwise (CLAUDE.md auto-done footgun).
|
| 68 |
+
#
|
| 69 |
+
# Engine facts used (CLAUDE.md / gamerules.rs):
|
| 70 |
+
# - weap cost 2000, prereq `proc` (pre-placed ⇒ available T1).
|
| 71 |
+
# - proc prereq is `powr` (pre-placed ⇒ proc functions).
|
| 72 |
+
# - hunt bot's foes() includes Building, so flanking infantry that
|
| 73 |
+
# have the fact as their nearest target attack the base.
|
| 74 |
+
# - `harv` Command.harvest(uid, mx, my) per turn keeps the harvest
|
| 75 |
+
# loop running; idle harvs ⇒ no income.
|
| 76 |
+
# - place_building has no adjacency requirement; weap can be
|
| 77 |
+
# placed anywhere in-bounds (we pre-pick a safe spot via the
|
| 78 |
+
# description's coordinate hint).
|
| 79 |
+
# - hard tier: ≥2 agent spawn_point groups (UPGRADED contract,
|
| 80 |
+
# tests/test_hard_tier.py) — north-flank vs south-flank base.
|
| 81 |
+
#
|
| 82 |
+
# Validate (no model / no network):
|
| 83 |
+
# python3 -m pytest tests/test_rob_multiple_simultaneous_pressures.py -q
|
| 84 |
+
|
| 85 |
+
meta:
|
| 86 |
+
id: rob-multiple-simultaneous-pressures
|
| 87 |
+
title: 'Compound Incident — Multi-Front Simultaneous Pressure (Defence + Economy + Tech)'
|
| 88 |
+
capability: reasoning
|
| 89 |
+
real_world_meaning: >
|
| 90 |
+
Ops triage during a compound incident — three independent failures
|
| 91 |
+
must be handled simultaneously, none of which can be sequenced or
|
| 92 |
+
paused. Your base is under a hunt charge (defence), the harvesters
|
| 93 |
+
feeding income are being raided (economy), and a tech objective
|
| 94 |
+
(war factory) must be completed before a hard deadline. Focusing
|
| 95 |
+
on any single pressure forfeits the others; the intended play
|
| 96 |
+
keeps all three loops running in parallel, funding the tech with
|
| 97 |
+
the harvest income that the defenders are simultaneously
|
| 98 |
+
protecting.
|
| 99 |
+
robotics_analogue: >
|
| 100 |
+
Disaster-recovery multi-system failure — a control room handles
|
| 101 |
+
a security event (perimeter under attack), a throughput event
|
| 102 |
+
(data pipeline workers under harassment), and a planned upgrade
|
| 103 |
+
(capability rollout) at the same time. Pausing the upgrade to
|
| 104 |
+
fight the security incident misses the rollout SLA; abandoning
|
| 105 |
+
the pipeline to harden the perimeter starves the upgrade of
|
| 106 |
+
resources. The capability under test is parallel-task triage
|
| 107 |
+
under genuine simultaneous pressure.
|
| 108 |
+
benchmark_anchor:
|
| 109 |
+
- 'SC2 multi-front simultaneous pressure'
|
| 110 |
+
- 'ops triage during compound incident'
|
| 111 |
+
- 'DR multi-system failure'
|
| 112 |
+
- 'military theater-wide simultaneous threats'
|
| 113 |
+
author: openra-bench
|
| 114 |
+
|
| 115 |
+
base_map: rush-hour-arena
|
| 116 |
+
# Exactly enough for the weap (cost 2000). The intended play funds
|
| 117 |
+
# any extra defender / structure spend from the harvest loop income
|
| 118 |
+
# that runs THROUGH the raid. Anything more than 2000 lets a
|
| 119 |
+
# focus-defence policy buy out the threat without ever touching tech.
|
| 120 |
+
starting_cash: 2000
|
| 121 |
+
|
| 122 |
+
base:
|
| 123 |
+
agent:
|
| 124 |
+
faction: allies
|
| 125 |
+
# `hunt`: each enemy unit attacks its OWN nearest agent target
|
| 126 |
+
# (Infantry/Vehicle/MCV/Building). The geometry of the placed
|
| 127 |
+
# enemy units splits the pressure into two axes — tanks at the
|
| 128 |
+
# patch lane hunt the harvs (economy raid), flanking infantry
|
| 129 |
+
# hunt the fact/proc (base hunt).
|
| 130 |
+
enemy:
|
| 131 |
+
faction: soviet
|
| 132 |
+
bot_type: hunt
|
| 133 |
+
tools:
|
| 134 |
+
- observe
|
| 135 |
+
- build
|
| 136 |
+
- place_building
|
| 137 |
+
- harvest
|
| 138 |
+
- move_units
|
| 139 |
+
- attack_unit
|
| 140 |
+
- attack_move
|
| 141 |
+
- stop
|
| 142 |
+
planning: true
|
| 143 |
+
# No interrupts: fixed-cadence steps give the predictable
|
| 144 |
+
# ~90 ticks/turn we need to align the after_ticks 5401 fail
|
| 145 |
+
# clause with max_turns 61 (93+90·60=5493 ≥ 5401 ✓). Interrupt
|
| 146 |
+
# mode shortens the per-step advance and would make a stall
|
| 147 |
+
# episode draw before the deadline bites.
|
| 148 |
+
termination:
|
| 149 |
+
max_ticks: 12000
|
| 150 |
+
# Per-level actors via overrides — keeps the audit explicit for
|
| 151 |
+
# each tier's threat composition.
|
| 152 |
+
actors: []
|
| 153 |
+
|
| 154 |
+
levels:
|
| 155 |
+
# ── EASY ─────────────────────────────────────────────────────────
|
| 156 |
+
# Bare three-pressure skill: ONE flanking hunt group (only the base
|
| 157 |
+
# pressure axis), pre-placed defenders blunt the raid mouth, weap
|
| 158 |
+
# is the sole tech clause. Stall / focus-defence-skip-tech /
|
| 159 |
+
# focus-tech-lose-econ all LOSE; the intended multi-handle WINS.
|
| 160 |
+
# max_turns 61 → reachable tick 93 + 90·60 = 5493 > 5401 ✓.
|
| 161 |
+
easy:
|
| 162 |
+
description: >
|
| 163 |
+
Compound incident. You hold a base on the west — Construction
|
| 164 |
+
Yard (fact at 10,18), refinery (proc at 8,18), power plant
|
| 165 |
+
(powr at 8,20), and 2 harvesters working an ore patch east of
|
| 166 |
+
the proc (mines at 24,16 and 24,22). Four light tank defenders
|
| 167 |
+
ring the patch at x=18. You start with $2000 — exactly enough
|
| 168 |
+
for a war factory. THREE pressures are live AT THE SAME TIME:
|
| 169 |
+
(1) DEFEND — a single hunt squad of 3 rifle infantry is staged
|
| 170 |
+
NORTH of the base; they charge whatever agent target is
|
| 171 |
+
nearest, including the fact.
|
| 172 |
+
(2) ECONOMY — one enemy tank (raider) is at the lane mouth
|
| 173 |
+
east of the patch; under the hunt bot it targets the
|
| 174 |
+
harvester nearest to it. If the harvs die the win breaks.
|
| 175 |
+
(3) TECH — you must have a war factory (weap, cost 2000, prereq
|
| 176 |
+
proc) standing by tick 5400.
|
| 177 |
+
Win = `weap` AND `proc` AND ≥1 harvester AND `fact` still
|
| 178 |
+
standing, before tick 5400. Stalling, focusing on defence and
|
| 179 |
+
skipping tech, and focusing on tech while abandoning the
|
| 180 |
+
harvesters all LOSE.
|
| 181 |
+
overrides:
|
| 182 |
+
actors:
|
| 183 |
+
# Agent base on the west — fact + proc + powr + 2 harv +
|
| 184 |
+
# 2 mines + 4 defenders per the pack core.
|
| 185 |
+
- {type: fact, owner: agent, position: [10, 18]}
|
| 186 |
+
- {type: proc, owner: agent, position: [8, 18]}
|
| 187 |
+
- {type: powr, owner: agent, position: [8, 20]}
|
| 188 |
+
- {type: harv, owner: agent, position: [14, 16]}
|
| 189 |
+
- {type: harv, owner: agent, position: [14, 22]}
|
| 190 |
+
- {type: mine, owner: neutral, position: [24, 16]}
|
| 191 |
+
- {type: mine, owner: neutral, position: [24, 22]}
|
| 192 |
+
# Four 1tnk defenders ringing the patch lane at x=18
|
| 193 |
+
# (between the harvs and the raider mouth). Tank-on-tank
|
| 194 |
+
# is required to actually kill a raider 1tnk (rifle e1 dies
|
| 195 |
+
# before it can do it).
|
| 196 |
+
- {type: '1tnk', owner: agent, position: [18, 16]}
|
| 197 |
+
- {type: '1tnk', owner: agent, position: [18, 22]}
|
| 198 |
+
- {type: '1tnk', owner: agent, position: [22, 18]}
|
| 199 |
+
- {type: '1tnk', owner: agent, position: [22, 20]}
|
| 200 |
+
# ECONOMY-RAID PRESSURE — 1 raider tank at the lane mouth.
|
| 201 |
+
# Its nearest agent target is the harv at (14,16) ⇒ raids the
|
| 202 |
+
# economy under hunt semantics.
|
| 203 |
+
- {type: '1tnk', owner: enemy, position: [60, 18]}
|
| 204 |
+
# BASE-HUNT PRESSURE — 3 e1 infantry staged on the NORTHERN
|
| 205 |
+
# flank. Their nearest agent target is fact/powr (no harv
|
| 206 |
+
# between them and the base from y=4), so under hunt the
|
| 207 |
+
# group charges the base buildings directly.
|
| 208 |
+
- {type: e1, owner: enemy, position: [40, 4], stance: 3, count: 3}
|
| 209 |
+
# Anti-DRAW: persistent unarmed enemy `fact` marker far east.
|
| 210 |
+
- {type: fact, owner: enemy, position: [120, 4]}
|
| 211 |
+
win_condition:
|
| 212 |
+
all_of:
|
| 213 |
+
- {has_building: weap}
|
| 214 |
+
- {has_building: proc}
|
| 215 |
+
- {unit_type_count_gte: {type: harv, n: 1}}
|
| 216 |
+
- {has_building: fact}
|
| 217 |
+
# ECONOMY teeth: post-weap residual EV bar. starting_cash 2000
|
| 218 |
+
# exactly funds the weap (cost 2000) ⇒ to clear this 1000-EV
|
| 219 |
+
# bar the harvesters MUST be in `harvest` mode through the
|
| 220 |
+
# raid so cash refills. A focus-tech policy that queues weap
|
| 221 |
+
# and pulls the harvs home (no harvest order) drains cash to
|
| 222 |
+
# 0 and never re-fills ⇒ this clause never latches ⇒ LOSS by
|
| 223 |
+
# the after_ticks fail clause.
|
| 224 |
+
- {economy_value_gte: 1000}
|
| 225 |
+
- {within_ticks: 5400}
|
| 226 |
+
# 5401 ≤ 93 + 90·60 = 5493 ⇒ deadline bites inside max_turns.
|
| 227 |
+
# Loss is reachable: clock OR fact razed OR harvs eradicated.
|
| 228 |
+
fail_condition:
|
| 229 |
+
any_of:
|
| 230 |
+
- {after_ticks: 5401}
|
| 231 |
+
- {not: {has_building: fact}}
|
| 232 |
+
- {not: {unit_type_count_gte: {type: harv, n: 1}}}
|
| 233 |
+
max_turns: 61
|
| 234 |
+
|
| 235 |
+
# ── MEDIUM ───────────────────────────────────────────────────────
|
| 236 |
+
# +1 controlled variable vs easy: the raid axis now has TWO raider
|
| 237 |
+
# tanks (one at each patch latitude — the harv ring can't ignore
|
| 238 |
+
# either) AND the hunt squad is heavier (4× e1 + 1× e3). Same
|
| 239 |
+
# clock; the budget is still 2000 ⇒ no slack on the cash side.
|
| 240 |
+
medium:
|
| 241 |
+
description: >
|
| 242 |
+
The same compound incident, scaled up on both pressure axes.
|
| 243 |
+
Base layout unchanged (fact+proc+powr at the west, 2 harvs on
|
| 244 |
+
the patch east of proc, 4 light tank defenders on the ring).
|
| 245 |
+
Pressures:
|
| 246 |
+
(1) DEFEND — a heavier hunt squad of 4 rifle infantry + 1
|
| 247 |
+
rocket soldier (e3) on the northern flank charges the base.
|
| 248 |
+
(2) ECONOMY — TWO raider tanks are staged at the patch lane
|
| 249 |
+
mouth, one on each harv latitude (y=16 and y=22) — the
|
| 250 |
+
harv ring can't ignore either.
|
| 251 |
+
(3) TECH — war factory (`weap`) by tick 5400.
|
| 252 |
+
Win = `weap` AND `proc` AND ≥1 harvester AND `fact` standing
|
| 253 |
+
before tick 5400. Stall, focus-defence-skip-tech,
|
| 254 |
+
focus-tech-lose-econ all LOSE.
|
| 255 |
+
overrides:
|
| 256 |
+
actors:
|
| 257 |
+
- {type: fact, owner: agent, position: [10, 18]}
|
| 258 |
+
- {type: proc, owner: agent, position: [8, 18]}
|
| 259 |
+
- {type: powr, owner: agent, position: [8, 20]}
|
| 260 |
+
- {type: harv, owner: agent, position: [14, 16]}
|
| 261 |
+
- {type: harv, owner: agent, position: [14, 22]}
|
| 262 |
+
- {type: mine, owner: neutral, position: [24, 16]}
|
| 263 |
+
- {type: mine, owner: neutral, position: [24, 22]}
|
| 264 |
+
- {type: '1tnk', owner: agent, position: [18, 16]}
|
| 265 |
+
- {type: '1tnk', owner: agent, position: [18, 22]}
|
| 266 |
+
- {type: '1tnk', owner: agent, position: [22, 18]}
|
| 267 |
+
- {type: '1tnk', owner: agent, position: [22, 20]}
|
| 268 |
+
# ECONOMY-RAID — two raiders, one per harv latitude.
|
| 269 |
+
- {type: '1tnk', owner: enemy, position: [60, 16]}
|
| 270 |
+
- {type: '1tnk', owner: enemy, position: [60, 22]}
|
| 271 |
+
# BASE-HUNT — 4 e1 + 1 e3 on the northern flank.
|
| 272 |
+
- {type: e1, owner: enemy, position: [40, 4], stance: 3, count: 4}
|
| 273 |
+
- {type: e3, owner: enemy, position: [42, 4], stance: 3, count: 1}
|
| 274 |
+
# Anti-DRAW marker.
|
| 275 |
+
- {type: fact, owner: enemy, position: [120, 4]}
|
| 276 |
+
win_condition:
|
| 277 |
+
all_of:
|
| 278 |
+
- {has_building: weap}
|
| 279 |
+
- {has_building: proc}
|
| 280 |
+
- {unit_type_count_gte: {type: harv, n: 1}}
|
| 281 |
+
- {has_building: fact}
|
| 282 |
+
# See easy: post-weap EV bar makes the harvest loop load-
|
| 283 |
+
# bearing so focus-tech-lose-econ cannot satisfy the win.
|
| 284 |
+
- {economy_value_gte: 1000}
|
| 285 |
+
- {within_ticks: 5400}
|
| 286 |
+
fail_condition:
|
| 287 |
+
any_of:
|
| 288 |
+
- {after_ticks: 5401}
|
| 289 |
+
- {not: {has_building: fact}}
|
| 290 |
+
- {not: {unit_type_count_gte: {type: harv, n: 1}}}
|
| 291 |
+
max_turns: 61
|
| 292 |
+
|
| 293 |
+
# ── HARD ─────────────────────────────────────────────────────────
|
| 294 |
+
# +2 controlled variables vs medium (per the spec "hunt + raider +
|
| 295 |
+
# tighter clock + 2 spawn groups"):
|
| 296 |
+
# 1. TIGHTER clock (within_ticks 4500; max_turns 51 → reachable
|
| 297 |
+
# tick 93+90·50 = 4593 ≥ 4501 ✓). The tech window shrinks so a
|
| 298 |
+
# sequenced "defend then tech" attempt runs out of clock.
|
| 299 |
+
# 2. TWO spawn_point groups (north y=14 base vs south y=26 base)
|
| 300 |
+
# — the BASE LATITUDE flips by seed, so a single memorised
|
| 301 |
+
# ring / weap placement cannot generalise. Both spawns face
|
| 302 |
+
# the SAME enemy composition (enemy actors are not spawn-
|
| 303 |
+
# filtered, per oramap.rs): the medium pack's 2 raider tanks
|
| 304 |
+
# and one heavy hunt squad, plus a mirrored off-axis hunt
|
| 305 |
+
# squad so the seed that lands on the off-axis flank still
|
| 306 |
+
# gets a same-latitude charge (otherwise the off-axis flank
|
| 307 |
+
# would have noticeably lighter pressure than the on-axis
|
| 308 |
+
# one). Same composition as medium per axis — the discriminator
|
| 309 |
+
# is clock-tightness + spawn variation, not enemy-count
|
| 310 |
+
# escalation.
|
| 311 |
+
hard:
|
| 312 |
+
description: >
|
| 313 |
+
The compound incident on a tighter clock and a seed-varied
|
| 314 |
+
start. Your base stages from a seed-chosen latitude (NORTH
|
| 315 |
+
y=14 or SOUTH y=26) — both formations cover the same patch
|
| 316 |
+
column (mines at x=24, two per latitude). Pressures:
|
| 317 |
+
(1) DEFEND — a hunt squad of 4 rifle infantry + 1 rocket
|
| 318 |
+
soldier charges from the centre-line; whichever base
|
| 319 |
+
latitude you got is the one it heads for.
|
| 320 |
+
(2) ECONOMY — TWO raider tanks at the patch lane mouth on
|
| 321 |
+
the centre-line; they hunt the nearest harv (which is
|
| 322 |
+
always the active spawn's, since the off-axis spawn
|
| 323 |
+
isn't placed).
|
| 324 |
+
(3) TECH — war factory (`weap`) by tick 4500 (clock down from
|
| 325 |
+
5400).
|
| 326 |
+
Win = `weap` AND `proc` AND ≥1 harvester AND `fact` standing
|
| 327 |
+
before tick 4500. Stall, focus-defence-skip-tech,
|
| 328 |
+
focus-tech-lose-econ, and slow-multi-handle (over-budget on
|
| 329 |
+
clock) all LOSE.
|
| 330 |
+
overrides:
|
| 331 |
+
actors:
|
| 332 |
+
# Spawn 0 — NORTH base (y centered on 14). Four 1tnk
|
| 333 |
+
# defenders ring the two harv latitudes (12 and 16).
|
| 334 |
+
- {type: fact, owner: agent, position: [10, 14], spawn_point: 0}
|
| 335 |
+
- {type: proc, owner: agent, position: [8, 14], spawn_point: 0}
|
| 336 |
+
- {type: powr, owner: agent, position: [8, 16], spawn_point: 0}
|
| 337 |
+
- {type: harv, owner: agent, position: [14, 12], spawn_point: 0}
|
| 338 |
+
- {type: harv, owner: agent, position: [14, 16], spawn_point: 0}
|
| 339 |
+
- {type: '1tnk', owner: agent, position: [18, 12], spawn_point: 0}
|
| 340 |
+
- {type: '1tnk', owner: agent, position: [18, 16], spawn_point: 0}
|
| 341 |
+
- {type: '1tnk', owner: agent, position: [22, 13], spawn_point: 0}
|
| 342 |
+
- {type: '1tnk', owner: agent, position: [22, 15], spawn_point: 0}
|
| 343 |
+
# Spawn 1 — SOUTH base (y centered on 26). Mirror of spawn 0
|
| 344 |
+
# at y=24/28.
|
| 345 |
+
- {type: fact, owner: agent, position: [10, 26], spawn_point: 1}
|
| 346 |
+
- {type: proc, owner: agent, position: [8, 26], spawn_point: 1}
|
| 347 |
+
- {type: powr, owner: agent, position: [8, 28], spawn_point: 1}
|
| 348 |
+
- {type: harv, owner: agent, position: [14, 24], spawn_point: 1}
|
| 349 |
+
- {type: harv, owner: agent, position: [14, 28], spawn_point: 1}
|
| 350 |
+
- {type: '1tnk', owner: agent, position: [18, 24], spawn_point: 1}
|
| 351 |
+
- {type: '1tnk', owner: agent, position: [18, 28], spawn_point: 1}
|
| 352 |
+
- {type: '1tnk', owner: agent, position: [22, 25], spawn_point: 1}
|
| 353 |
+
- {type: '1tnk', owner: agent, position: [22, 27], spawn_point: 1}
|
| 354 |
+
# Mines — neutrals are NOT spawn-filtered (per CLAUDE.md);
|
| 355 |
+
# one set per latitude so each spawn has a workable patch
|
| 356 |
+
# on each of its two harv rows.
|
| 357 |
+
- {type: mine, owner: neutral, position: [24, 12]}
|
| 358 |
+
- {type: mine, owner: neutral, position: [24, 16]}
|
| 359 |
+
- {type: mine, owner: neutral, position: [24, 24]}
|
| 360 |
+
- {type: mine, owner: neutral, position: [24, 28]}
|
| 361 |
+
# ECONOMY-RAID — TWO raider tanks (matching medium) at the
|
| 362 |
+
# patch lane mouth. Placed at the centre-line latitudes
|
| 363 |
+
# (y=18, y=22) so they sit equidistant from the two
|
| 364 |
+
# possible base latitudes (y=14 north / y=26 south) and the
|
| 365 |
+
# hunt bot resolves each raider to whichever harv is
|
| 366 |
+
# currently nearest. (Enemy actors are not spawn-filtered.)
|
| 367 |
+
- {type: '1tnk', owner: enemy, position: [60, 18]}
|
| 368 |
+
- {type: '1tnk', owner: enemy, position: [60, 22]}
|
| 369 |
+
# BASE-HUNT — one heavy hunt squad (matching medium: 4 e1 +
|
| 370 |
+
# 1 e3) at the centre-line so it charges the nearest base
|
| 371 |
+
# buildings whichever latitude the agent is on (the e1 at
|
| 372 |
+
# the centre is ≤12 cells from either spawn's fact, and the
|
| 373 |
+
# rotation between spawns is the seed-driven variation).
|
| 374 |
+
- {type: e1, owner: enemy, position: [40, 20], stance: 3, count: 4}
|
| 375 |
+
- {type: e3, owner: enemy, position: [42, 20], stance: 3, count: 1}
|
| 376 |
+
# Anti-DRAW marker.
|
| 377 |
+
- {type: fact, owner: enemy, position: [120, 20]}
|
| 378 |
+
win_condition:
|
| 379 |
+
all_of:
|
| 380 |
+
- {has_building: weap}
|
| 381 |
+
- {has_building: proc}
|
| 382 |
+
- {unit_type_count_gte: {type: harv, n: 1}}
|
| 383 |
+
- {has_building: fact}
|
| 384 |
+
# See easy: post-weap EV bar makes the harvest loop load-
|
| 385 |
+
# bearing so focus-tech-lose-econ cannot satisfy the win.
|
| 386 |
+
# Bar held at 1000 (same as easy/medium) — the discriminator
|
| 387 |
+
# on hard is the tighter clock + spawn variation, not a
|
| 388 |
+
# harder EV target.
|
| 389 |
+
- {economy_value_gte: 1000}
|
| 390 |
+
- {within_ticks: 4500}
|
| 391 |
+
fail_condition:
|
| 392 |
+
any_of:
|
| 393 |
+
- {after_ticks: 4501}
|
| 394 |
+
- {not: {has_building: fact}}
|
| 395 |
+
- {not: {unit_type_count_gte: {type: harv, n: 1}}}
|
| 396 |
+
max_turns: 51
|
|
@@ -486,6 +486,12 @@ UPGRADED = [
|
|
| 486 |
# assignment flips per seed and a memorised "which jeep to which
|
| 487 |
# corner" plan cannot generalise.
|
| 488 |
"scout-map-reveal-percent-target",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
]
|
| 490 |
|
| 491 |
# Consciously NOT spawn-varied, with the reason (keeps the curation
|
|
|
|
| 486 |
# assignment flips per seed and a memorised "which jeep to which
|
| 487 |
# corner" plan cannot generalise.
|
| 488 |
"scout-map-reveal-percent-target",
|
| 489 |
+
# Wave-6 compound-incident triage pack (defence + economy + tech
|
| 490 |
+
# under simultaneous pressure). Hard tier defines two agent
|
| 491 |
+
# spawn_point groups (NORTH y=14 / SOUTH y=26) round-robined by
|
| 492 |
+
# seed; the centre-line raider tanks and centre-line hunt squad
|
| 493 |
+
# face the seed-chosen latitude with equal pressure.
|
| 494 |
+
"rob-multiple-simultaneous-pressures",
|
| 495 |
]
|
| 496 |
|
| 497 |
# Consciously NOT spawn-varied, with the reason (keeps the curation
|
|
@@ -0,0 +1,437 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""rob-multiple-simultaneous-pressures — compound-incident triage.
|
| 2 |
+
|
| 3 |
+
Wave-6 REASONING pack: three live pressures must be handled at once —
|
| 4 |
+
DEFENCE (hunt charge on the fact), ECONOMY (raider tanks on the harv
|
| 5 |
+
patch), TECH (`weap` by deadline). Single-axis policies cannot win;
|
| 6 |
+
the intended multi-handle keeps all three loops running in parallel.
|
| 7 |
+
|
| 8 |
+
Bar (per CLAUDE.md): the intended multi-handle policy must WIN on
|
| 9 |
+
every (level, seed); stall / focus-defence-skip-tech / focus-tech-
|
| 10 |
+
lose-econ must LOSE on every (level, seed). No draws.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import types
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
import pytest
|
| 19 |
+
|
| 20 |
+
pytest.importorskip("openra_rl_training", reason="Rust env wheel not installed")
|
| 21 |
+
from openra_bench.scenarios import load_pack
|
| 22 |
+
from openra_bench.scenarios.loader import compile_level
|
| 23 |
+
from openra_bench.scenarios.win_conditions import WinContext, evaluate
|
| 24 |
+
|
| 25 |
+
PACKS = Path(__file__).parent.parent / "openra_bench" / "scenarios" / "packs"
|
| 26 |
+
PACK_PATH = PACKS / "rob-multiple-simultaneous-pressures.yaml"
|
| 27 |
+
LEVELS = ("easy", "medium", "hard")
|
| 28 |
+
SEEDS = (1, 2, 3, 4)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# ── Pack-shape tests (cheap; do not run the engine) ─────────────────
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def test_pack_compiles_with_three_levels():
|
| 35 |
+
pack = load_pack(PACK_PATH)
|
| 36 |
+
assert pack.meta.id == "rob-multiple-simultaneous-pressures"
|
| 37 |
+
assert pack.meta.capability == "reasoning"
|
| 38 |
+
assert set(pack.levels) == {"easy", "medium", "hard"}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def test_meta_benchmark_anchor_includes_required_anchors():
|
| 42 |
+
pack = load_pack(PACK_PATH)
|
| 43 |
+
anchors = pack.meta.benchmark_anchor or []
|
| 44 |
+
joined = " ".join(anchors).lower()
|
| 45 |
+
for needle in (
|
| 46 |
+
"sc2 multi-front",
|
| 47 |
+
"ops triage",
|
| 48 |
+
"dr multi-system",
|
| 49 |
+
"military",
|
| 50 |
+
):
|
| 51 |
+
assert needle in joined, f"missing anchor keyword: {needle!r}"
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def test_uses_hunt_bot():
|
| 55 |
+
"""A `hunt` bot drives both pressure axes — the raiders at the
|
| 56 |
+
patch hunt the harvesters; the flanking infantry hunt the base
|
| 57 |
+
buildings. The pack must declare `hunt`."""
|
| 58 |
+
pack = load_pack(PACK_PATH)
|
| 59 |
+
enemy = pack.base.get("enemy") if isinstance(pack.base, dict) else None
|
| 60 |
+
assert enemy is not None
|
| 61 |
+
bot = enemy.get("bot_type") or enemy.get("bot")
|
| 62 |
+
assert bot == "hunt", f"expected hunt bot, got {bot!r}"
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def test_starting_cash_is_2000_exact():
|
| 66 |
+
"""starting_cash must equal weap cost (2000) so cash slack cannot
|
| 67 |
+
be used to bypass either the tech or the income-from-harvest loop."""
|
| 68 |
+
pack = load_pack(PACK_PATH)
|
| 69 |
+
assert pack.starting_cash == 2000, (
|
| 70 |
+
f"expected starting_cash 2000 (weap cost), got {pack.starting_cash}"
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def test_every_level_has_fail_condition():
|
| 75 |
+
pack = load_pack(PACK_PATH)
|
| 76 |
+
for lvl in LEVELS:
|
| 77 |
+
c = compile_level(pack, lvl)
|
| 78 |
+
assert c.fail_condition is not None, f"{lvl} missing fail_condition"
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def test_hard_has_two_spawn_point_groups():
|
| 82 |
+
"""Hard-tier curation contract: ≥2 distinct agent spawn_point
|
| 83 |
+
groups so the seed round-robins the base latitude."""
|
| 84 |
+
c = compile_level(load_pack(PACK_PATH), "hard")
|
| 85 |
+
sp = {
|
| 86 |
+
(a.spawn_point if a.spawn_point is not None else 0)
|
| 87 |
+
for a in c.scenario.actors
|
| 88 |
+
if a.owner == "agent"
|
| 89 |
+
}
|
| 90 |
+
assert len(sp) >= 2, f"hard needs ≥2 spawn_point groups, got {sp}"
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def test_tick_budget_aligned_with_max_turns():
|
| 94 |
+
"""within_ticks and after_ticks must be reachable inside max_turns
|
| 95 |
+
(~90 ticks/turn): otherwise the deadline never bites and a stall
|
| 96 |
+
draws instead of losing."""
|
| 97 |
+
pack = load_pack(PACK_PATH)
|
| 98 |
+
for lvl in LEVELS:
|
| 99 |
+
level_def = pack.levels[lvl]
|
| 100 |
+
max_turns = level_def.max_turns
|
| 101 |
+
reachable = 93 + 90 * (max_turns - 1)
|
| 102 |
+
win = compile_level(pack, lvl).win_condition.model_dump(exclude_none=True)
|
| 103 |
+
|
| 104 |
+
def _collect(node, key, out):
|
| 105 |
+
if isinstance(node, dict):
|
| 106 |
+
if key in node:
|
| 107 |
+
out.append(node[key])
|
| 108 |
+
for v in node.values():
|
| 109 |
+
_collect(v, key, out)
|
| 110 |
+
elif isinstance(node, list):
|
| 111 |
+
for v in node:
|
| 112 |
+
_collect(v, key, out)
|
| 113 |
+
wts: list = []
|
| 114 |
+
_collect(win, "within_ticks", wts)
|
| 115 |
+
assert wts, f"{lvl} has no within_ticks clock teeth"
|
| 116 |
+
for wt in wts:
|
| 117 |
+
assert wt <= reachable, (
|
| 118 |
+
f"{lvl} within_ticks={wt} > reachable={reachable}"
|
| 119 |
+
)
|
| 120 |
+
fail = compile_level(pack, lvl).fail_condition.model_dump(exclude_none=True)
|
| 121 |
+
ats: list = []
|
| 122 |
+
_collect(fail, "after_ticks", ats)
|
| 123 |
+
assert ats, f"{lvl} has no after_ticks fail teeth"
|
| 124 |
+
for at in ats:
|
| 125 |
+
assert at <= reachable, (
|
| 126 |
+
f"{lvl} after_ticks={at} > reachable={reachable}"
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
# ── Predicate-level checks (synthesised WinContext, no engine) ──────
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def _ctx(*, own_buildings=(), units=(), tick=1000, cash=2000, resources=0):
|
| 134 |
+
sig = types.SimpleNamespace(
|
| 135 |
+
game_tick=tick,
|
| 136 |
+
units_killed=0,
|
| 137 |
+
units_lost=0,
|
| 138 |
+
own_buildings=list(own_buildings),
|
| 139 |
+
own_building_types={t for (t, _, _) in own_buildings},
|
| 140 |
+
enemies_seen_ids=set(),
|
| 141 |
+
enemy_buildings_seen_ids=set(),
|
| 142 |
+
cash=cash,
|
| 143 |
+
resources=resources,
|
| 144 |
+
then_progress={},
|
| 145 |
+
)
|
| 146 |
+
return WinContext(
|
| 147 |
+
signals=sig,
|
| 148 |
+
render_state={"units_summary": list(units)},
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def test_predicates_easy():
|
| 153 |
+
c = compile_level(load_pack(PACK_PATH), "easy")
|
| 154 |
+
full = [
|
| 155 |
+
("fact", 10, 18), ("proc", 8, 18), ("powr", 8, 20),
|
| 156 |
+
("weap", 14, 18),
|
| 157 |
+
]
|
| 158 |
+
harvs = [
|
| 159 |
+
{"cell_x": 14, "cell_y": 16, "type": "harv"},
|
| 160 |
+
{"cell_x": 14, "cell_y": 22, "type": "harv"},
|
| 161 |
+
]
|
| 162 |
+
# WIN: weap + proc + ≥1 harv + fact + EV ≥ 1000, in time.
|
| 163 |
+
assert evaluate(
|
| 164 |
+
c.win_condition,
|
| 165 |
+
_ctx(own_buildings=full, units=harvs, tick=3000, cash=1200),
|
| 166 |
+
)
|
| 167 |
+
# No weap → not a win.
|
| 168 |
+
no_weap = [b for b in full if b[0] != "weap"]
|
| 169 |
+
assert not evaluate(
|
| 170 |
+
c.win_condition,
|
| 171 |
+
_ctx(own_buildings=no_weap, units=harvs, tick=3000, cash=1200),
|
| 172 |
+
)
|
| 173 |
+
# EV below 1000 → not a win (focus-tech-lose-econ failure mode).
|
| 174 |
+
assert not evaluate(
|
| 175 |
+
c.win_condition,
|
| 176 |
+
_ctx(own_buildings=full, units=harvs, tick=3000, cash=0),
|
| 177 |
+
)
|
| 178 |
+
# No harv → fail.
|
| 179 |
+
assert evaluate(
|
| 180 |
+
c.fail_condition,
|
| 181 |
+
_ctx(own_buildings=full, units=[], tick=3000, cash=1200),
|
| 182 |
+
)
|
| 183 |
+
# No fact (base razed) → fail.
|
| 184 |
+
no_fact = [b for b in full if b[0] != "fact"]
|
| 185 |
+
assert evaluate(
|
| 186 |
+
c.fail_condition,
|
| 187 |
+
_ctx(own_buildings=no_fact, units=harvs, tick=3000, cash=1200),
|
| 188 |
+
)
|
| 189 |
+
# Past deadline → fail.
|
| 190 |
+
assert evaluate(
|
| 191 |
+
c.fail_condition,
|
| 192 |
+
_ctx(own_buildings=full, units=harvs, tick=5402, cash=1200),
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def test_predicates_hard_tighter_clock():
|
| 197 |
+
"""Hard's within_ticks is 4500 (tighter than easy/medium's 5400)."""
|
| 198 |
+
c = compile_level(load_pack(PACK_PATH), "hard")
|
| 199 |
+
full = [
|
| 200 |
+
("fact", 10, 14), ("proc", 8, 14), ("powr", 8, 16),
|
| 201 |
+
("weap", 14, 14),
|
| 202 |
+
]
|
| 203 |
+
harvs = [{"cell_x": 14, "cell_y": 12, "type": "harv"}]
|
| 204 |
+
assert evaluate(
|
| 205 |
+
c.win_condition,
|
| 206 |
+
_ctx(own_buildings=full, units=harvs, tick=4000, cash=1200),
|
| 207 |
+
)
|
| 208 |
+
assert evaluate(
|
| 209 |
+
c.fail_condition,
|
| 210 |
+
_ctx(own_buildings=full, units=harvs, tick=4501, cash=1200),
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
# ── engine-driven scripted policies ─────────────────────────────────
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def _find_units(rs, type_name):
|
| 218 |
+
return [
|
| 219 |
+
u for u in (rs.get("units_summary", []) or [])
|
| 220 |
+
if str(u.get("type", "")).lower() == type_name
|
| 221 |
+
]
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def _base_xy(rs):
|
| 225 |
+
facts = [
|
| 226 |
+
b for b in (rs.get("own_buildings", []) or [])
|
| 227 |
+
if str(b.get("type", "")).lower() == "fact"
|
| 228 |
+
]
|
| 229 |
+
if not facts:
|
| 230 |
+
return None
|
| 231 |
+
return min(facts, key=lambda b: b["cell_x"])
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
def _stall(rs, Command):
|
| 235 |
+
"""Idle every turn — no harvest, no build, no defence orders.
|
| 236 |
+
Defenders auto-engage but harvs never harvest (no income) and
|
| 237 |
+
weap is never built ⇒ has_building:weap never latches ⇒ timeout
|
| 238 |
+
LOSS via after_ticks."""
|
| 239 |
+
return [Command.observe()]
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def _focus_defence_skip_tech(rs, Command):
|
| 243 |
+
"""Hold every defender at the lane mouth; KICK harvs on the patch
|
| 244 |
+
(income flows); but NEVER queue weap. Weap is required by the win
|
| 245 |
+
predicate, so this LOSES on the clock even though the base may
|
| 246 |
+
survive."""
|
| 247 |
+
cmds = []
|
| 248 |
+
base = _base_xy(rs)
|
| 249 |
+
bx = base["cell_x"] if base else 10
|
| 250 |
+
by = base["cell_y"] if base else 18
|
| 251 |
+
harvs = _find_units(rs, "harv")
|
| 252 |
+
patch_xs = [24, 24, 24, 24]
|
| 253 |
+
patch_ys = [by - 2, by + 2, by - 4, by + 4]
|
| 254 |
+
for i, h in enumerate(harvs):
|
| 255 |
+
mx = patch_xs[i % len(patch_xs)]
|
| 256 |
+
my = patch_ys[i % len(patch_ys)]
|
| 257 |
+
cmds.append(Command.harvest([str(h["id"])], mx, my))
|
| 258 |
+
for u in (rs.get("units_summary", []) or []):
|
| 259 |
+
if str(u.get("type", "")).lower() == "1tnk":
|
| 260 |
+
cmds.append(
|
| 261 |
+
Command.move_units([str(u["id"])], target_x=bx + 4, target_y=by)
|
| 262 |
+
)
|
| 263 |
+
if not cmds:
|
| 264 |
+
cmds.append(Command.observe())
|
| 265 |
+
return cmds
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def _focus_tech_lose_econ_policy():
|
| 269 |
+
"""Queue weap from turn 1 (using the starting cash) BUT pull every
|
| 270 |
+
defender AND harvester to the base — the patch is abandoned, so
|
| 271 |
+
raiders eat the harvs (unit_type_count_gte:{harv,1} ⇒ fail). Even
|
| 272 |
+
if weap finishes, the harv-count fail clause kills the run."""
|
| 273 |
+
state = {"weap_queued": False, "weap_attempts": 0}
|
| 274 |
+
|
| 275 |
+
def pol(rs, Command):
|
| 276 |
+
ob = rs.get("own_buildings", []) or []
|
| 277 |
+
own_b = {b["type"] for b in ob}
|
| 278 |
+
prod = rs.get("production", []) or []
|
| 279 |
+
cash = int(rs.get("cash", 0) or 0)
|
| 280 |
+
base = _base_xy(rs)
|
| 281 |
+
bx = base["cell_x"] if base else 10
|
| 282 |
+
by = base["cell_y"] if base else 18
|
| 283 |
+
cmds = []
|
| 284 |
+
if (
|
| 285 |
+
"weap" not in own_b
|
| 286 |
+
and "weap" not in prod
|
| 287 |
+
and cash >= 2000
|
| 288 |
+
):
|
| 289 |
+
cmds.append(Command.build("weap"))
|
| 290 |
+
state["weap_queued"] = True
|
| 291 |
+
if "weap" not in own_b:
|
| 292 |
+
i = state["weap_attempts"]
|
| 293 |
+
cmds.append(
|
| 294 |
+
Command.place_building("weap", bx + 6 + (i % 4), by - 2 + (i // 4))
|
| 295 |
+
)
|
| 296 |
+
state["weap_attempts"] += 1
|
| 297 |
+
for u in (rs.get("units_summary", []) or []):
|
| 298 |
+
cmds.append(
|
| 299 |
+
Command.move_units([str(u["id"])], target_x=bx, target_y=by)
|
| 300 |
+
)
|
| 301 |
+
if not cmds:
|
| 302 |
+
cmds.append(Command.observe())
|
| 303 |
+
return cmds
|
| 304 |
+
|
| 305 |
+
return pol
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
def _intended_multi_handle_policy():
|
| 309 |
+
"""The intended REASONING capability play: handle all three
|
| 310 |
+
pressures in parallel from turn 1.
|
| 311 |
+
|
| 312 |
+
PRESSURE A (economy/raid): keep both harvesters in `harvest`
|
| 313 |
+
mode every turn — they cycle between proc and the local
|
| 314 |
+
mine; income accrues to top up the tech buy. The pre-placed
|
| 315 |
+
tank ring is on Defend stance and auto-fires on incoming
|
| 316 |
+
raiders at the patch lane mouth (no explicit move orders
|
| 317 |
+
needed — moving the defenders OFF the ring would abandon
|
| 318 |
+
the patch and lose).
|
| 319 |
+
PRESSURE B (defence/hunt): the ring tanks ALSO catch any
|
| 320 |
+
flanking hunter that crosses the patch column on its way to
|
| 321 |
+
the fact (Defend stance auto-engage). For close-in hunters
|
| 322 |
+
the harv proximity is enough to make them targets too.
|
| 323 |
+
PRESSURE C (tech): queue `weap` on turn 1 with the starting
|
| 324 |
+
cash, place it immediately in a safe slot west of the base
|
| 325 |
+
(between fact and proc, away from the hunt approach).
|
| 326 |
+
"""
|
| 327 |
+
state = {"weap_queued": False, "weap_attempts": 0}
|
| 328 |
+
|
| 329 |
+
def pol(rs, Command):
|
| 330 |
+
ob = rs.get("own_buildings", []) or []
|
| 331 |
+
own_b = {b["type"] for b in ob}
|
| 332 |
+
prod = rs.get("production", []) or []
|
| 333 |
+
cash = int(rs.get("cash", 0) or 0)
|
| 334 |
+
base = _base_xy(rs)
|
| 335 |
+
bx = base["cell_x"] if base else 10
|
| 336 |
+
by = base["cell_y"] if base else 18
|
| 337 |
+
cmds = []
|
| 338 |
+
|
| 339 |
+
if (
|
| 340 |
+
"weap" not in own_b
|
| 341 |
+
and "weap" not in prod
|
| 342 |
+
and cash >= 2000
|
| 343 |
+
):
|
| 344 |
+
cmds.append(Command.build("weap"))
|
| 345 |
+
state["weap_queued"] = True
|
| 346 |
+
if "weap" not in own_b:
|
| 347 |
+
i = state["weap_attempts"]
|
| 348 |
+
cmds.append(
|
| 349 |
+
Command.place_building(
|
| 350 |
+
"weap", bx + 4 + (i % 4), by + (i // 4)
|
| 351 |
+
)
|
| 352 |
+
)
|
| 353 |
+
state["weap_attempts"] += 1
|
| 354 |
+
|
| 355 |
+
harvs = _find_units(rs, "harv")
|
| 356 |
+
patch_xs = [24, 24, 24, 24]
|
| 357 |
+
patch_ys = [by - 2, by + 2, by - 4, by + 4]
|
| 358 |
+
for i, h in enumerate(harvs):
|
| 359 |
+
mx = patch_xs[i % len(patch_xs)]
|
| 360 |
+
my = patch_ys[i % len(patch_ys)]
|
| 361 |
+
cmds.append(Command.harvest([str(h["id"])], mx, my))
|
| 362 |
+
|
| 363 |
+
if not cmds:
|
| 364 |
+
cmds.append(Command.observe())
|
| 365 |
+
return cmds
|
| 366 |
+
|
| 367 |
+
return pol
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
# ── Engine-bound tests (parameterised over seeds 1..4) ───────────────
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
@pytest.mark.parametrize("seed", SEEDS)
|
| 374 |
+
@pytest.mark.parametrize("level", LEVELS)
|
| 375 |
+
def test_intended_multi_handle_wins(level, seed):
|
| 376 |
+
"""Intended multi-handle must WIN on every (level, seed)."""
|
| 377 |
+
pytest.importorskip("openra_train")
|
| 378 |
+
from openra_bench.eval_core import run_level
|
| 379 |
+
|
| 380 |
+
c = compile_level(load_pack(PACK_PATH), level)
|
| 381 |
+
res = run_level(c, _intended_multi_handle_policy(), seed=seed)
|
| 382 |
+
own_b = res.signals.own_building_types
|
| 383 |
+
assert res.outcome == "win", (
|
| 384 |
+
f"intended multi-handle must WIN on {level} s={seed}; got "
|
| 385 |
+
f"{res.outcome} turns={res.turns} own_buildings={own_b} "
|
| 386 |
+
f"units_lost={res.signals.units_lost} cash={res.signals.cash}"
|
| 387 |
+
)
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
@pytest.mark.parametrize("seed", SEEDS)
|
| 391 |
+
@pytest.mark.parametrize("level", LEVELS)
|
| 392 |
+
def test_stall_loses(level, seed):
|
| 393 |
+
"""Stall must be a real timeout LOSS on every (level, seed)."""
|
| 394 |
+
pytest.importorskip("openra_train")
|
| 395 |
+
from openra_bench.eval_core import run_level
|
| 396 |
+
|
| 397 |
+
c = compile_level(load_pack(PACK_PATH), level)
|
| 398 |
+
res = run_level(c, _stall, seed=seed)
|
| 399 |
+
assert res.outcome == "loss", (
|
| 400 |
+
f"stall must LOSS on {level} s={seed}; got {res.outcome} "
|
| 401 |
+
f"own_buildings={res.signals.own_building_types}"
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
@pytest.mark.parametrize("seed", SEEDS)
|
| 406 |
+
@pytest.mark.parametrize("level", LEVELS)
|
| 407 |
+
def test_focus_defence_skip_tech_loses(level, seed):
|
| 408 |
+
"""Focus on defence and never queue weap ⇒ has_building:weap
|
| 409 |
+
never latches ⇒ timeout LOSS on every (level, seed)."""
|
| 410 |
+
pytest.importorskip("openra_train")
|
| 411 |
+
from openra_bench.eval_core import run_level
|
| 412 |
+
|
| 413 |
+
c = compile_level(load_pack(PACK_PATH), level)
|
| 414 |
+
res = run_level(c, _focus_defence_skip_tech, seed=seed)
|
| 415 |
+
own_b = res.signals.own_building_types
|
| 416 |
+
assert res.outcome == "loss", (
|
| 417 |
+
f"focus-defence-skip-tech must LOSE on {level} s={seed}; got "
|
| 418 |
+
f"{res.outcome} own_buildings={own_b}"
|
| 419 |
+
)
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
@pytest.mark.parametrize("seed", SEEDS)
|
| 423 |
+
@pytest.mark.parametrize("level", LEVELS)
|
| 424 |
+
def test_focus_tech_lose_econ_loses(level, seed):
|
| 425 |
+
"""Focus on tech (queue weap, pull everything home) ⇒ harvs
|
| 426 |
+
abandoned ⇒ fail by harv-count (or by clock). Must LOSE."""
|
| 427 |
+
pytest.importorskip("openra_train")
|
| 428 |
+
from openra_bench.eval_core import run_level
|
| 429 |
+
|
| 430 |
+
c = compile_level(load_pack(PACK_PATH), level)
|
| 431 |
+
res = run_level(c, _focus_tech_lose_econ_policy(), seed=seed)
|
| 432 |
+
own_b = res.signals.own_building_types
|
| 433 |
+
assert res.outcome == "loss", (
|
| 434 |
+
f"focus-tech-lose-econ must LOSE on {level} s={seed}; got "
|
| 435 |
+
f"{res.outcome} own_buildings={own_b} "
|
| 436 |
+
f"units_lost={res.signals.units_lost}"
|
| 437 |
+
)
|