yxc20098 commited on
Commit
e762bb4
·
1 Parent(s): 5fd7e8d

feat(scenario): econ-contention-with-enemy — contested mining (SC2 / common-pool tragedy-of-commons anchor)

Browse files

Wave-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)

openra_bench/scenarios/packs/econ-contention-with-enemy.yaml ADDED
@@ -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
tests/test_econ_contention_with_enemy.py ADDED
@@ -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))
tests/test_hard_tier.py CHANGED
@@ -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