yxc20098 commited on
Commit
1f70ce6
·
1 Parent(s): 908bacf

fix(scenario): build-defensive-skirt-corners — pbox now load-bearing after engine pbox-weapon fix

Browse files

The engine pbox-weapon fix makes a BUILT pbox an active direct-fire
anti-infantry tower. This pack's intended SKIRT policy previously LOST
(the four corner rushes razed the fact before any pbox could be built;
inert pboxes did nothing and the pre-placed e1 ring died instantly).

Recalibrated to the def-walls-vs-towers idiom so the pbox is genuinely
load-bearing:
- the four rush bands now arrive as a `scheduled_events: spawn_actors`
wave at tick 1800 — AFTER the skirt has time to build all 4 pboxes
serially — with one band sitting ON each corner region so a pbox
planted there engages its band immediately;
- removed the pre-placed e1 combat ring (only one far-corner
non-combatant e1 per spawn group remains for units_summary);
- the four corner-region discs moved to the diagonal-corner positions
where the bands spawn (fact +/-(18,11)); the kill bar already
present is now satisfied by pbox kills.

The four corner region clauses still discriminate: a concentrate at one
corner fails three regions and lets three waves through. Intended SKIRT
wins every level + seed; stall / concentrate / pure-army lose.

openra_bench/scenarios/packs/build-defensive-skirt-corners.yaml CHANGED
@@ -14,12 +14,25 @@
14
  #
15
  # The win predicate makes the four-corner topology load-bearing — total
16
  # pbox count alone is NOT enough; ONE pbox must sit inside a radius-4
17
- # disc in EACH of the four corner regions (NE/NW/SE/SW of the fact).
 
18
  # A concentrate-at-one-corner policy meets the count (4 pbox) but
19
  # satisfies only ONE of the four region clauses and fails the other
20
  # three — AND the three uncovered corner waves reach the fact and raze
21
  # it. Concentrate LOSES; the intended four-corner skirt WINS.
22
  #
 
 
 
 
 
 
 
 
 
 
 
 
23
  # Real-world anchors (binding meta.benchmark_anchor):
24
  # • MicroRTS pillbox placement — the canonical RTS sub-benchmark for
25
  # where a finite set of static defences should be planted; the
@@ -35,10 +48,11 @@
35
  # converge on the fact and raze it — the fact-alive fail clause
36
  # fires → LOSS (`after_ticks` is the deterministic backstop, no
37
  # draw degeneracy).
38
- # • concentrate (all 4 pbox massed at ONE corner, e.g. NE): meets the
39
- # count clause and satisfies the NE region clause but FAILS the NW,
40
- # SE, SW region clauses (0 pbox in each) AND the three uncovered
41
- # corner waves walk into the fact and raze it → LOSS.
 
42
  # • pure-army (only e1, never a pbox): FAILS the count clause AND all
43
  # four region clauses; the rifle wall alone cannot hold four
44
  # concurrent rushes → LOSS.
@@ -48,27 +62,33 @@
48
  # fact-alive clauses all satisfied → WIN.
49
  #
50
  # Why the spec works (engine combat sheet, per CLAUDE.md):
51
- # • pbox = pillbox (anti-infantry base defence; cost 600cr; very high
52
- # dps vs rifle infantry; 1×1 footprint). Defence and infantry are
53
- # SEPARATE production queues, so a model may queue build('pbox')
54
- # and build('e1') in parallel.
 
55
  # • `building_count_gte:{pbox,4}` + FOUR `building_in_region` clauses
56
  # (one radius-4 disc per corner) make the four-corner SKIRT load-
57
  # bearing: a concentration satisfies the count and exactly one
58
  # region clause and fails the other three.
 
 
 
59
  # • `building_count_gte:{fact,1}` (PRESENT-TENSE) is the fact-alive
60
  # check — `has_building:fact` is a one-shot ever-seen set that
61
  # stays true after the fact is razed (documented CLAUDE.md footgun).
62
- # • `after_ticks: BUDGET+1` fail clause is reachable within max_turns
63
- # (no interrupts exactly 90 ticks/step max tick = 93+90·(N-1));
64
- # stallers hit it and LOSE rather than draw.
 
 
65
  # • A persistent unarmed enemy `fact` keeps the episode alive past
66
  # full rusher elimination so the win/fail evaluator sees the
67
  # terminal frame (no DRAW collapse on enemy-wipe auto-done).
68
- # • The `rusher` bot charges the agent centroid (the fact at map
69
- # centre) — that is WHY a corner wave whose corner has no pbox
70
- # walks straight into the fact, and why a skirt with a pbox in
71
- # every corner shreds every wave at its own approach.
72
  # • Cash is intentionally tight (exactly 4×600cr = 2400cr). A model
73
  # that wastes cash on extra units cannot complete the pbox count.
74
 
@@ -87,7 +107,8 @@ meta:
87
  the three uncovered corner waves stride untouched into the fact and
88
  raze it. The win predicate makes the topology load-bearing: total
89
  pbox count is not enough — one pbox must sit inside a radius-4 disc
90
- in EVERY corner region, AND the fact must survive.
 
91
  robotics_analogue: >
92
  Distributed defense / quadrant coverage: when one central asset is
93
  threatened from all bearings, finite defensive capacity must be
@@ -129,23 +150,28 @@ base:
129
  levels:
130
  # ── EASY ── bare FOUR-CORNER-SKIRT skill: budget covers exactly 4
131
  # pbox (2400cr). Win requires the count (4 pbox) AND one pbox inside
132
- # the radius-4 disc of EACH corner region (NE (76,10), NW (52,10),
133
- # SE (76,30), SW (52,30) — corners of the fact at (64,20)). A
 
 
134
  # concentrate-at-one-corner layout meets the count but satisfies
135
- # exactly one region clause. Stall loses (clock OR fact razed);
136
- # pure-army loses (count). max_turns 60 reachable tick
137
- # 93+90·59 = 5403; deadline 5400.
 
 
138
  easy:
139
  description: >
140
- Four rusher bands of rifle infantry charge your construction yard
141
- (fact, at map centre (64,20)) CONCURRENTLY from the four diagonal
142
- corners (NE, NW, SE, SW). Build 4 pillboxes (pbox — 600cr each,
143
- budget exactly 2400) AND place ONE of them inside the radius-4
144
- disc of EACH corner region around the fact: NE (76,10), NW
145
- (52,10), SE (76,30), SW (52,30). Massing all four pillboxes on a
146
- single corner holds that corner but lets the other three waves
147
- reach the fact. Stall, pure-army, and concentrate all LOSE. The
148
- fact must survive; ≥4 enemy units must die before tick 5400.
 
149
  starting_cash: 2400
150
  overrides:
151
  actors:
@@ -155,35 +181,37 @@ levels:
155
  - {type: fact, owner: agent, position: [64, 20]}
156
  - {type: tent, owner: agent, position: [60, 16]}
157
  - {type: powr, owner: agent, position: [60, 24]}
158
- # 4 pre-placed e1 defenders ringing the fact (the skirt pboxes
159
- # are the load-bearing wall; these are supporting fire so the
160
- # concentrate / pure-army counterfactuals are still
161
- # discriminated). stance:2 (return fire) so they hold position.
162
- - {type: e1, owner: agent, position: [66, 19], stance: 2}
163
- - {type: e1, owner: agent, position: [66, 21], stance: 2}
164
- - {type: e1, owner: agent, position: [62, 19], stance: 2}
165
- - {type: e1, owner: agent, position: [62, 21], stance: 2}
166
- # Four rusher bands, one per diagonal map corner. Each charges
167
- # the agent centroid (the central fact), so each wave approaches
168
- # along its own diagonal axis. 3× e1 per corner is enough to
169
- # raze the fact through an uncovered corner but a single pbox
170
- # planted in that corner shreds it.
171
- - {type: e1, owner: enemy, position: [114, 6], stance: 3, count: 3}
172
- - {type: e1, owner: enemy, position: [14, 6], stance: 3, count: 3}
173
- - {type: e1, owner: enemy, position: [114, 34], stance: 3, count: 3}
174
- - {type: e1, owner: enemy, position: [14, 34], stance: 3, count: 3}
175
  # Unarmed high-HP marker (anti-DRAW): keeps the episode alive
176
  # past full rusher elimination so the win/fail check fires.
177
  - {type: fact, owner: enemy, position: [64, 4]}
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  win_condition:
179
  all_of:
180
  - {building_count_gte: {type: pbox, n: 4}}
181
- - {building_in_region: {type: pbox, x: 76, y: 10, radius: 4, count: 1}}
182
- - {building_in_region: {type: pbox, x: 52, y: 10, radius: 4, count: 1}}
183
- - {building_in_region: {type: pbox, x: 76, y: 30, radius: 4, count: 1}}
184
- - {building_in_region: {type: pbox, x: 52, y: 30, radius: 4, count: 1}}
185
  - {building_count_gte: {type: fact, n: 1}}
186
- - {units_killed_gte: 4}
187
  - {within_ticks: 5400}
188
  fail_condition:
189
  any_of:
@@ -191,63 +219,53 @@ levels:
191
  - {not: {building_count_gte: {type: fact, n: 1}}}
192
  max_turns: 60
193
 
194
- # ── MEDIUM ── +1 axis: the kill bar is raised from ≥4 to ≥8. The
195
- # rush count is the SAME as easy ( e1 per corner = 12 total) so
196
- # the central pre-placed defenders hold the fact long enough for the
197
- # four-corner skirt to come online but the higher kill bar means
198
- # the skirt must actually ENGAGE and clear most of the rush, not
199
- # merely survive on the clock. A concentrate layout, with three
200
- # corners uncovered, both fails three region clauses AND lets those
201
- # waves walk into the fact. The four-corner region bar is unchanged.
202
- # max_turns 60 ⇒ reachable tick 5403; deadline 5400.
203
  medium:
204
  description: >
205
- Four rusher bands — 3 rifle infantry each (12 total) — charge
206
- your construction yard (fact, at map centre (64,20))
207
  CONCURRENTLY from the four diagonal corners. Build 4 pillboxes
208
  (budget exactly 2400cr = 4 pbox at 600 each) AND place ONE inside
209
- the radius-4 disc of EACH corner region: NE (76,10), NW (52,10),
210
- SE (76,30), SW (52,30). Massing all four pillboxes on a single
211
  corner satisfies only one region clause and lets the three
212
  uncovered waves walk into the fact. Stall, pure-army, and
213
- concentrate all LOSE. The fact must survive; ≥8 enemy units must
214
- die before tick 5400 — the medium kill bar forces the skirt to
215
- actively clear the rush, not merely outlast it.
216
  starting_cash: 2400
217
  overrides:
218
  actors:
219
  - {type: fact, owner: agent, position: [64, 20]}
220
  - {type: tent, owner: agent, position: [60, 16]}
221
  - {type: powr, owner: agent, position: [60, 24]}
222
- # 4 pre-placed e1 defenders ringing the fact supporting fire;
223
- # the skirt pboxes remain the load-bearing wall.
224
- - {type: e1, owner: agent, position: [66, 19], stance: 2}
225
- - {type: e1, owner: agent, position: [66, 21], stance: 2}
226
- - {type: e1, owner: agent, position: [62, 19], stance: 2}
227
- - {type: e1, owner: agent, position: [62, 21], stance: 2}
228
- # Four rusher bands, 3× e1 per corner (12 total) — the same
229
- # count as easy so the central pre-placed defenders hold the
230
- # fact long enough for the four-corner skirt to come online.
231
- # The medium +1 axis is the raised kill bar (≥8 vs easy's ≥4):
232
- # the skirt must actually engage and clear most of the rush,
233
- # not merely survive it. A concentrate layout, with three
234
- # corners uncovered, both fails three region clauses AND lets
235
- # those waves through to the fact.
236
- - {type: e1, owner: enemy, position: [114, 6], stance: 3, count: 3}
237
- - {type: e1, owner: enemy, position: [14, 6], stance: 3, count: 3}
238
- - {type: e1, owner: enemy, position: [114, 34], stance: 3, count: 3}
239
- - {type: e1, owner: enemy, position: [14, 34], stance: 3, count: 3}
240
  # Anti-DRAW marker.
241
  - {type: fact, owner: enemy, position: [64, 4]}
 
 
 
 
 
 
 
 
 
 
242
  win_condition:
243
  all_of:
244
  - {building_count_gte: {type: pbox, n: 4}}
245
- - {building_in_region: {type: pbox, x: 76, y: 10, radius: 4, count: 1}}
246
- - {building_in_region: {type: pbox, x: 52, y: 10, radius: 4, count: 1}}
247
- - {building_in_region: {type: pbox, x: 76, y: 30, radius: 4, count: 1}}
248
- - {building_in_region: {type: pbox, x: 52, y: 30, radius: 4, count: 1}}
249
  - {building_count_gte: {type: fact, n: 1}}
250
- - {units_killed_gte: 8}
251
  - {within_ticks: 5400}
252
  fail_condition:
253
  any_of:
@@ -259,96 +277,94 @@ levels:
259
  # fact (and therefore the four corner regions of the skirt) FLIPS
260
  # between a WEST base (fact at x=50) and an EAST base (fact at x=78)
261
  # per seed — a 14-cell swing each way from the easy/medium centre at
262
- # x=64. The flip is along the EAST-WEST axis (not north-south) so the
263
- # fact stays at y=20 and remains equidistant from all four diagonal
264
- # corner rushes the threat geometry is balanced on both seeds; the
265
- # only thing the seed changes is WHERE the skirt's four corner discs
266
- # sit. The win predicate's `any_of` accepts the four-corner skirt
267
- # around EITHER candidate fact longitude but a single memorised
268
- # "skirt the corners of (64,20)" plan does NOT work: the fact never
269
- # sits at x=64 on hard, so a x=64-centred skirt lands every pbox
270
- # ≥10 cells outside the active corner discs (radius 4) and fails the
271
- # region bar. The model must READ the fact's longitude from
272
- # observation and place the four corner pboxes relative to it. The
273
- # four rusher bands are FIXED at the four map corners and always
274
- # place every seed (enemy actors don't honour spawn_point
275
- # CLAUDE.md / oramap.rs::expand_scenario_actors); the rusher charges
276
- # the agent centroid, so all four bands converge on whichever fact
277
- # is present this seed. Kill bar 8. max_turns 60 ⇒ reachable tick
278
- # 5403; deadline 5400.
279
  hard:
280
  description: >
281
  The agent construction yard (fact) flips between a WEST base
282
  (fact at (50,20)) and an EAST base (fact at (78,20)) by seed; the
283
  four corner regions of the skirt must follow. Build 4 pillboxes
284
  (budget 2400cr = 4 pbox at 600 each) AND place ONE inside the
285
- radius-4 disc of EACH corner of the CURRENT fact (read the fact's
286
- longitude from the observation). Massing all four pillboxes on
287
- one corner, or skirting the corners of the wrong (x=64) centre,
288
- satisfies at most one region clause and lets the uncovered waves
289
- raze the fact. Stall, pure-army, and concentrate all LOSE. The
290
- fact must survive; ≥8 enemy units must die before tick 5400.
 
 
 
291
  starting_cash: 2400
292
  overrides:
293
  actors:
294
  # spawn_point 0 — WEST base. Fact at (50,20); corner regions
295
- # at NE (62,10), NW (38,10), SE (62,30), SW (38,30). SIX
296
- # pre-placed e1 ring the fact (two more than easy/medium) so
297
- # the off-centre base holds the fact long enough for the skirt
298
- # to come online regardless of the order the four corners are
299
- # built in (the win predicate only scores the FINAL skirt, so
300
- # build order must not decide the outcome).
301
  - {type: fact, owner: agent, position: [50, 20], spawn_point: 0}
302
  - {type: tent, owner: agent, position: [46, 16], spawn_point: 0}
303
  - {type: powr, owner: agent, position: [46, 24], spawn_point: 0}
304
- - {type: e1, owner: agent, position: [52, 19], stance: 2, spawn_point: 0}
305
- - {type: e1, owner: agent, position: [52, 21], stance: 2, spawn_point: 0}
306
- - {type: e1, owner: agent, position: [48, 19], stance: 2, spawn_point: 0}
307
- - {type: e1, owner: agent, position: [48, 21], stance: 2, spawn_point: 0}
308
- - {type: e1, owner: agent, position: [50, 18], stance: 2, spawn_point: 0}
309
- - {type: e1, owner: agent, position: [50, 22], stance: 2, spawn_point: 0}
310
  # spawn_point 1 — EAST base. Fact at (78,20); corner regions
311
- # at NE (90,10), NW (66,10), SE (90,30), SW (66,30). SIX
312
- # pre-placed e1 ring the fact (mirror of the WEST base).
313
  - {type: fact, owner: agent, position: [78, 20], spawn_point: 1}
314
  - {type: tent, owner: agent, position: [74, 16], spawn_point: 1}
315
  - {type: powr, owner: agent, position: [74, 24], spawn_point: 1}
316
- - {type: e1, owner: agent, position: [80, 19], stance: 2, spawn_point: 1}
317
- - {type: e1, owner: agent, position: [80, 21], stance: 2, spawn_point: 1}
318
- - {type: e1, owner: agent, position: [76, 19], stance: 2, spawn_point: 1}
319
- - {type: e1, owner: agent, position: [76, 21], stance: 2, spawn_point: 1}
320
- - {type: e1, owner: agent, position: [78, 18], stance: 2, spawn_point: 1}
321
- - {type: e1, owner: agent, position: [78, 22], stance: 2, spawn_point: 1}
322
- # Four rusher bands FIXED at the four map corners (3× e1 each =
323
- # 12 total, same load as easy/medium). Enemies don't honour
324
- # spawn_point so all four always place; the rusher charges the
325
- # agent centroid so every band converges on whichever fact the
326
- # seed selected — the seed flips ONLY the agent base / skirt
327
- # geometry, not the threat count.
328
- - {type: e1, owner: enemy, position: [114, 6], stance: 3, count: 3}
329
- - {type: e1, owner: enemy, position: [14, 6], stance: 3, count: 3}
330
- - {type: e1, owner: enemy, position: [114, 34], stance: 3, count: 3}
331
- - {type: e1, owner: enemy, position: [14, 34], stance: 3, count: 3}
332
  # Anti-DRAW marker on the symmetry axis.
333
  - {type: fact, owner: enemy, position: [64, 4]}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  win_condition:
335
  all_of:
336
  - {building_count_gte: {type: pbox, n: 4}}
337
  - any_of:
338
- # WEST base (fact x=50): corners at x=38 (NW/SW) and x=62 (NE/SE).
339
  - all_of:
340
- - {building_in_region: {type: pbox, x: 62, y: 10, radius: 4, count: 1}}
341
- - {building_in_region: {type: pbox, x: 38, y: 10, radius: 4, count: 1}}
342
- - {building_in_region: {type: pbox, x: 62, y: 30, radius: 4, count: 1}}
343
- - {building_in_region: {type: pbox, x: 38, y: 30, radius: 4, count: 1}}
344
- # EAST base (fact x=78): corners at x=66 (NW/SW) and x=90 (NE/SE).
345
  - all_of:
346
- - {building_in_region: {type: pbox, x: 90, y: 10, radius: 4, count: 1}}
347
- - {building_in_region: {type: pbox, x: 66, y: 10, radius: 4, count: 1}}
348
- - {building_in_region: {type: pbox, x: 90, y: 30, radius: 4, count: 1}}
349
- - {building_in_region: {type: pbox, x: 66, y: 30, radius: 4, count: 1}}
350
  - {building_count_gte: {type: fact, n: 1}}
351
- - {units_killed_gte: 8}
352
  - {within_ticks: 5400}
353
  fail_condition:
354
  any_of:
 
14
  #
15
  # The win predicate makes the four-corner topology load-bearing — total
16
  # pbox count alone is NOT enough; ONE pbox must sit inside a radius-4
17
+ # disc in EACH of the four corner regions (NE/NW/SE/SW of the fact),
18
+ # AND the pillboxes must actually KILL the rush (`units_killed_gte`).
19
  # A concentrate-at-one-corner policy meets the count (4 pbox) but
20
  # satisfies only ONE of the four region clauses and fails the other
21
  # three — AND the three uncovered corner waves reach the fact and raze
22
  # it. Concentrate LOSES; the intended four-corner skirt WINS.
23
  #
24
+ # pbox is the load-bearing weapon. After the engine pbox-weapon fix
25
+ # (`fix(engine): pbox gets a direct-fire Armament`) a BUILT pbox is an
26
+ # active direct-fire anti-infantry tower (M60mg MG: a burst shreds an
27
+ # e1). The four rush bands arrive as a `scheduled_events: spawn_actors`
28
+ # wave at a fixed tick — AFTER the agent has had time to build all 4
29
+ # pillboxes serially. Each band spawns AT one of the four corner
30
+ # regions, so a pbox planted in that region engages its band the moment
31
+ # the wave arrives. There are NO pre-placed agent defenders, so the
32
+ # pbox SKIRT is the sole source of kill output: a concentrate /
33
+ # pure-army layout leaves three corners uncovered — those bands walk to
34
+ # the fact unopposed and raze it.
35
+ #
36
  # Real-world anchors (binding meta.benchmark_anchor):
37
  # • MicroRTS pillbox placement — the canonical RTS sub-benchmark for
38
  # where a finite set of static defences should be planted; the
 
48
  # converge on the fact and raze it — the fact-alive fail clause
49
  # fires → LOSS (`after_ticks` is the deterministic backstop, no
50
  # draw degeneracy).
51
+ # • concentrate (all 4 pbox massed at ONE corner): meets the count
52
+ # clause and satisfies the one region clause for that corner but
53
+ # FAILS the other three region clauses (0 pbox in each) AND the
54
+ # three uncovered corner waves walk into the fact and raze it →
55
+ # LOSS.
56
  # • pure-army (only e1, never a pbox): FAILS the count clause AND all
57
  # four region clauses; the rifle wall alone cannot hold four
58
  # concurrent rushes → LOSS.
 
62
  # fact-alive clauses all satisfied → WIN.
63
  #
64
  # Why the spec works (engine combat sheet, per CLAUDE.md):
65
+ # • pbox = pillbox (anti-infantry base defence; cost 600cr; direct-
66
+ # fire M60mg anti-infantry MG after the engine pbox-weapon fix;
67
+ # 1×1 footprint). Defence and infantry are SEPARATE production
68
+ # queues, so a model may queue build('pbox') and build('e1') in
69
+ # parallel.
70
  # • `building_count_gte:{pbox,4}` + FOUR `building_in_region` clauses
71
  # (one radius-4 disc per corner) make the four-corner SKIRT load-
72
  # bearing: a concentration satisfies the count and exactly one
73
  # region clause and fails the other three.
74
+ # • `units_killed_gte:K` makes the pbox weapon load-bearing: with no
75
+ # pre-placed agent defenders the pbox SKIRT is the sole kill
76
+ # source; a stall / pure-army layout kills 0.
77
  # • `building_count_gte:{fact,1}` (PRESENT-TENSE) is the fact-alive
78
  # check — `has_building:fact` is a one-shot ever-seen set that
79
  # stays true after the fact is razed (documented CLAUDE.md footgun).
80
+ # • The rush arrives as a `scheduled_events: spawn_actors` wave at a
81
+ # fixed tick (1800), AFTER the agent has had time to build 4 pbox
82
+ # serially. This makes the build/rush race fair: the intended skirt
83
+ # comfortably completes before the wave, while a staller /
84
+ # concentrate / pure-army spend still cannot satisfy the predicate.
85
  # • A persistent unarmed enemy `fact` keeps the episode alive past
86
  # full rusher elimination so the win/fail evaluator sees the
87
  # terminal frame (no DRAW collapse on enemy-wipe auto-done).
88
+ # • The `rusher` bot charges the agent centroid (the fact) that is
89
+ # WHY a corner wave whose corner has no pbox walks straight into
90
+ # the fact, and why a skirt with a pbox in every corner shreds
91
+ # every wave at its own approach.
92
  # • Cash is intentionally tight (exactly 4×600cr = 2400cr). A model
93
  # that wastes cash on extra units cannot complete the pbox count.
94
 
 
107
  the three uncovered corner waves stride untouched into the fact and
108
  raze it. The win predicate makes the topology load-bearing: total
109
  pbox count is not enough — one pbox must sit inside a radius-4 disc
110
+ in EVERY corner region, the pillboxes must KILL the rush, AND the
111
+ fact must survive.
112
  robotics_analogue: >
113
  Distributed defense / quadrant coverage: when one central asset is
114
  threatened from all bearings, finite defensive capacity must be
 
150
  levels:
151
  # ── EASY ── bare FOUR-CORNER-SKIRT skill: budget covers exactly 4
152
  # pbox (2400cr). Win requires the count (4 pbox) AND one pbox inside
153
+ # the radius-4 disc of EACH corner region (NE (82,9), NW (46,9),
154
+ # SE (82,31), SW (46,31) — the four diagonal corners around the fact
155
+ # at (64,20)) AND a kill quota (≥9). Each rush band spawns AT one
156
+ # corner region so a pbox planted there engages it immediately. A
157
  # concentrate-at-one-corner layout meets the count but satisfies
158
+ # exactly one region clause AND lets three corner waves through. The
159
+ # rush arrives at tick 1800 after the skirt has had time to
160
+ # assemble. Stall loses (clock OR fact razed); pure-army loses
161
+ # (count). max_turns 60 ⇒ reachable tick 93+90·59 = 5403;
162
+ # deadline 5400.
163
  easy:
164
  description: >
165
+ Four rusher bands of rifle infantry will charge your construction
166
+ yard (fact, at map centre (64,20)) CONCURRENTLY from the four
167
+ diagonal corners. Build 4 pillboxes (pbox — 600cr each, budget
168
+ exactly 2400) AND place ONE of them inside the radius-4 disc of
169
+ EACH corner region: NE (82,9), NW (46,9), SE (82,31), SW (46,31).
170
+ Each band spawns at one corner a pbox planted there shreds it.
171
+ Massing all four pillboxes on a single corner holds that corner
172
+ but lets the other three waves reach the fact. Stall, pure-army,
173
+ and concentrate all LOSE. The fact must survive; ≥9 enemy units
174
+ must die before tick 5400.
175
  starting_cash: 2400
176
  overrides:
177
  actors:
 
181
  - {type: fact, owner: agent, position: [64, 20]}
182
  - {type: tent, owner: agent, position: [60, 16]}
183
  - {type: powr, owner: agent, position: [60, 24]}
184
+ # ONE non-combatant agent e1 parked in the far NW map corner,
185
+ # nowhere near the fact or any corner disc. It exists only so
186
+ # units_summary is non-empty (hard-tier env-reset check); it
187
+ # never reaches combat and contributes ZERO kills — the pbox
188
+ # SKIRT is the sole source of kill output.
189
+ - {type: e1, owner: agent, position: [4, 4], stance: 2}
 
 
 
 
 
 
 
 
 
 
 
190
  # Unarmed high-HP marker (anti-DRAW): keeps the episode alive
191
  # past full rusher elimination so the win/fail check fires.
192
  - {type: fact, owner: enemy, position: [64, 4]}
193
+ # Scheduled rush wave — injected at tick 1800, one band AT each
194
+ # of the four corner regions. The rusher bot charges the agent
195
+ # centroid (the central fact). By tick 1800 all 4 skirt pillboxes
196
+ # are built; a pbox planted in a corner engages that corner's
197
+ # band the moment the wave spawns.
198
+ scheduled_events:
199
+ - tick: 1800
200
+ type: spawn_actors
201
+ actors:
202
+ - {type: e1, owner: enemy, position: [82, 9], stance: 3, count: 3}
203
+ - {type: e1, owner: enemy, position: [46, 9], stance: 3, count: 3}
204
+ - {type: e1, owner: enemy, position: [82, 31], stance: 3, count: 3}
205
+ - {type: e1, owner: enemy, position: [46, 31], stance: 3, count: 3}
206
  win_condition:
207
  all_of:
208
  - {building_count_gte: {type: pbox, n: 4}}
209
+ - {building_in_region: {type: pbox, x: 82, y: 9, radius: 4, count: 1}}
210
+ - {building_in_region: {type: pbox, x: 46, y: 9, radius: 4, count: 1}}
211
+ - {building_in_region: {type: pbox, x: 82, y: 31, radius: 4, count: 1}}
212
+ - {building_in_region: {type: pbox, x: 46, y: 31, radius: 4, count: 1}}
213
  - {building_count_gte: {type: fact, n: 1}}
214
+ - {units_killed_gte: 9}
215
  - {within_ticks: 5400}
216
  fail_condition:
217
  any_of:
 
219
  - {not: {building_count_gte: {type: fact, n: 1}}}
220
  max_turns: 60
221
 
222
+ # ── MEDIUM ── +1 axis: heavier rush (4× e1 per corner = 16 total)
223
+ # and a higher kill bar (≥13). The four-corner region bar is
224
+ # unchanged. A concentrate layout, with three corners uncovered,
225
+ # both fails three region clauses AND lets those heavier waves walk
226
+ # into the fact. max_turns 60 reachable tick 5403; deadline 5400.
 
 
 
 
227
  medium:
228
  description: >
229
+ Four rusher bands — 4 rifle infantry each (16 total) — will
230
+ charge your construction yard (fact, at map centre (64,20))
231
  CONCURRENTLY from the four diagonal corners. Build 4 pillboxes
232
  (budget exactly 2400cr = 4 pbox at 600 each) AND place ONE inside
233
+ the radius-4 disc of EACH corner region: NE (82,9), NW (46,9),
234
+ SE (82,31), SW (46,31). Massing all four pillboxes on a single
235
  corner satisfies only one region clause and lets the three
236
  uncovered waves walk into the fact. Stall, pure-army, and
237
+ concentrate all LOSE. The fact must survive; ≥13 enemy units
238
+ must die before tick 5400.
 
239
  starting_cash: 2400
240
  overrides:
241
  actors:
242
  - {type: fact, owner: agent, position: [64, 20]}
243
  - {type: tent, owner: agent, position: [60, 16]}
244
  - {type: powr, owner: agent, position: [60, 24]}
245
+ # Non-combatant NW-corner e1 (see easy)non-empty
246
+ # units_summary, zero kill contribution.
247
+ - {type: e1, owner: agent, position: [4, 4], stance: 2}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  # Anti-DRAW marker.
249
  - {type: fact, owner: enemy, position: [64, 4]}
250
+ # Heavier rush wave — 4× e1 per corner (16 total), injected at
251
+ # tick 1800 AT each of the four corner regions.
252
+ scheduled_events:
253
+ - tick: 1800
254
+ type: spawn_actors
255
+ actors:
256
+ - {type: e1, owner: enemy, position: [82, 9], stance: 3, count: 4}
257
+ - {type: e1, owner: enemy, position: [46, 9], stance: 3, count: 4}
258
+ - {type: e1, owner: enemy, position: [82, 31], stance: 3, count: 4}
259
+ - {type: e1, owner: enemy, position: [46, 31], stance: 3, count: 4}
260
  win_condition:
261
  all_of:
262
  - {building_count_gte: {type: pbox, n: 4}}
263
+ - {building_in_region: {type: pbox, x: 82, y: 9, radius: 4, count: 1}}
264
+ - {building_in_region: {type: pbox, x: 46, y: 9, radius: 4, count: 1}}
265
+ - {building_in_region: {type: pbox, x: 82, y: 31, radius: 4, count: 1}}
266
+ - {building_in_region: {type: pbox, x: 46, y: 31, radius: 4, count: 1}}
267
  - {building_count_gte: {type: fact, n: 1}}
268
+ - {units_killed_gte: 13}
269
  - {within_ticks: 5400}
270
  fail_condition:
271
  any_of:
 
277
  # fact (and therefore the four corner regions of the skirt) FLIPS
278
  # between a WEST base (fact at x=50) and an EAST base (fact at x=78)
279
  # per seed — a 14-cell swing each way from the easy/medium centre at
280
+ # x=64. The flip is along the EAST-WEST axis so the fact stays at
281
+ # y=20 and remains equidistant from all four diagonal corner rushes.
282
+ # The four corner regions track the fact: WEST fact (50,20) NE
283
+ # (68,9) NW (32,9) SE (68,31) SW (32,31); EAST fact (78,20) NE
284
+ # (96,9) NW (60,9) SE (96,31) SW (60,31). The scheduled wave spawns
285
+ # ALL EIGHT bands (four per candidate fact)the spawn_actors list
286
+ # is not spawn_point-filtered so whichever fact the seed selects,
287
+ # its four corner regions each have a band sitting on them for the
288
+ # skirt to engage, while the four off-fact bands charge across the
289
+ # map toward the active fact. A single memorised "skirt (64,20)"
290
+ # plan does NOT work: the easy/medium discs (x∈{46,82}) land ≥10
291
+ # cells outside every hard disc, failing the region bar. The model
292
+ # must READ the fact's longitude from observation. Kill bar 13.
293
+ # max_turns 60 reachable tick 5403; deadline 5400.
 
 
 
294
  hard:
295
  description: >
296
  The agent construction yard (fact) flips between a WEST base
297
  (fact at (50,20)) and an EAST base (fact at (78,20)) by seed; the
298
  four corner regions of the skirt must follow. Build 4 pillboxes
299
  (budget 2400cr = 4 pbox at 600 each) AND place ONE inside the
300
+ radius-4 disc of EACH corner of the CURRENT fact. For a WEST fact
301
+ (50,20) the corners are NE (68,9) NW (32,9) SE (68,31) SW
302
+ (32,31); for an EAST fact (78,20) they are NE (96,9) NW (60,9)
303
+ SE (96,31) SW (60,31) read the fact's longitude from the
304
+ observation. Massing all four pillboxes on one corner, or
305
+ skirting the wrong (x=64) centre, satisfies at most one region
306
+ clause and lets the uncovered waves raze the fact. Stall,
307
+ pure-army, and concentrate all LOSE. The fact must survive;
308
+ ≥13 enemy units must die before tick 5400.
309
  starting_cash: 2400
310
  overrides:
311
  actors:
312
  # spawn_point 0 — WEST base. Fact at (50,20); corner regions
313
+ # at NE (68,9), NW (32,9), SE (68,31), SW (32,31). A single
314
+ # non-combatant e1 parks in the far SW map corner (zero kill
315
+ # contribution; non-empty units_summary for the env-reset
316
+ # check).
 
 
317
  - {type: fact, owner: agent, position: [50, 20], spawn_point: 0}
318
  - {type: tent, owner: agent, position: [46, 16], spawn_point: 0}
319
  - {type: powr, owner: agent, position: [46, 24], spawn_point: 0}
320
+ - {type: e1, owner: agent, position: [4, 36], stance: 2, spawn_point: 0}
 
 
 
 
 
321
  # spawn_point 1 — EAST base. Fact at (78,20); corner regions
322
+ # at NE (96,9), NW (60,9), SE (96,31), SW (60,31).
 
323
  - {type: fact, owner: agent, position: [78, 20], spawn_point: 1}
324
  - {type: tent, owner: agent, position: [74, 16], spawn_point: 1}
325
  - {type: powr, owner: agent, position: [74, 24], spawn_point: 1}
326
+ - {type: e1, owner: agent, position: [4, 36], stance: 2, spawn_point: 1}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  # Anti-DRAW marker on the symmetry axis.
328
  - {type: fact, owner: enemy, position: [64, 4]}
329
+ # Scheduled rush wave — EIGHT bands (3× e1 each), four sitting on
330
+ # the WEST candidate's corner regions and four on the EAST
331
+ # candidate's, injected at tick 1800. The spawn_actors list is
332
+ # not spawn_point-filtered so all eight always inject; the rusher
333
+ # charges the agent centroid so the four bands on the active
334
+ # fact's corners are engaged by the skirt at their discs while
335
+ # the four off-fact bands charge across to the active fact.
336
+ scheduled_events:
337
+ - tick: 1800
338
+ type: spawn_actors
339
+ actors:
340
+ # WEST candidate corners.
341
+ - {type: e1, owner: enemy, position: [68, 9], stance: 3, count: 3}
342
+ - {type: e1, owner: enemy, position: [32, 9], stance: 3, count: 3}
343
+ - {type: e1, owner: enemy, position: [68, 31], stance: 3, count: 3}
344
+ - {type: e1, owner: enemy, position: [32, 31], stance: 3, count: 3}
345
+ # EAST candidate corners.
346
+ - {type: e1, owner: enemy, position: [96, 9], stance: 3, count: 3}
347
+ - {type: e1, owner: enemy, position: [60, 9], stance: 3, count: 3}
348
+ - {type: e1, owner: enemy, position: [96, 31], stance: 3, count: 3}
349
+ - {type: e1, owner: enemy, position: [60, 31], stance: 3, count: 3}
350
  win_condition:
351
  all_of:
352
  - {building_count_gte: {type: pbox, n: 4}}
353
  - any_of:
354
+ # WEST base (fact x=50): four corner regions.
355
  - all_of:
356
+ - {building_in_region: {type: pbox, x: 68, y: 9, radius: 4, count: 1}}
357
+ - {building_in_region: {type: pbox, x: 32, y: 9, radius: 4, count: 1}}
358
+ - {building_in_region: {type: pbox, x: 68, y: 31, radius: 4, count: 1}}
359
+ - {building_in_region: {type: pbox, x: 32, y: 31, radius: 4, count: 1}}
360
+ # EAST base (fact x=78): four corner regions.
361
  - all_of:
362
+ - {building_in_region: {type: pbox, x: 96, y: 9, radius: 4, count: 1}}
363
+ - {building_in_region: {type: pbox, x: 60, y: 9, radius: 4, count: 1}}
364
+ - {building_in_region: {type: pbox, x: 96, y: 31, radius: 4, count: 1}}
365
+ - {building_in_region: {type: pbox, x: 60, y: 31, radius: 4, count: 1}}
366
  - {building_count_gte: {type: fact, n: 1}}
367
+ - {units_killed_gte: 13}
368
  - {within_ticks: 5400}
369
  fail_condition:
370
  any_of:
tests/test_build_defensive_skirt_corners.py CHANGED
@@ -22,11 +22,18 @@ pbox count alone is not enough:
22
  ever-seen set that stays true after the fact is razed — CLAUDE.md
23
  footgun);
24
  * `units_killed_gte:K` ⇒ the skirt has to actually engage the rush;
 
 
 
25
  * `within_ticks` paired with `after_ticks` ⇒ a non-finisher is a real
26
  reachable timeout LOSS (no interrupts ⇒ each step is exactly 90
27
  ticks, so max_turns is a hard tick budget the `after_ticks` deadline
28
  reliably bites in).
29
 
 
 
 
 
30
  The scripted-policy validations prove deterministically that:
31
 
32
  * the intended adaptive SKIRT policy (one pbox in EACH of the four
@@ -91,10 +98,17 @@ def _fact_cell(rs):
91
  return facts[0].get("cell_x"), facts[0].get("cell_y")
92
 
93
 
 
 
 
 
 
 
 
94
  def make_adaptive_skirt():
95
  """Intended SKIRT topology: read the fact's cell from the observation
96
  on turn 1, then place one pbox in EACH of the four corner regions
97
- around it (offsets +/-12 in x, +/-10 in y). This is the policy the
98
  pack rewards: the skirt must follow the fact, which on hard flips
99
  between x=50 and x=78 by seed."""
100
  state = {"cells": None}
@@ -105,12 +119,7 @@ def make_adaptive_skirt():
105
  if fc is None:
106
  return [C.observe()]
107
  fx, fy = fc
108
- state["cells"] = [
109
- (fx + 12, fy - 10), # NE
110
- (fx - 12, fy - 10), # NW
111
- (fx + 12, fy + 10), # SE
112
- (fx - 12, fy + 10), # SW
113
- ]
114
  return _build_and_place(rs, C, state["cells"])
115
 
116
  return policy
@@ -129,25 +138,23 @@ def make_concentrate():
129
  if fc is None:
130
  return [C.observe()]
131
  fx, fy = fc
132
- cx, cy = fx + 12, fy - 10 # NE corner
133
- state["cells"] = [
134
- (cx - 1, cy - 1), (cx + 1, cy - 1),
135
- (cx - 1, cy + 1), (cx + 1, cy + 1),
136
- ]
137
  return _build_and_place(rs, C, state["cells"])
138
 
139
  return policy
140
 
141
 
142
  def make_wrong_centre_skirt():
143
- """A skirt centred on the OLD (64,20) location — wins easy/medium
144
- (where the fact IS at (64,20)) but FAILS the region clauses on hard
145
- (where the fact is at (50,20) or (78,20) per seed, so a skirt around
146
- (64,20) lands every pbox >=10 cells outside the active corner discs
147
- of radius 4). Demonstrates the spawn-driven discrimination: a
148
- memorised cell list that worked at lower tiers does NOT generalise
149
- to the hard fact-flip."""
150
- cells = [(76, 10), (52, 10), (76, 30), (52, 30)]
151
 
152
  def policy(rs, C):
153
  return _build_and_place(rs, C, cells)
@@ -231,6 +238,56 @@ def test_win_requires_four_corner_region_clauses():
231
  assert len(regions) == 4, layout
232
 
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  @pytest.mark.parametrize("level", LEVELS)
235
  def test_every_level_has_a_reachable_timeout_fail(level):
236
  """Non-win must be a real LOSS: the `after_ticks` fail clause must
 
22
  ever-seen set that stays true after the fact is razed — CLAUDE.md
23
  footgun);
24
  * `units_killed_gte:K` ⇒ the skirt has to actually engage the rush;
25
+ the pbox is the load-bearing weapon (engine pbox-weapon fix), and
26
+ with no pre-placed agent defenders the pbox skirt is the SOLE kill
27
+ source — a stall / pure-army layout kills 0;
28
  * `within_ticks` paired with `after_ticks` ⇒ a non-finisher is a real
29
  reachable timeout LOSS (no interrupts ⇒ each step is exactly 90
30
  ticks, so max_turns is a hard tick budget the `after_ticks` deadline
31
  reliably bites in).
32
 
33
+ The rush arrives as a `scheduled_events: spawn_actors` wave at tick
34
+ 1800 — AFTER the skirt has had time to assemble — with one band sitting
35
+ ON each corner region so a pbox planted there engages it immediately.
36
+
37
  The scripted-policy validations prove deterministically that:
38
 
39
  * the intended adaptive SKIRT policy (one pbox in EACH of the four
 
98
  return facts[0].get("cell_x"), facts[0].get("cell_y")
99
 
100
 
101
+ # Corner-region offsets from the fact (NE, NW, SE, SW). The four
102
+ # corner discs sit at fact + (+/-18, +/-11). The scheduled rush bands
103
+ # spawn ON these discs, so a pbox planted in a disc engages its band
104
+ # the moment the wave arrives.
105
+ SKIRT_OFFSETS = [(18, -11), (-18, -11), (18, 11), (-18, 11)]
106
+
107
+
108
  def make_adaptive_skirt():
109
  """Intended SKIRT topology: read the fact's cell from the observation
110
  on turn 1, then place one pbox in EACH of the four corner regions
111
+ around it (offsets +/-18 in x, +/-11 in y). This is the policy the
112
  pack rewards: the skirt must follow the fact, which on hard flips
113
  between x=50 and x=78 by seed."""
114
  state = {"cells": None}
 
119
  if fc is None:
120
  return [C.observe()]
121
  fx, fy = fc
122
+ state["cells"] = [(fx + dx, fy + dy) for dx, dy in SKIRT_OFFSETS]
 
 
 
 
 
123
  return _build_and_place(rs, C, state["cells"])
124
 
125
  return policy
 
138
  if fc is None:
139
  return [C.observe()]
140
  fx, fy = fc
141
+ cx, cy = fx + 18, fy - 11 # NE corner
142
+ # Four pboxes in a 1-cell-spaced row at the NE corner.
143
+ state["cells"] = [(cx - i, cy) for i in range(4)]
 
 
144
  return _build_and_place(rs, C, state["cells"])
145
 
146
  return policy
147
 
148
 
149
  def make_wrong_centre_skirt():
150
+ """A skirt centred on the OLD (64,20) location — the easy/medium
151
+ fact cell — but applied on HARD where the fact is at (50,20) or
152
+ (78,20) per seed. A skirt around (64,20) lands every pbox >=10 cells
153
+ outside the active corner discs (radius 4), failing the region
154
+ clauses. Demonstrates the spawn-driven discrimination: a memorised
155
+ cell list that worked at lower tiers does NOT generalise to the hard
156
+ fact-flip."""
157
+ cells = [(64 + dx, 20 + dy) for dx, dy in SKIRT_OFFSETS]
158
 
159
  def policy(rs, C):
160
  return _build_and_place(rs, C, cells)
 
238
  assert len(regions) == 4, layout
239
 
240
 
241
+ def test_win_requires_a_kill_quota():
242
+ """The pbox skirt must actively KILL the rush: every level's win
243
+ clause carries a `units_killed_gte` quota. With no pre-placed agent
244
+ defenders the pbox skirt is the sole kill source, so this clause
245
+ makes the pbox weapon load-bearing."""
246
+ pack = load_pack(PACK)
247
+ for lvl in LEVELS:
248
+ c = compile_level(pack, lvl)
249
+ wc = c.win_condition.model_dump(exclude_none=True)
250
+ kill = [
251
+ cl for cl in wc.get("all_of", []) or []
252
+ if isinstance(cl, dict) and "units_killed_gte" in cl
253
+ ]
254
+ assert kill, f"{lvl}: missing units_killed_gte kill quota"
255
+ assert int(kill[0]["units_killed_gte"]) >= 8, (lvl, kill)
256
+
257
+
258
+ def test_rush_arrives_as_a_scheduled_event():
259
+ """The four-corner rush is injected via `scheduled_events:
260
+ spawn_actors` AFTER the skirt has time to assemble — there is no
261
+ t=0 enemy band racing the build."""
262
+ pack = load_pack(PACK)
263
+ for lvl in LEVELS:
264
+ raw = pack.levels[lvl]
265
+ ov = getattr(raw, "overrides", None) or {}
266
+ if hasattr(ov, "model_dump"):
267
+ ov = ov.model_dump(exclude_none=True)
268
+ evts = ov.get("scheduled_events") or []
269
+ assert evts, f"{lvl}: expected a scheduled rush wave"
270
+ assert any(e.get("type") == "spawn_actors" for e in evts), (lvl, evts)
271
+
272
+
273
+ def test_no_pre_placed_agent_combat_screen():
274
+ """The pbox skirt must be the sole kill source — there is no
275
+ pre-placed agent combat screen ringing the fact. Only ONE
276
+ non-combatant agent e1 per active spawn group is parked in a far
277
+ map corner (so units_summary is non-empty for the env-reset check);
278
+ it never fights."""
279
+ for lvl in LEVELS:
280
+ c = compile_level(load_pack(PACK), lvl)
281
+ agent_units = [
282
+ a for a in c.scenario.actors
283
+ if a.owner == "agent" and a.type == "e1"
284
+ ]
285
+ assert len(agent_units) <= 2, (lvl, [a.position for a in agent_units])
286
+ for a in agent_units:
287
+ x, y = a.position
288
+ assert x <= 6 and (y <= 6 or y >= 34), (lvl, a.position)
289
+
290
+
291
  @pytest.mark.parametrize("level", LEVELS)
292
  def test_every_level_has_a_reachable_timeout_fail(level):
293
  """Non-win must be a real LOSS: the `after_ticks` fail clause must