yxc20098 commited on
Commit
4aaa7a1
·
1 Parent(s): db8b75b

feat(scenario): def-retreat-and-rebuild — strategic withdrawal + rebuild (CICERO retreat / replanning anchor)

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