Spaces:
Running
feat(scenario): econ-contention-with-enemy — contested mining (SC2 / common-pool tragedy-of-commons anchor)
Browse filesWave-5 Group F REASONING pack. A shared central ore patch sits between
the agent and an enemy that fields its own (flavor) harvesters AND a
worker-priority `raider` bot. The bench-binding decision is the
COMMON-POOL TRADEOFF: pure mining (no defense) lets the raider pick
off harvs at the contested patch; pure defending abandons the revenue
stream; stalling commits to neither and the economy bar never bites.
The intended COMMITTED-BALANCE play keeps the pre-ringed defenders on
station (auto-engaging the inbound raider on default Defend stance)
AND keeps every harvester in `harvest` mode each turn — extracting
THROUGH the contestation, not after it.
Anchors: SC2 contested expansion / split-base play; game theory
common-pool resource (Hardin tragedy of commons); competitive market
entry / TAM contention; fishing common-resource dynamics.
Validation (scripted policies, seed 1..4, all levels):
stall : LOSS (no income → bar unmet)
pure-mine-no-defense : LOSS (raider unopposed → harv count drops)
pure-defend : LOSS (no harvest → bar unmet)
intended-balance : WIN (ev 2500-12500, lost 0-1)
Hard tier defines 2 agent spawn_point groups (NORTH y=14 / SOUTH y=26
base orientation, rear-guard 3tnk at y=14 vs y=26 marks the spawn
group); shared harv-lane + defender ring at y=18..22 keep the task
symmetric per spawn while enemy raiders/proc/harvs always place.
Engine notes / footguns documented in the pack:
- pre-placed harv actors have `activity: None` (no auto-harvest);
enemy harvs are FLAVOR (no scripted bot orders them to mine)
- enemy proc + flavor harvs placed at x=76-80 (out of sight at
start) so own defenders don't auto-aggro them before the raider
- persistent enemy `fact` at (128,20) prevents auto-done collapse
- within_ticks=5400 / after_ticks=5401 / max_turns=61 (deadline
bites at tick ceiling 5493)
Files:
openra_bench/scenarios/packs/econ-contention-with-enemy.yaml
tests/test_econ_contention_with_enemy.py
tests/test_hard_tier.py (UPGRADED += econ-contention-with-enemy)
|
@@ -0,0 +1,324 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# econ-contention-with-enemy — contested central ore patch with enemy
|
| 2 |
+
#
|
| 3 |
+
# REASONING capability — shared resource contention. The agent and the
|
| 4 |
+
# enemy both lay claim to a mid-map ore patch. The bench-binding
|
| 5 |
+
# decision is the COMMON-POOL TRADEOFF: pure mining (no defense) lets
|
| 6 |
+
# the enemy `raider` bot pick off the agent's harvesters at the
|
| 7 |
+
# contested patch; pure defending (no harvest orders) abandons the
|
| 8 |
+
# revenue stream; stalling commits to neither and the economy bar
|
| 9 |
+
# never bites. The intended COMMITTED-BALANCE play keeps the
|
| 10 |
+
# pre-ringed defenders on station (auto-engaging the inbound raider)
|
| 11 |
+
# AND keeps every harvester in `harvest` mode each turn so the
|
| 12 |
+
# contested patch yields throughput THROUGH the contestation.
|
| 13 |
+
#
|
| 14 |
+
# Real-world anchors:
|
| 15 |
+
# • SC2 contested expansion / split-mining (third-base dispute) —
|
| 16 |
+
# committing to both mine AND defend instead of one-sided commit.
|
| 17 |
+
# • Common-pool resource (Hardin "tragedy of the commons") — the
|
| 18 |
+
# pasture / fishery is finite, both parties draw from it, and a
|
| 19 |
+
# pure-extract policy that ignores the contender loses access.
|
| 20 |
+
# • Competitive market entry / TAM contention — you and the
|
| 21 |
+
# incumbent both pull from the same total addressable market;
|
| 22 |
+
# pure-extraction without defending the channel loses share.
|
| 23 |
+
# • Fishing common-resource dynamics — a contested patch yields
|
| 24 |
+
# less per boat with more boats present; abandoning the patch
|
| 25 |
+
# means the rival fully captures it.
|
| 26 |
+
#
|
| 27 |
+
# ENGINE NOTES (verified 2026-05-20 with scripted run_level probes
|
| 28 |
+
# at HEAD; smoke runs at /tmp/probe_v4.py + /tmp/probe_pack.py):
|
| 29 |
+
# 1. Pre-placed `harv` actors are inserted with `activity: None`
|
| 30 |
+
# (env.rs::build_scenario_actor) — they do NOT auto-harvest at
|
| 31 |
+
# placement, so every harvester needs an explicit
|
| 32 |
+
# `Command.harvest(...)` order each turn. Stall (only observe)
|
| 33 |
+
# therefore keeps EV at the starting_cash floor (500) regardless
|
| 34 |
+
# of how the raider fight resolves.
|
| 35 |
+
# 2. Pre-placed ENEMY `harv` actors are ALSO inserted with
|
| 36 |
+
# `activity: None` and no scripted bot orders harvesters, so the
|
| 37 |
+
# enemy harvs at the east of the patch are FLAVOR / observable
|
| 38 |
+
# rivals (the agent sees them as "enemy units at the contested
|
| 39 |
+
# patch" in `enemy_positions`) but do NOT actually pull ore. The
|
| 40 |
+
# "common-pool" mechanic is encoded via the raider's worker-
|
| 41 |
+
# priority attack: keeping harvs at the patch under raider
|
| 42 |
+
# pressure IS the contestation cost. (Engine doesn't yet model
|
| 43 |
+
# multi-side simultaneous mining of the same patch; the raider-
|
| 44 |
+
# on-harv attack is the bench-binding contestation.)
|
| 45 |
+
# 3. The `raider` ScriptedBehavior (openra-sim/src/scripted_bot.rs)
|
| 46 |
+
# targets the nearest agent `harv` each turn; falls back to
|
| 47 |
+
# nearest combat actor only when no harv is alive. The agent's
|
| 48 |
+
# pre-ringed defenders (2× 3tnk at x=26 on easy, 3× 3tnk on
|
| 49 |
+
# medium, 4× 3tnk on hard) auto-engage the inbound raider on
|
| 50 |
+
# default `Defend` stance — the intended policy is to LEAVE the
|
| 51 |
+
# defenders ringed (they do their job automatically) and only
|
| 52 |
+
# spend orders on harvest commands.
|
| 53 |
+
# 4. The enemy proc + harvs are placed at x=76-80 (far east), well
|
| 54 |
+
# outside the agent's default sight range at start, so they do
|
| 55 |
+
# NOT auto-aggro the agent's defender ring before the raider
|
| 56 |
+
# arrives. Required for the discriminator: a wandering agent
|
| 57 |
+
# sweep east (the "pure-mine-no-defense + chase enemy proc"
|
| 58 |
+
# failure mode) is what loses, not a passive ringed defender.
|
| 59 |
+
# 5. Bar tuning (probe_pack.py seed-1..4, scripted policies):
|
| 60 |
+
# - stall: ev=500 lost=0..2 (no harvest)
|
| 61 |
+
# - pure-mine-no-defense: ev=500..1500 lost=2..4 (raider unopposed)
|
| 62 |
+
# - pure-defend: ev=500 lost=2..4 (no harvest)
|
| 63 |
+
# - intended-balance: ev=2500..12500 lost=0..1
|
| 64 |
+
# Easy bar 2000, medium bar 3500, hard bar 3500. Every non-intended
|
| 65 |
+
# play falls below the bar (or loses harvs below the required
|
| 66 |
+
# count); intended clears it by a comfortable margin.
|
| 67 |
+
# 6. Tick alignment (CLAUDE.md): max_turns=61 ⇒ ceiling tick
|
| 68 |
+
# 93 + 90·60 = 5493. within_ticks=5400 and after_ticks=5401 both
|
| 69 |
+
# fit; non-finishers LOSE (not draw).
|
| 70 |
+
# 7. Persistent enemy `fact` at (128,20) prevents the engine from
|
| 71 |
+
# auto-`done`-ing when the raider squad is wiped (would collapse
|
| 72 |
+
# a clean run to DRAW before the economy bar is satisfied).
|
| 73 |
+
# 8. Hard tier: seed-varied AGENT spawn_point groups (NORTH y=14 /
|
| 74 |
+
# SOUTH y=26) with symmetric defender rings + shared harv-lane
|
| 75 |
+
# geometry. A rear-guard 3tnk at the base y-row (y=14 vs y=26)
|
| 76 |
+
# marks the spawn so test_hard_tier's units_summary check sees
|
| 77 |
+
# distinct starts. Raider strike force + enemy flavor cluster
|
| 78 |
+
# ALWAYS place (enemy actors don't honour spawn_point —
|
| 79 |
+
# CLAUDE.md), so each seed faces the SAME contestation problem
|
| 80 |
+
# from a different base orientation.
|
| 81 |
+
|
| 82 |
+
meta:
|
| 83 |
+
id: econ-contention-with-enemy
|
| 84 |
+
title: 'Contested Central Ore Patch — Common-Pool Resource vs Enemy Raider'
|
| 85 |
+
capability: reasoning
|
| 86 |
+
real_world_meaning: >
|
| 87 |
+
Common-pool resource contention with an active rival. A shared
|
| 88 |
+
ore patch sits in the middle of the map; the enemy fields its
|
| 89 |
+
own harvesters AND a worker-priority raider bot that drives
|
| 90 |
+
specifically at your harvesters. Pure extraction (mine only,
|
| 91 |
+
don't defend) lets the raider pick off your harvs at the
|
| 92 |
+
contested patch; pure defense (engage the raider, but stop
|
| 93 |
+
mining) abandons the revenue stream; stalling commits to neither
|
| 94 |
+
and the economy clock bites. The intended committed-balance play
|
| 95 |
+
keeps the pre-ringed defenders on station (auto-engaging the
|
| 96 |
+
inbound raider) AND keeps every harvester in `harvest` mode each
|
| 97 |
+
turn — extracting THROUGH the contestation, not after it.
|
| 98 |
+
robotics_analogue: >
|
| 99 |
+
Multi-agent extraction from a shared resource zone with an
|
| 100 |
+
actively-hostile rival fleet. The harvesters keep visiting the
|
| 101 |
+
contested deposit; the defender fleet must intercept the
|
| 102 |
+
rival's worker-priority raiders BEFORE they reach the
|
| 103 |
+
collectors, without leaving the patch unmanned. Abandoning
|
| 104 |
+
either function — extraction or interception — loses the day
|
| 105 |
+
to a one-sided commit.
|
| 106 |
+
benchmark_anchor:
|
| 107 |
+
- "SC2 contested mining / split-base play"
|
| 108 |
+
- "game theory common-pool resource (Hardin tragedy of commons)"
|
| 109 |
+
- "competitive market entry / TAM contention"
|
| 110 |
+
- "fishing common-resource dynamics"
|
| 111 |
+
author: openra-bench
|
| 112 |
+
|
| 113 |
+
base_map: rush-hour-arena
|
| 114 |
+
starting_cash: 500
|
| 115 |
+
|
| 116 |
+
base:
|
| 117 |
+
agent: {faction: allies, cash: 500}
|
| 118 |
+
enemy: {faction: soviet, cash: 0, bot_type: raider}
|
| 119 |
+
tools: [observe, build, place_building, harvest, move_units, attack_unit, attack_move, stop]
|
| 120 |
+
planning: true
|
| 121 |
+
termination: {max_ticks: 7000}
|
| 122 |
+
actors:
|
| 123 |
+
# Base seed — fact + proc for refining + 2 harvs on the patch lane.
|
| 124 |
+
- {type: fact, owner: agent, position: [8, 18]}
|
| 125 |
+
- {type: proc, owner: agent, position: [12, 18]}
|
| 126 |
+
- {type: harv, owner: agent, position: [14, 18]}
|
| 127 |
+
- {type: harv, owner: agent, position: [14, 20]}
|
| 128 |
+
# Two 3tnk defenders ringed east of the contested patch — auto-
|
| 129 |
+
# engage the raider on default Defend stance; the intended policy
|
| 130 |
+
# leaves them ringed and only spends orders on `harvest`.
|
| 131 |
+
- {type: '3tnk', owner: agent, position: [26, 18]}
|
| 132 |
+
- {type: '3tnk', owner: agent, position: [26, 20]}
|
| 133 |
+
# Shared central ore patch — the contention target.
|
| 134 |
+
- {type: mine, owner: neutral, position: [22, 20]}
|
| 135 |
+
- {type: mine, owner: neutral, position: [22, 22]}
|
| 136 |
+
# Enemy CONTESTED FLAVOR — own proc + 2 harvs at the east edge
|
| 137 |
+
# of the patch zone (visible in enemy_positions as "rival
|
| 138 |
+
# harvesters at the contested patch"). Placed at x=76-80 (out
|
| 139 |
+
# of agent sight range at start) so they do NOT auto-aggro the
|
| 140 |
+
# defender ring before the raider arrives.
|
| 141 |
+
- {type: proc, owner: enemy, position: [80, 20]}
|
| 142 |
+
- {type: harv, owner: enemy, position: [76, 18]}
|
| 143 |
+
- {type: harv, owner: enemy, position: [76, 22]}
|
| 144 |
+
# ONE raider tank (easy) — see overrides for medium/hard.
|
| 145 |
+
- {type: '1tnk', owner: enemy, position: [60, 20]}
|
| 146 |
+
# Persistent enemy marker far east — prevents engine auto-`done`
|
| 147 |
+
# on raider squad wipe before the economy bar is satisfied.
|
| 148 |
+
- {type: fact, owner: enemy, position: [128, 20]}
|
| 149 |
+
|
| 150 |
+
levels:
|
| 151 |
+
# ── EASY ─────────────────────────────────────────────────────────
|
| 152 |
+
# The bare skill: ONE weak raider (1tnk), loose bar (2000 EV), 2
|
| 153 |
+
# 3tnk defenders pre-ringed. Stall LOSES (no income); pure-mine-
|
| 154 |
+
# no-defense LOSES (defenders moved off-vector → raider picks off
|
| 155 |
+
# harvs); pure-defend LOSES (no harvest orders → no income); the
|
| 156 |
+
# intended committed-balance play (harvest + leave defenders
|
| 157 |
+
# ringed) WINS comfortably (probed EV ≈ 12500 by tick 5400 on
|
| 158 |
+
# seeds 1..4).
|
| 159 |
+
easy:
|
| 160 |
+
description: >
|
| 161 |
+
Your refinery (`proc` at 12,18) sits west of a shared ore patch
|
| 162 |
+
at (22,20). Two harvesters are pre-placed at (14,18) and
|
| 163 |
+
(14,20); two heavy tanks ring the patch at (26,*). ONE enemy
|
| 164 |
+
raider tank is staged east at (60,20) — it specifically targets
|
| 165 |
+
your harvesters (worker-priority `raider` bot). The enemy also
|
| 166 |
+
fields a refinery and two harvesters at the east side of the
|
| 167 |
+
patch zone (visible as rival harvesters contesting the same
|
| 168 |
+
patch). Win when your economy value (cash + stored resources)
|
| 169 |
+
reaches 2000 AND at least 2 harvesters are still alive, before
|
| 170 |
+
tick 5400. Stalling leaves EV at the 500 starting floor;
|
| 171 |
+
chasing the raider east with your defenders abandons the patch
|
| 172 |
+
and the raider's successor / lone path picks off your harvs;
|
| 173 |
+
pulling harvesters home and only defending stops the income.
|
| 174 |
+
The intended play is to keep both harvesters in `harvest` mode
|
| 175 |
+
AND leave your ringed defenders in place — they auto-engage
|
| 176 |
+
the raider on contact, and throughput climbs unimpeded.
|
| 177 |
+
overrides: {}
|
| 178 |
+
win_condition:
|
| 179 |
+
all_of:
|
| 180 |
+
- {economy_value_gte: 2000}
|
| 181 |
+
- {unit_type_count_gte: {type: harv, n: 2}}
|
| 182 |
+
- {within_ticks: 5400}
|
| 183 |
+
# after_ticks 5401 ≤ 93+90·60 = 5493 ⇒ deadline bites inside
|
| 184 |
+
# max_turns. Loss is reachable: EV/harv bar unmet at the deadline
|
| 185 |
+
# OR harv count drops to 0 first.
|
| 186 |
+
fail_condition:
|
| 187 |
+
any_of:
|
| 188 |
+
- {after_ticks: 5401}
|
| 189 |
+
- {not: {unit_type_count_gte: {type: harv, n: 1}}}
|
| 190 |
+
max_turns: 61
|
| 191 |
+
|
| 192 |
+
# ── MEDIUM ───────────────────────────────────────────────────────
|
| 193 |
+
# +1 controlled variable: FULL raider strike (TWO raider 1tnks
|
| 194 |
+
# staggered on the y-line), tighter bar (3500 EV), and ONE extra
|
| 195 |
+
# defender (3× 3tnk) to keep the intended play winnable. Same
|
| 196 |
+
# patch + agent base geometry as easy.
|
| 197 |
+
medium:
|
| 198 |
+
description: >
|
| 199 |
+
Same patch layout — `proc` at (12,18), mines at (22,20) and
|
| 200 |
+
(22,22), two harvesters at (14,18) and (14,20). The defender
|
| 201 |
+
ring is THREE heavy tanks now (26,18 / 26,20 / 26,22). TWO
|
| 202 |
+
enemy raider tanks now stagger down the y-line at x=60 (one at
|
| 203 |
+
y=18, one at y=22) — both target your harvesters. The enemy
|
| 204 |
+
refinery and two harvesters remain at the east side of the
|
| 205 |
+
contested zone. Win when economy value reaches 3500 AND at
|
| 206 |
+
least 2 harvesters are still alive, before tick 5400. Stalling
|
| 207 |
+
leaves EV at 500; chasing raiders east abandons the harvs to
|
| 208 |
+
whichever raider is in transit; pulling harvs home stops the
|
| 209 |
+
income. The intended play is the same committed balance —
|
| 210 |
+
both harvesters in `harvest` mode AND the ringed defenders on
|
| 211 |
+
station — and throughput clears 3500 with margin.
|
| 212 |
+
overrides:
|
| 213 |
+
actors:
|
| 214 |
+
- {type: fact, owner: agent, position: [8, 18]}
|
| 215 |
+
- {type: proc, owner: agent, position: [12, 18]}
|
| 216 |
+
- {type: harv, owner: agent, position: [14, 18]}
|
| 217 |
+
- {type: harv, owner: agent, position: [14, 20]}
|
| 218 |
+
# MEDIUM: 3 defenders (one more than easy).
|
| 219 |
+
- {type: '3tnk', owner: agent, position: [26, 18]}
|
| 220 |
+
- {type: '3tnk', owner: agent, position: [26, 20]}
|
| 221 |
+
- {type: '3tnk', owner: agent, position: [26, 22]}
|
| 222 |
+
- {type: mine, owner: neutral, position: [22, 20]}
|
| 223 |
+
- {type: mine, owner: neutral, position: [22, 22]}
|
| 224 |
+
- {type: proc, owner: enemy, position: [80, 20]}
|
| 225 |
+
- {type: harv, owner: enemy, position: [76, 18]}
|
| 226 |
+
- {type: harv, owner: enemy, position: [76, 22]}
|
| 227 |
+
# MEDIUM: TWO raider 1tnks staggered on the y-line.
|
| 228 |
+
- {type: '1tnk', owner: enemy, position: [60, 18]}
|
| 229 |
+
- {type: '1tnk', owner: enemy, position: [60, 22]}
|
| 230 |
+
- {type: fact, owner: enemy, position: [128, 20]}
|
| 231 |
+
win_condition:
|
| 232 |
+
all_of:
|
| 233 |
+
- {economy_value_gte: 3500}
|
| 234 |
+
- {unit_type_count_gte: {type: harv, n: 2}}
|
| 235 |
+
- {within_ticks: 5400}
|
| 236 |
+
fail_condition:
|
| 237 |
+
any_of:
|
| 238 |
+
- {after_ticks: 5401}
|
| 239 |
+
- {not: {unit_type_count_gte: {type: harv, n: 1}}}
|
| 240 |
+
max_turns: 61
|
| 241 |
+
|
| 242 |
+
# ── HARD ─────────────────────────────────────────────────────────
|
| 243 |
+
# +2 controlled variables vs medium:
|
| 244 |
+
# 1. THREE enemy harvs flavor (a denser contested zone) + TWO
|
| 245 |
+
# raider tanks attacking from TWO distinct y-vectors (north
|
| 246 |
+
# y=18 and south y=22), so a single-y defender ring would
|
| 247 |
+
# leak one vector. The 4-tank defender ring (y=18..22) handles
|
| 248 |
+
# both vectors.
|
| 249 |
+
# 2. TWO seed-driven AGENT spawn_point groups: NORTH-flank base
|
| 250 |
+
# (fact/proc + rear-guard 3tnk at y=14) vs SOUTH-flank base
|
| 251 |
+
# (y=26). The harv lane + central defender ring are SHARED
|
| 252 |
+
# across spawns (y=18..22) so the committed-balance task is
|
| 253 |
+
# symmetric per spawn; only the base orientation flips. The
|
| 254 |
+
# rear-guard 3tnk at y=14 vs y=26 is the spawn-distinguishing
|
| 255 |
+
# unit (test_hard_tier checks units_summary not buildings).
|
| 256 |
+
# Enemy raiders / proc / harvs ALWAYS place (enemy actors
|
| 257 |
+
# don't honour spawn_point — CLAUDE.md), so each seed faces
|
| 258 |
+
# the same contestation problem from a different base
|
| 259 |
+
# orientation. A memorised "always defend at y=N" opening
|
| 260 |
+
# cannot generalise across the seed flip.
|
| 261 |
+
hard:
|
| 262 |
+
description: >
|
| 263 |
+
A contested mining run against TWO raider tanks attacking from
|
| 264 |
+
two distinct y-vectors (north y=18 and south y=22), with a
|
| 265 |
+
denser enemy contested zone (a refinery and three rival
|
| 266 |
+
harvesters at the east edge). Your base orientation varies by
|
| 267 |
+
seed (north-flank or south-flank) — both halves cover the same
|
| 268 |
+
contested patch column at (22,*). Win when economy value
|
| 269 |
+
reaches 3500 AND at least 2 harvesters are still alive, before
|
| 270 |
+
tick 5400. Stalling, chasing the raiders east, and pulling
|
| 271 |
+
harvesters home all LOSE. The intended play is the same
|
| 272 |
+
committed-balance: both harvesters in `harvest` mode AND the
|
| 273 |
+
ringed defenders on station (covering both y-vectors with the
|
| 274 |
+
4-tank ring), and throughput clears 3500 with margin.
|
| 275 |
+
overrides:
|
| 276 |
+
actors:
|
| 277 |
+
# Spawn 0 — NORTH base (fact/proc at y=14, rear-guard tank
|
| 278 |
+
# at y=14 marks the spawn group so unit positions differ
|
| 279 |
+
# across seeds — test_hard_tier checks units_summary, not
|
| 280 |
+
# buildings).
|
| 281 |
+
- {type: fact, owner: agent, position: [8, 14], spawn_point: 0}
|
| 282 |
+
- {type: proc, owner: agent, position: [12, 14], spawn_point: 0}
|
| 283 |
+
- {type: harv, owner: agent, position: [14, 18], spawn_point: 0}
|
| 284 |
+
- {type: harv, owner: agent, position: [14, 22], spawn_point: 0}
|
| 285 |
+
# 4-tank ring covers y=18..22 — both raider y-vectors.
|
| 286 |
+
- {type: '3tnk', owner: agent, position: [26, 18], spawn_point: 0}
|
| 287 |
+
- {type: '3tnk', owner: agent, position: [26, 20], spawn_point: 0}
|
| 288 |
+
- {type: '3tnk', owner: agent, position: [26, 22], spawn_point: 0}
|
| 289 |
+
# Rear-guard tank at NORTH y=14 (the spawn-distinguishing unit).
|
| 290 |
+
- {type: '3tnk', owner: agent, position: [13, 14], spawn_point: 0}
|
| 291 |
+
- {type: mine, owner: neutral, position: [22, 20], spawn_point: 0}
|
| 292 |
+
- {type: mine, owner: neutral, position: [22, 22], spawn_point: 0}
|
| 293 |
+
# Spawn 1 — SOUTH base (fact/proc at y=26).
|
| 294 |
+
- {type: fact, owner: agent, position: [8, 26], spawn_point: 1}
|
| 295 |
+
- {type: proc, owner: agent, position: [12, 26], spawn_point: 1}
|
| 296 |
+
- {type: harv, owner: agent, position: [14, 18], spawn_point: 1}
|
| 297 |
+
- {type: harv, owner: agent, position: [14, 22], spawn_point: 1}
|
| 298 |
+
- {type: '3tnk', owner: agent, position: [26, 18], spawn_point: 1}
|
| 299 |
+
- {type: '3tnk', owner: agent, position: [26, 20], spawn_point: 1}
|
| 300 |
+
- {type: '3tnk', owner: agent, position: [26, 22], spawn_point: 1}
|
| 301 |
+
# Rear-guard tank at SOUTH y=26.
|
| 302 |
+
- {type: '3tnk', owner: agent, position: [13, 26], spawn_point: 1}
|
| 303 |
+
- {type: mine, owner: neutral, position: [22, 20], spawn_point: 1}
|
| 304 |
+
- {type: mine, owner: neutral, position: [22, 22], spawn_point: 1}
|
| 305 |
+
# Enemy contested zone — DENSER (3 rival harvs + proc).
|
| 306 |
+
# Always placed (no spawn_point on enemy actors per CLAUDE.md).
|
| 307 |
+
- {type: proc, owner: enemy, position: [80, 20]}
|
| 308 |
+
- {type: harv, owner: enemy, position: [76, 16]}
|
| 309 |
+
- {type: harv, owner: enemy, position: [76, 20]}
|
| 310 |
+
- {type: harv, owner: enemy, position: [76, 24]}
|
| 311 |
+
# TWO raiders on TWO y-vectors (north + south).
|
| 312 |
+
- {type: '1tnk', owner: enemy, position: [60, 18]}
|
| 313 |
+
- {type: '1tnk', owner: enemy, position: [60, 22]}
|
| 314 |
+
- {type: fact, owner: enemy, position: [128, 20]}
|
| 315 |
+
win_condition:
|
| 316 |
+
all_of:
|
| 317 |
+
- {economy_value_gte: 3500}
|
| 318 |
+
- {unit_type_count_gte: {type: harv, n: 2}}
|
| 319 |
+
- {within_ticks: 5400}
|
| 320 |
+
fail_condition:
|
| 321 |
+
any_of:
|
| 322 |
+
- {after_ticks: 5401}
|
| 323 |
+
- {not: {unit_type_count_gte: {type: harv, n: 1}}}
|
| 324 |
+
max_turns: 61
|
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""econ-contention-with-enemy — REASONING capability validation.
|
| 2 |
+
|
| 3 |
+
Common-pool contested mining. A shared central ore patch sits between
|
| 4 |
+
the agent and an enemy that fields its own (flavor) harvesters AND a
|
| 5 |
+
worker-priority `raider` bot that drives at the agent's harvesters.
|
| 6 |
+
|
| 7 |
+
Bar (CLAUDE.md "no defect, no cheat"):
|
| 8 |
+
- stall LOSES every tier / seed (no harvest cmd → EV stuck at the
|
| 9 |
+
starting_cash floor → bar unmet → timeout LOSS).
|
| 10 |
+
- pure-mine-no-defense LOSES (harvest but move defenders FAR off-
|
| 11 |
+
vector → raider's worker-priority attack runs unopposed → harvs
|
| 12 |
+
die → fail by harv count).
|
| 13 |
+
- pure-defend LOSES (defenders engage, NO harvest orders → no
|
| 14 |
+
income → bar unmet → timeout LOSS).
|
| 15 |
+
- intended-balance WINS (harvest + leave the pre-ringed defenders
|
| 16 |
+
on station — they auto-engage on Defend stance; throughput climbs
|
| 17 |
+
uninterrupted).
|
| 18 |
+
- hard tier defines ≥2 agent spawn_point groups (NORTH y=14 /
|
| 19 |
+
SOUTH y=26 base orientation, rear-guard tank marks the spawn)
|
| 20 |
+
so a memorised opening cannot generalise.
|
| 21 |
+
|
| 22 |
+
Anchors: SC2 contested expansion / split-mining; game theory common-
|
| 23 |
+
pool resource (Hardin tragedy of commons); competitive market entry /
|
| 24 |
+
TAM contention; fishing common-resource dynamics.
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
from pathlib import Path
|
| 30 |
+
|
| 31 |
+
import pytest
|
| 32 |
+
|
| 33 |
+
pytest.importorskip("openra_train", reason="Rust env wheel not installed")
|
| 34 |
+
|
| 35 |
+
pytest.importorskip("openra_rl_training", reason="Rust env wheel not installed")
|
| 36 |
+
from openra_bench.eval_core import run_level
|
| 37 |
+
from openra_bench.scenarios import load_pack
|
| 38 |
+
from openra_bench.scenarios.loader import PACKS_DIR, compile_level
|
| 39 |
+
from openra_bench.scenarios.win_conditions import WinContext, evaluate
|
| 40 |
+
|
| 41 |
+
PACK = PACKS_DIR / "econ-contention-with-enemy.yaml"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# ── policies ────────────────────────────────────────────────────────
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _stall(rs, Command):
|
| 48 |
+
return [Command.observe()]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _pure_mine_no_defense(rs, Command):
|
| 52 |
+
"""Harvest only — move defenders to the FAR NW corner so the
|
| 53 |
+
raider's worker-priority attack on the harv lane runs unopposed.
|
| 54 |
+
|
| 55 |
+
Movement uses `move_units` (not `attack_move`) so defenders don't
|
| 56 |
+
sweep-engage the raider en route; they leave the harv lane and
|
| 57 |
+
don't come back. The patch column at x=22 works for every spawn
|
| 58 |
+
(the harv geometry is shared north/south on hard)."""
|
| 59 |
+
units = rs.get("units_summary", []) or []
|
| 60 |
+
harvs = [u for u in units if u.get("type") == "harv"]
|
| 61 |
+
tanks = [u for u in units if u.get("type") in ("1tnk", "3tnk")]
|
| 62 |
+
cmds = []
|
| 63 |
+
for h in harvs:
|
| 64 |
+
cmds.append(Command.harvest([str(h["id"])], 22, 20))
|
| 65 |
+
for t in tanks:
|
| 66 |
+
cmds.append(Command.move_units([str(t["id"])], 5, 5))
|
| 67 |
+
return cmds or [Command.observe()]
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def _pure_defend(rs, Command):
|
| 71 |
+
"""Defenders attack-move east, NO harvest orders → no income."""
|
| 72 |
+
units = rs.get("units_summary", []) or []
|
| 73 |
+
tanks = [u for u in units if u.get("type") in ("1tnk", "3tnk")]
|
| 74 |
+
cmds = []
|
| 75 |
+
for t in tanks:
|
| 76 |
+
cmds.append(Command.attack_move([str(t["id"])], 60, 20))
|
| 77 |
+
return cmds or [Command.observe()]
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _intended(rs, Command):
|
| 81 |
+
"""Committed balance: every harv in `harvest` mode at the contested
|
| 82 |
+
patch (22,20); defenders stay ringed and auto-engage the raider on
|
| 83 |
+
default Defend stance — no explicit defender orders needed."""
|
| 84 |
+
units = rs.get("units_summary", []) or []
|
| 85 |
+
harvs = [u for u in units if u.get("type") == "harv"]
|
| 86 |
+
cmds = []
|
| 87 |
+
for h in harvs:
|
| 88 |
+
cmds.append(Command.harvest([str(h["id"])], 22, 20))
|
| 89 |
+
return cmds or [Command.observe()]
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
# ── helpers ─────────────────────────────────────────────────────────
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _run(level, policy, seed=1):
|
| 96 |
+
c = compile_level(load_pack(PACK), level)
|
| 97 |
+
assert c.map_supported, "rush-hour-arena terrain must be present"
|
| 98 |
+
return c, run_level(c, policy, seed=seed)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _ev(res):
|
| 102 |
+
return res.signals.cash + res.signals.resources
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
# ── structural ──────────────────────────────────────────────────────
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def test_pack_loads_and_meta_active():
|
| 109 |
+
pack = load_pack(PACK)
|
| 110 |
+
assert pack.meta.id == "econ-contention-with-enemy"
|
| 111 |
+
assert pack.meta.capability == "reasoning"
|
| 112 |
+
assert pack.meta.real_world_meaning
|
| 113 |
+
assert pack.meta.robotics_analogue
|
| 114 |
+
anchors = pack.meta.benchmark_anchor
|
| 115 |
+
joined = " ".join(anchors).lower()
|
| 116 |
+
assert "sc2" in joined
|
| 117 |
+
assert "common-pool" in joined or "commons" in joined
|
| 118 |
+
assert "tam" in joined or "market entry" in joined
|
| 119 |
+
assert "fishing" in joined
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def test_uses_raider_bot():
|
| 123 |
+
"""The pack must declare the Wave-2 `raider` bot — the worker-
|
| 124 |
+
priority idiom is what binds the contestation cost to the harv
|
| 125 |
+
line (not generic combat)."""
|
| 126 |
+
pack = load_pack(PACK)
|
| 127 |
+
enemy = pack.base.get("enemy") if isinstance(pack.base, dict) else None
|
| 128 |
+
assert enemy is not None
|
| 129 |
+
bot = enemy.get("bot_type") or enemy.get("bot")
|
| 130 |
+
assert bot == "raider", f"expected raider bot, got {bot!r}"
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def test_all_tiers_have_reachable_deadlines():
|
| 134 |
+
"""Tick-alignment idiom: within_ticks ≤ ceiling AND after_ticks
|
| 135 |
+
≤ ceiling AND within_ticks + 1 == after_ticks (non-finisher
|
| 136 |
+
LOSES, not draws)."""
|
| 137 |
+
pack = load_pack(PACK)
|
| 138 |
+
for lvl in ("easy", "medium", "hard"):
|
| 139 |
+
L = pack.levels[lvl]
|
| 140 |
+
ceiling = 93 + 90 * (L.max_turns - 1)
|
| 141 |
+
wt = next(
|
| 142 |
+
int(c["within_ticks"])
|
| 143 |
+
for c in L.win_condition.model_dump()["all_of"]
|
| 144 |
+
if "within_ticks" in c
|
| 145 |
+
)
|
| 146 |
+
ft = next(
|
| 147 |
+
int(c["after_ticks"])
|
| 148 |
+
for c in L.fail_condition.model_dump()["any_of"]
|
| 149 |
+
if "after_ticks" in c
|
| 150 |
+
)
|
| 151 |
+
assert wt <= ceiling, f"{lvl}: within_ticks {wt} > ceiling {ceiling}"
|
| 152 |
+
assert ft <= ceiling, f"{lvl}: after_ticks {ft} > ceiling {ceiling}"
|
| 153 |
+
assert wt + 1 == ft, (
|
| 154 |
+
f"{lvl}: within_ticks {wt} / after_ticks {ft} mismatch "
|
| 155 |
+
"(non-finisher must LOSE, not draw — fail clause one tick"
|
| 156 |
+
" past win clause)"
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def test_hard_has_two_seed_driven_spawn_groups():
|
| 161 |
+
"""Hard tier: ≥2 distinct agent spawn_point groups so engine
|
| 162 |
+
round-robins start by seed. The committed-balance task is the same
|
| 163 |
+
per spawn but base orientation flips (NORTH y=14 vs SOUTH y=26),
|
| 164 |
+
so a memorised opening cannot generalise."""
|
| 165 |
+
c = compile_level(load_pack(PACK), "hard")
|
| 166 |
+
sp = {
|
| 167 |
+
(a.spawn_point if a.spawn_point is not None else 0)
|
| 168 |
+
for a in c.scenario.actors
|
| 169 |
+
if a.owner == "agent"
|
| 170 |
+
}
|
| 171 |
+
assert len(sp) >= 2, (
|
| 172 |
+
f"hard must define ≥2 agent spawn_point groups; got {sorted(sp)}"
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def test_fail_condition_present_on_every_tier():
|
| 177 |
+
pack = load_pack(PACK)
|
| 178 |
+
for lvl in ("easy", "medium", "hard"):
|
| 179 |
+
c = compile_level(pack, lvl)
|
| 180 |
+
assert c.fail_condition is not None, f"{lvl} needs a fail_condition"
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
# ── predicate-level (no engine) ─────────────────────────────────────
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def _ctx(*, units=(), tick=1000, cash=0, resources=0):
|
| 187 |
+
import types
|
| 188 |
+
|
| 189 |
+
sig = types.SimpleNamespace(
|
| 190 |
+
game_tick=tick,
|
| 191 |
+
units_killed=0,
|
| 192 |
+
units_lost=0,
|
| 193 |
+
cash=cash,
|
| 194 |
+
resources=resources,
|
| 195 |
+
own_buildings=[],
|
| 196 |
+
own_building_types=set(),
|
| 197 |
+
enemies_seen_ids=set(),
|
| 198 |
+
enemy_buildings_seen_ids=set(),
|
| 199 |
+
)
|
| 200 |
+
return WinContext(
|
| 201 |
+
signals=sig,
|
| 202 |
+
render_state={"units_summary": list(units)},
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def test_predicates_enforce_capability():
|
| 207 |
+
"""Win requires (EV bar AND ≥2 harvs) AND in-time; fail fires on
|
| 208 |
+
timeout OR all-harvs-dead."""
|
| 209 |
+
c = compile_level(load_pack(PACK), "medium")
|
| 210 |
+
two_harvs = [
|
| 211 |
+
{"cell_x": 14, "cell_y": 18, "type": "harv"},
|
| 212 |
+
{"cell_x": 14, "cell_y": 20, "type": "harv"},
|
| 213 |
+
]
|
| 214 |
+
|
| 215 |
+
# Intended: bar met, 2 harvs, in time → WIN
|
| 216 |
+
assert evaluate(
|
| 217 |
+
c.win_condition,
|
| 218 |
+
_ctx(units=two_harvs, tick=2000, cash=3500),
|
| 219 |
+
)
|
| 220 |
+
# Bar one short of 3500 → not a win
|
| 221 |
+
assert not evaluate(
|
| 222 |
+
c.win_condition,
|
| 223 |
+
_ctx(units=two_harvs, tick=2000, cash=3499),
|
| 224 |
+
)
|
| 225 |
+
# Only 1 harv (raider got one) → not a win
|
| 226 |
+
assert not evaluate(
|
| 227 |
+
c.win_condition,
|
| 228 |
+
_ctx(units=two_harvs[:1], tick=2000, cash=10000),
|
| 229 |
+
)
|
| 230 |
+
# All harvs dead → real fail (capability collapses)
|
| 231 |
+
assert evaluate(
|
| 232 |
+
c.fail_condition,
|
| 233 |
+
_ctx(units=[], tick=2000, cash=10000),
|
| 234 |
+
)
|
| 235 |
+
# Timeout (tick past after_ticks): bar unmet → fail
|
| 236 |
+
assert evaluate(
|
| 237 |
+
c.fail_condition,
|
| 238 |
+
_ctx(units=two_harvs, tick=5402, cash=0),
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
# ── engine-driven policies (the no-cheat bar) ───────────────────────
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
|
| 246 |
+
@pytest.mark.parametrize("seed", [1, 2, 3, 4])
|
| 247 |
+
def test_stall_loses_every_tier_and_seed(level, seed):
|
| 248 |
+
"""No harvest order → harvs stay idle → EV stuck at starting_cash
|
| 249 |
+
(500) → bar unmet → timeout LOSS via after_ticks."""
|
| 250 |
+
_, r = _run(level, _stall, seed=seed)
|
| 251 |
+
assert r.outcome == "loss", (
|
| 252 |
+
f"{level}/seed{seed}: stall must LOSE; got {r.outcome} "
|
| 253 |
+
f"ev={_ev(r)} turns={r.turns}"
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
|
| 258 |
+
@pytest.mark.parametrize("seed", [1, 2, 3, 4])
|
| 259 |
+
def test_pure_mine_no_defense_loses(level, seed):
|
| 260 |
+
"""Harvest + abandon the defender ring (move defenders to NW
|
| 261 |
+
corner). The raider's worker-priority attack runs unopposed and
|
| 262 |
+
kills the harvs at the contested patch → fail by harv count."""
|
| 263 |
+
_, r = _run(level, _pure_mine_no_defense, seed=seed)
|
| 264 |
+
assert r.outcome == "loss", (
|
| 265 |
+
f"{level}/seed{seed}: pure-mine-no-defense must LOSE; got "
|
| 266 |
+
f"{r.outcome} ev={_ev(r)} lost={r.signals.units_lost}"
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
|
| 271 |
+
@pytest.mark.parametrize("seed", [1, 2, 3, 4])
|
| 272 |
+
def test_pure_defend_loses(level, seed):
|
| 273 |
+
"""Defenders engage, NO harvest orders → no income → bar unmet
|
| 274 |
+
→ timeout LOSS via after_ticks."""
|
| 275 |
+
_, r = _run(level, _pure_defend, seed=seed)
|
| 276 |
+
assert r.outcome == "loss", (
|
| 277 |
+
f"{level}/seed{seed}: pure-defend must LOSE; got {r.outcome} "
|
| 278 |
+
f"ev={_ev(r)} turns={r.turns}"
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
|
| 283 |
+
@pytest.mark.parametrize("seed", [1, 2, 3, 4])
|
| 284 |
+
def test_intended_balance_wins(level, seed):
|
| 285 |
+
"""The intended capability — harvest + ringed defenders (auto-
|
| 286 |
+
engage) — WINS every tier and every hard seed comfortably inside
|
| 287 |
+
the tick budget."""
|
| 288 |
+
_, r = _run(level, _intended, seed=seed)
|
| 289 |
+
assert r.outcome == "win", (
|
| 290 |
+
f"{level}/seed{seed}: intended-balance should WIN; got "
|
| 291 |
+
f"{r.outcome} ev={_ev(r)} turns={r.turns} "
|
| 292 |
+
f"lost={r.signals.units_lost}"
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
def test_outcomes_are_deterministic_per_seed():
|
| 297 |
+
"""Same seed, same policy → identical outcome and EV."""
|
| 298 |
+
c = compile_level(load_pack(PACK), "medium")
|
| 299 |
+
a = run_level(c, _intended, seed=2)
|
| 300 |
+
b = run_level(c, _intended, seed=2)
|
| 301 |
+
assert (a.outcome, a.turns, _ev(a)) == (b.outcome, b.turns, _ev(b))
|
|
@@ -365,6 +365,19 @@ UPGRADED = [
|
|
| 365 |
# opening generalisation without diluting the selective-action
|
| 366 |
# signal.
|
| 367 |
"proc-instruction-following-edge-case",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
]
|
| 369 |
|
| 370 |
# Consciously NOT spawn-varied, with the reason (keeps the curation
|
|
|
|
| 365 |
# opening generalisation without diluting the selective-action
|
| 366 |
# signal.
|
| 367 |
"proc-instruction-following-edge-case",
|
| 368 |
+
# Wave-5 Group F reasoning seed — common-pool contested mining
|
| 369 |
+
# (SC2 contested expansion / Hardin tragedy of commons / TAM
|
| 370 |
+
# contention). Hard defines two agent spawn_point groups (NORTH
|
| 371 |
+
# base y=14 / SOUTH base y=26) round-robined by seed with a
|
| 372 |
+
# shared harv-lane + defender ring at y=18..22 — base orientation
|
| 373 |
+
# flips per seed (rear-guard 3tnk at y=14 vs y=26 marks the
|
| 374 |
+
# spawn group). The enemy raider strike force + dense contested
|
| 375 |
+
# flavor (3 rival harvs + proc) ALWAYS place (enemy actors
|
| 376 |
+
# don't honour spawn_point — CLAUDE.md), so each seed faces the
|
| 377 |
+
# SAME committed-balance test from a different base orientation
|
| 378 |
+
# and a memorised "always defend at y=N" opening cannot
|
| 379 |
+
# generalise.
|
| 380 |
+
"econ-contention-with-enemy",
|
| 381 |
]
|
| 382 |
|
| 383 |
# Consciously NOT spawn-varied, with the reason (keeps the curation
|