yxc20098 commited on
Commit
f5eebfa
·
1 Parent(s): 1cc2d87

fix(scenario): mid-concede-vs-hold — recalibrate after engine movement fixes

Browse files
openra_bench/scenarios/packs/mid-concede-vs-hold.yaml CHANGED
@@ -14,35 +14,52 @@
14
  # and EAST (78,20), each with a small tank garrison; a 5-unit flex
15
  # squad parks at the centre (51,20). A scripted `hunt` enemy pushes
16
  # BOTH bases simultaneously, asymmetrically — one push is light
17
- # (WEST, 16 rifles), one is heavy (EAST, 34-42 rifles). The HEAVY
18
  # push fully razes the east base even against a wrong-side
19
  # consolidation; the LIGHT push overruns a bare WEST garrison but
20
- # the WEST garrison + FULL flex holds it past the survival floor.
21
- # The win is keeping fact+proc alive at AT LEAST ONE base in the
22
- # [after_ticks, within_ticks] band; the loss is the timeout (5401)
23
- # OR no complete base left (no fact OR no proc — both bases razed).
 
24
  #
25
- # RECALIBRATION (engine combat rebalance): the rebalance hugely
26
- # strengthened stationary defenders, so the original light/heavy
27
- # pushes (4 / 8-12 rifles) no longer threatened the garrisons and
28
- # the bar inverted. Re-tuned: pushes scaled up (WEST 16, EAST
29
- # 36 easy / 42 medium / 34 hard); `enemy_units_killed` termination
30
- # dropped (it auto-`done`d the episode the instant the agent cleared
31
- # a push, often before the survival floor DRAW); a `not proc:1`
32
- # fail clause added; a persistent unarmed enemy `fact` marker added
33
- # (anti auto-DRAW); the hard survival floor moved to tick 2400.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  #
35
- # * Stall ⇒ west garrison alone is overrun by the light push;
36
- # east garrison falls to the heavy push both razed ⇒ LOSS.
 
37
  # * Split-defend (half flex to each side) ⇒ each base gets too few
38
  # reinforcements; both fall ⇒ LOSS.
39
  # * Reinforce-both-back-and-forth ⇒ flex never lands long enough
40
  # anywhere to make the trade; both fall ⇒ LOSS.
41
  # * Consolidate flex on the WRONG (heavy EAST) side ⇒ the heavy
42
- # push razes the east base anyway AND the un-garrisoned west
43
- # base falls ⇒ every refinery lost ⇒ LOSS.
44
- # * Consolidate flex on the WEST (light) side west holds, east
45
- # base falls (conceded by design) ⇒ ONE base saved ⇒ WIN.
 
46
  #
47
  # Map: arena 160x60, cordon 4 (generated on demand via mapgen). The
48
  # rush-hour-arena top-level base_map is only the support-flag sentinel
@@ -137,39 +154,49 @@ levels:
137
  actors:
138
  # ── WEST agent base ──
139
  # fact/proc/powr at stance:2 so they don't trigger any
140
- # spurious movement; the agent's mobile garrison nearby is
141
- # stance:3 to auto-engage anything in range.
 
 
 
 
142
  - {type: fact, owner: agent, position: [24, 20], stance: 2, health: 100}
143
  - {type: proc, owner: agent, position: [24, 23], stance: 2, health: 100}
144
  - {type: powr, owner: agent, position: [22, 20], stance: 2, health: 100}
145
- - {type: 2tnk, owner: agent, position: [27, 19], stance: 3, count: 2, health: 100}
146
- - {type: 1tnk, owner: agent, position: [27, 21], stance: 3, count: 2, health: 100}
147
- - {type: e3, owner: agent, position: [26, 20], stance: 3, count: 2, health: 100}
148
  # ── EAST agent base — symmetric layout ──
149
  - {type: fact, owner: agent, position: [78, 20], stance: 2, health: 100}
150
  - {type: proc, owner: agent, position: [78, 23], stance: 2, health: 100}
151
  - {type: powr, owner: agent, position: [80, 20], stance: 2, health: 100}
152
- - {type: 2tnk, owner: agent, position: [75, 19], stance: 3, count: 2, health: 100}
153
- - {type: 1tnk, owner: agent, position: [75, 21], stance: 3, count: 2, health: 100}
154
- - {type: e3, owner: agent, position: [76, 20], stance: 3, count: 2, health: 100}
155
  # ── Flex squad — parked centre, must be committed to ONE side
156
- - {type: 2tnk, owner: agent, position: [51, 20], stance: 3, count: 2, health: 100}
157
- - {type: 1tnk, owner: agent, position: [51, 22], stance: 3, count: 2, health: 100}
158
- - {type: jeep, owner: agent, position: [51, 18], stance: 3, health: 100}
159
- # ── WEST enemy push (LIGHT) — far west edge. Sized (16 e1)
160
- # so the WEST garrison ALONE is overrun (a staller loses the
161
- # west base) but the WEST garrison + consolidated flex holds
162
- # it past the survival floor. Re-tuned UP from 4 after the
163
- # engine combat rebalance hugely strengthened stationary
164
- # defenders a 4-rifle push no longer threatens even a bare
165
- # garrison.
166
- - {type: e1, owner: enemy, position: [5, 20], stance: 3, count: 16, health: 100}
167
- # ── EAST enemy push (HEAVY) — far east edge. Sized (36 e1)
168
  # so it FULLY razes the east base (fact + proc) even against
169
- # a wrong-side consolidation: the heavy side is genuinely
170
- # unsavable. Re-tuned UP from 8 for the same combat-rebalance
171
- # reason 8 rifles were beatable by the consolidated force.
 
 
 
 
 
 
 
172
  - {type: e1, owner: enemy, position: [155, 21], stance: 3, count: 36, health: 100}
 
173
  # Persistent unarmed enemy `fact` marker far off-map (anti
174
  # auto-DRAW): keeps the episode alive past the survival floor
175
  # so the building-survival win/fail evaluates instead of the
@@ -234,27 +261,30 @@ levels:
234
  - {type: fact, owner: agent, position: [24, 20], stance: 2, health: 100}
235
  - {type: proc, owner: agent, position: [24, 23], stance: 2, health: 100}
236
  - {type: powr, owner: agent, position: [22, 20], stance: 2, health: 100}
237
- - {type: 2tnk, owner: agent, position: [27, 19], stance: 3, count: 2, health: 100}
238
- - {type: 1tnk, owner: agent, position: [27, 21], stance: 3, count: 2, health: 100}
239
- - {type: e3, owner: agent, position: [26, 20], stance: 3, count: 2, health: 100}
240
  # ── EAST agent base ──
241
  - {type: fact, owner: agent, position: [78, 20], stance: 2, health: 100}
242
  - {type: proc, owner: agent, position: [78, 23], stance: 2, health: 100}
243
  - {type: powr, owner: agent, position: [80, 20], stance: 2, health: 100}
244
- - {type: 2tnk, owner: agent, position: [75, 19], stance: 3, count: 2, health: 100}
245
- - {type: 1tnk, owner: agent, position: [75, 21], stance: 3, count: 2, health: 100}
246
- - {type: e3, owner: agent, position: [76, 20], stance: 3, count: 2, health: 100}
247
  # ── Flex squad ──
248
- - {type: 2tnk, owner: agent, position: [51, 20], stance: 3, count: 2, health: 100}
249
- - {type: 1tnk, owner: agent, position: [51, 22], stance: 3, count: 2, health: 100}
250
- - {type: jeep, owner: agent, position: [51, 18], stance: 3, health: 100}
251
- # ── WEST enemy push (LIGHT) — same 16-rifle band as easy:
252
- # overruns a bare garrison, held by the consolidated force.
253
- - {type: e1, owner: enemy, position: [5, 20], stance: 3, count: 16, health: 100}
254
- # ── EAST enemy push (HEAVY) — heavier than easy (42 vs 36):
255
- # the heavy-side trade is even more clearly unsavable, so a
256
- # mis-commit is punished harder.
257
- - {type: e1, owner: enemy, position: [155, 21], stance: 3, count: 42, health: 100}
 
 
 
258
  # Persistent unarmed enemy `fact` marker (anti auto-DRAW).
259
  - {type: fact, owner: enemy, position: [150, 5]}
260
  # Same survival band as easy; max_turns 60 → tick reach 5403 ≥ 5401.
@@ -333,36 +363,36 @@ levels:
333
  - {type: fact, owner: agent, position: [24, 20], stance: 2, health: 100, spawn_point: 0}
334
  - {type: proc, owner: agent, position: [24, 23], stance: 2, health: 100, spawn_point: 0}
335
  - {type: powr, owner: agent, position: [22, 20], stance: 2, health: 100, spawn_point: 0}
336
- - {type: 2tnk, owner: agent, position: [27, 19], stance: 3, count: 2, health: 100, spawn_point: 0}
337
- - {type: 1tnk, owner: agent, position: [27, 21], stance: 3, count: 2, health: 100, spawn_point: 0}
338
- - {type: e3, owner: agent, position: [26, 20], stance: 3, count: 2, health: 100, spawn_point: 0}
339
  # ── WEST agent base — spawn_point 1 copy ──
340
  - {type: fact, owner: agent, position: [24, 20], stance: 2, health: 100, spawn_point: 1}
341
  - {type: proc, owner: agent, position: [24, 23], stance: 2, health: 100, spawn_point: 1}
342
  - {type: powr, owner: agent, position: [22, 20], stance: 2, health: 100, spawn_point: 1}
343
- - {type: 2tnk, owner: agent, position: [27, 19], stance: 3, count: 2, health: 100, spawn_point: 1}
344
- - {type: 1tnk, owner: agent, position: [27, 21], stance: 3, count: 2, health: 100, spawn_point: 1}
345
- - {type: e3, owner: agent, position: [26, 20], stance: 3, count: 2, health: 100, spawn_point: 1}
346
  # ── EAST agent base — spawn_point 0 copy ──
347
  - {type: fact, owner: agent, position: [78, 20], stance: 2, health: 100, spawn_point: 0}
348
  - {type: proc, owner: agent, position: [78, 23], stance: 2, health: 100, spawn_point: 0}
349
  - {type: powr, owner: agent, position: [80, 20], stance: 2, health: 100, spawn_point: 0}
350
- - {type: 2tnk, owner: agent, position: [75, 19], stance: 3, count: 2, health: 100, spawn_point: 0}
351
- - {type: 1tnk, owner: agent, position: [75, 21], stance: 3, count: 2, health: 100, spawn_point: 0}
352
- - {type: e3, owner: agent, position: [76, 20], stance: 3, count: 2, health: 100, spawn_point: 0}
353
  # ── EAST agent base — spawn_point 1 copy ──
354
  - {type: fact, owner: agent, position: [78, 20], stance: 2, health: 100, spawn_point: 1}
355
  - {type: proc, owner: agent, position: [78, 23], stance: 2, health: 100, spawn_point: 1}
356
  - {type: powr, owner: agent, position: [80, 20], stance: 2, health: 100, spawn_point: 1}
357
- - {type: 2tnk, owner: agent, position: [75, 19], stance: 3, count: 2, health: 100, spawn_point: 1}
358
- - {type: 1tnk, owner: agent, position: [75, 21], stance: 3, count: 2, health: 100, spawn_point: 1}
359
- - {type: e3, owner: agent, position: [76, 20], stance: 3, count: 2, health: 100, spawn_point: 1}
360
  # ── Flex squad, spawn_point 0 — pre-positioned WEST of centre
361
  # (closer to the weak-side base). The model still has to scout
362
  # and decide; the start nudges the right policy.
363
- - {type: 2tnk, owner: agent, position: [48, 20], stance: 3, count: 2, health: 100, spawn_point: 0}
364
- - {type: 1tnk, owner: agent, position: [48, 22], stance: 3, count: 2, health: 100, spawn_point: 0}
365
- - {type: jeep, owner: agent, position: [48, 18], stance: 3, health: 100, spawn_point: 0}
366
  # ── Flex squad, spawn_point 1 — pre-positioned EAST of centre
367
  # (x=54), on the far side of the centre line from the weak
368
  # WEST base: a "wrong-side" start that punishes a memorised
@@ -374,23 +404,27 @@ levels:
374
  # combat rebalance made those survivors numerous enough that
375
  # a too-distant flex arrived after the WEST base was already
376
  # being overrun.
377
- - {type: 2tnk, owner: agent, position: [54, 20], stance: 3, count: 2, health: 100, spawn_point: 1}
378
- - {type: 1tnk, owner: agent, position: [54, 22], stance: 3, count: 2, health: 100, spawn_point: 1}
379
- - {type: jeep, owner: agent, position: [54, 18], stance: 3, health: 100, spawn_point: 1}
380
- # ── WEST enemy push (LIGHT) — far west, same 16-rifle band
381
  # as easy/medium.
382
- - {type: e1, owner: enemy, position: [5, 20], stance: 3, count: 16, health: 100}
383
- # ── EAST enemy push (HEAVY) — far east, 34-rifle band.
384
- # Slightly lighter than medium's 42: hard's seed-varied flex
385
- # start (spawn_point 1 starts the flex EAST of centre) gives
386
- # the WEST consolidation less travel time, and after the
387
- # conceded EAST base falls the heavy push's survivors march
388
- # on the held WEST base at 42 those survivors arrived in
389
- # such force they overran the WEST base before the survival
390
- # floor on the slower spawn group. 34 still fully razes the
391
- # conceded EAST base (a wrong-side consolidate loses every
392
- # refinery) but leaves the held WEST base winnable.
393
- - {type: e1, owner: enemy, position: [155, 21], stance: 3, count: 34, health: 100}
 
 
 
 
394
  # Persistent unarmed enemy `fact` marker (anti auto-DRAW).
395
  # Enemy actors do not honour spawn_point, so a single marker
396
  # (no spawn_point) places under either seed.
 
14
  # and EAST (78,20), each with a small tank garrison; a 5-unit flex
15
  # squad parks at the centre (51,20). A scripted `hunt` enemy pushes
16
  # BOTH bases simultaneously, asymmetrically — one push is light
17
+ # (WEST, 20 rifles), one is heavy (EAST, 72 rifles). The HEAVY
18
  # push fully razes the east base even against a wrong-side
19
  # consolidation; the LIGHT push overruns a bare WEST garrison but
20
+ # the WEST garrison + FULL flex, ordered to attack-move into the
21
+ # push, holds it past the survival floor. The win is keeping
22
+ # fact+proc alive at AT LEAST ONE base in the [after_ticks,
23
+ # within_ticks] band; the loss is the timeout (5401) OR no complete
24
+ # base left (no fact OR no proc — both bases razed).
25
  #
26
+ # RECALIBRATION (engine MOVEMENT fixes): two engine fixes shifted
27
+ # combat bench-wide (A) `attack_unit` on an out-of-sight target
28
+ # now paths normally instead of teleporting; (B) a unit executing a
29
+ # move both FIRES and TAKES FIRE en route (no sprint-invincibility),
30
+ # respecting stance. The net effect here: a fully-consolidated force
31
+ # that ATTACK-MOVES into a push is far more effective than before,
32
+ # and critically agent units left at `stance:3` AttackAnything
33
+ # now auto-advance to intercept any visible enemy, so a STALL policy
34
+ # self-played the scenario (the flex + garrisons hunted the pushes
35
+ # unaided) and won for free. Re-tuned:
36
+ # * every agent mobile unit set to `stance:1` ReturnFire — it
37
+ # fires back when shot but never advances/initiates on its own,
38
+ # so the capability (explicitly committing the flex) is
39
+ # load-bearing again; the intended policy must ATTACK-MOVE.
40
+ # * WEST light push 16→20, EAST heavy push 36/42/34→72 (uniform).
41
+ # The buffed consolidated attack-move force could hold the EAST
42
+ # base against the old 34-42 e1 heavy push (wrong-side
43
+ # consolidate WON); 72 e1 razes the conceded base before the
44
+ # force clears it, on every level and seed.
45
+ # (Earlier recalibration, still in effect: `enemy_units_killed`
46
+ # termination dropped — the win is a survival-band check; a
47
+ # `not proc:1` fail clause; a persistent unarmed enemy `fact`
48
+ # marker (anti auto-DRAW); the hard survival floor at tick 2400.)
49
  #
50
+ # * Stall ⇒ stance:1 units only return fire, never advance: the
51
+ # bare west garrison is overrun by the light push, the east
52
+ # garrison falls to the heavy push ⇒ both razed ⇒ LOSS.
53
  # * Split-defend (half flex to each side) ⇒ each base gets too few
54
  # reinforcements; both fall ⇒ LOSS.
55
  # * Reinforce-both-back-and-forth ⇒ flex never lands long enough
56
  # anywhere to make the trade; both fall ⇒ LOSS.
57
  # * Consolidate flex on the WRONG (heavy EAST) side ⇒ the heavy
58
+ # 72-e1 push razes the east base anyway AND the un-garrisoned
59
+ # west base falls ⇒ every refinery lost ⇒ LOSS.
60
+ # * Consolidate flex on the WEST (light) side and ATTACK-MOVE into
61
+ # the push ⇒ west holds, east base falls (conceded by design)
62
+ # ⇒ ONE base saved ⇒ WIN.
63
  #
64
  # Map: arena 160x60, cordon 4 (generated on demand via mapgen). The
65
  # rush-hour-arena top-level base_map is only the support-flag sentinel
 
154
  actors:
155
  # ── WEST agent base ──
156
  # fact/proc/powr at stance:2 so they don't trigger any
157
+ # spurious movement; the agent's mobile garrison and the
158
+ # flex squad are stance:1 ReturnFire they fire back when
159
+ # shot but never advance/initiate, so the agent must
160
+ # explicitly ATTACK-MOVE the consolidated force into the
161
+ # push for the concede-vs-hold capability to be load-bearing
162
+ # (a stall policy cannot self-play the defence).
163
  - {type: fact, owner: agent, position: [24, 20], stance: 2, health: 100}
164
  - {type: proc, owner: agent, position: [24, 23], stance: 2, health: 100}
165
  - {type: powr, owner: agent, position: [22, 20], stance: 2, health: 100}
166
+ - {type: 2tnk, owner: agent, position: [27, 19], stance: 1, count: 2, health: 100}
167
+ - {type: 1tnk, owner: agent, position: [27, 21], stance: 1, count: 2, health: 100}
168
+ - {type: e3, owner: agent, position: [26, 20], stance: 1, count: 2, health: 100}
169
  # ── EAST agent base — symmetric layout ──
170
  - {type: fact, owner: agent, position: [78, 20], stance: 2, health: 100}
171
  - {type: proc, owner: agent, position: [78, 23], stance: 2, health: 100}
172
  - {type: powr, owner: agent, position: [80, 20], stance: 2, health: 100}
173
+ - {type: 2tnk, owner: agent, position: [75, 19], stance: 1, count: 2, health: 100}
174
+ - {type: 1tnk, owner: agent, position: [75, 21], stance: 1, count: 2, health: 100}
175
+ - {type: e3, owner: agent, position: [76, 20], stance: 1, count: 2, health: 100}
176
  # ── Flex squad — parked centre, must be committed to ONE side
177
+ - {type: 2tnk, owner: agent, position: [51, 20], stance: 1, count: 2, health: 100}
178
+ - {type: 1tnk, owner: agent, position: [51, 22], stance: 1, count: 2, health: 100}
179
+ - {type: jeep, owner: agent, position: [51, 18], stance: 1, health: 100}
180
+ # ── WEST enemy push (LIGHT) — far west edge. Sized (20 e1)
181
+ # so the bare WEST garrison (return-fire only see the
182
+ # stance note) is overrun, but the WEST garrison + the
183
+ # consolidated flex, ordered to ATTACK-MOVE into the push,
184
+ # clears it and holds the base past the survival floor.
185
+ - {type: e1, owner: enemy, position: [5, 20], stance: 3, count: 20, health: 100}
186
+ # ── EAST enemy push (HEAVY) — far east edge. Sized (72 e1)
 
 
187
  # so it FULLY razes the east base (fact + proc) even against
188
+ # a wrong-side full consolidation: the heavy side is genuinely
189
+ # unsavable. Re-tuned UP after the engine MOVEMENT fixes
190
+ # (attack_unit no longer teleports; a moving unit both fires
191
+ # and takes fire en route) made a consolidated stance:1
192
+ # attack-move force strong enough to hold the EAST base
193
+ # against the old 34-42 e1 push — at that size a wrong-side
194
+ # consolidate WON, collapsing the concede-vs-hold bar. 72 e1
195
+ # razes the EAST base before the force can clear it.
196
+ # EAST push split into two stacked actor entries (36+36=72)
197
+ # because the scenario schema caps a single actor count at 50.
198
  - {type: e1, owner: enemy, position: [155, 21], stance: 3, count: 36, health: 100}
199
+ - {type: e1, owner: enemy, position: [155, 24], stance: 3, count: 36, health: 100}
200
  # Persistent unarmed enemy `fact` marker far off-map (anti
201
  # auto-DRAW): keeps the episode alive past the survival floor
202
  # so the building-survival win/fail evaluates instead of the
 
261
  - {type: fact, owner: agent, position: [24, 20], stance: 2, health: 100}
262
  - {type: proc, owner: agent, position: [24, 23], stance: 2, health: 100}
263
  - {type: powr, owner: agent, position: [22, 20], stance: 2, health: 100}
264
+ - {type: 2tnk, owner: agent, position: [27, 19], stance: 1, count: 2, health: 100}
265
+ - {type: 1tnk, owner: agent, position: [27, 21], stance: 1, count: 2, health: 100}
266
+ - {type: e3, owner: agent, position: [26, 20], stance: 1, count: 2, health: 100}
267
  # ── EAST agent base ──
268
  - {type: fact, owner: agent, position: [78, 20], stance: 2, health: 100}
269
  - {type: proc, owner: agent, position: [78, 23], stance: 2, health: 100}
270
  - {type: powr, owner: agent, position: [80, 20], stance: 2, health: 100}
271
+ - {type: 2tnk, owner: agent, position: [75, 19], stance: 1, count: 2, health: 100}
272
+ - {type: 1tnk, owner: agent, position: [75, 21], stance: 1, count: 2, health: 100}
273
+ - {type: e3, owner: agent, position: [76, 20], stance: 1, count: 2, health: 100}
274
  # ── Flex squad ──
275
+ - {type: 2tnk, owner: agent, position: [51, 20], stance: 1, count: 2, health: 100}
276
+ - {type: 1tnk, owner: agent, position: [51, 22], stance: 1, count: 2, health: 100}
277
+ - {type: jeep, owner: agent, position: [51, 18], stance: 1, health: 100}
278
+ # ── WEST enemy push (LIGHT) — same 20-rifle band as easy:
279
+ # overruns the bare garrison, held by the consolidated force.
280
+ - {type: e1, owner: enemy, position: [5, 20], stance: 3, count: 20, health: 100}
281
+ # ── EAST enemy push (HEAVY) — same 72-rifle unsavable band
282
+ # as easy: a wrong-side full consolidation still loses the
283
+ # EAST base (and the un-garrisoned WEST base falls too).
284
+ # EAST push split into two stacked actor entries (36+36=72)
285
+ # because the scenario schema caps a single actor count at 50.
286
+ - {type: e1, owner: enemy, position: [155, 21], stance: 3, count: 36, health: 100}
287
+ - {type: e1, owner: enemy, position: [155, 24], stance: 3, count: 36, health: 100}
288
  # Persistent unarmed enemy `fact` marker (anti auto-DRAW).
289
  - {type: fact, owner: enemy, position: [150, 5]}
290
  # Same survival band as easy; max_turns 60 → tick reach 5403 ≥ 5401.
 
363
  - {type: fact, owner: agent, position: [24, 20], stance: 2, health: 100, spawn_point: 0}
364
  - {type: proc, owner: agent, position: [24, 23], stance: 2, health: 100, spawn_point: 0}
365
  - {type: powr, owner: agent, position: [22, 20], stance: 2, health: 100, spawn_point: 0}
366
+ - {type: 2tnk, owner: agent, position: [27, 19], stance: 1, count: 2, health: 100, spawn_point: 0}
367
+ - {type: 1tnk, owner: agent, position: [27, 21], stance: 1, count: 2, health: 100, spawn_point: 0}
368
+ - {type: e3, owner: agent, position: [26, 20], stance: 1, count: 2, health: 100, spawn_point: 0}
369
  # ── WEST agent base — spawn_point 1 copy ──
370
  - {type: fact, owner: agent, position: [24, 20], stance: 2, health: 100, spawn_point: 1}
371
  - {type: proc, owner: agent, position: [24, 23], stance: 2, health: 100, spawn_point: 1}
372
  - {type: powr, owner: agent, position: [22, 20], stance: 2, health: 100, spawn_point: 1}
373
+ - {type: 2tnk, owner: agent, position: [27, 19], stance: 1, count: 2, health: 100, spawn_point: 1}
374
+ - {type: 1tnk, owner: agent, position: [27, 21], stance: 1, count: 2, health: 100, spawn_point: 1}
375
+ - {type: e3, owner: agent, position: [26, 20], stance: 1, count: 2, health: 100, spawn_point: 1}
376
  # ── EAST agent base — spawn_point 0 copy ──
377
  - {type: fact, owner: agent, position: [78, 20], stance: 2, health: 100, spawn_point: 0}
378
  - {type: proc, owner: agent, position: [78, 23], stance: 2, health: 100, spawn_point: 0}
379
  - {type: powr, owner: agent, position: [80, 20], stance: 2, health: 100, spawn_point: 0}
380
+ - {type: 2tnk, owner: agent, position: [75, 19], stance: 1, count: 2, health: 100, spawn_point: 0}
381
+ - {type: 1tnk, owner: agent, position: [75, 21], stance: 1, count: 2, health: 100, spawn_point: 0}
382
+ - {type: e3, owner: agent, position: [76, 20], stance: 1, count: 2, health: 100, spawn_point: 0}
383
  # ── EAST agent base — spawn_point 1 copy ──
384
  - {type: fact, owner: agent, position: [78, 20], stance: 2, health: 100, spawn_point: 1}
385
  - {type: proc, owner: agent, position: [78, 23], stance: 2, health: 100, spawn_point: 1}
386
  - {type: powr, owner: agent, position: [80, 20], stance: 2, health: 100, spawn_point: 1}
387
+ - {type: 2tnk, owner: agent, position: [75, 19], stance: 1, count: 2, health: 100, spawn_point: 1}
388
+ - {type: 1tnk, owner: agent, position: [75, 21], stance: 1, count: 2, health: 100, spawn_point: 1}
389
+ - {type: e3, owner: agent, position: [76, 20], stance: 1, count: 2, health: 100, spawn_point: 1}
390
  # ── Flex squad, spawn_point 0 — pre-positioned WEST of centre
391
  # (closer to the weak-side base). The model still has to scout
392
  # and decide; the start nudges the right policy.
393
+ - {type: 2tnk, owner: agent, position: [48, 20], stance: 1, count: 2, health: 100, spawn_point: 0}
394
+ - {type: 1tnk, owner: agent, position: [48, 22], stance: 1, count: 2, health: 100, spawn_point: 0}
395
+ - {type: jeep, owner: agent, position: [48, 18], stance: 1, health: 100, spawn_point: 0}
396
  # ── Flex squad, spawn_point 1 — pre-positioned EAST of centre
397
  # (x=54), on the far side of the centre line from the weak
398
  # WEST base: a "wrong-side" start that punishes a memorised
 
404
  # combat rebalance made those survivors numerous enough that
405
  # a too-distant flex arrived after the WEST base was already
406
  # being overrun.
407
+ - {type: 2tnk, owner: agent, position: [54, 20], stance: 1, count: 2, health: 100, spawn_point: 1}
408
+ - {type: 1tnk, owner: agent, position: [54, 22], stance: 1, count: 2, health: 100, spawn_point: 1}
409
+ - {type: jeep, owner: agent, position: [54, 18], stance: 1, health: 100, spawn_point: 1}
410
+ # ── WEST enemy push (LIGHT) — far west, same 20-rifle band
411
  # as easy/medium.
412
+ - {type: e1, owner: enemy, position: [5, 20], stance: 3, count: 20, health: 100}
413
+ # ── EAST enemy push (HEAVY) — far east, same 72-rifle band
414
+ # as easy/medium. Hard's earlier survival floor (2400 vs
415
+ # easy/medium's 3000) is what makes the heavy push need to be
416
+ # this large: a wrong-side full consolidation can hold the
417
+ # EAST base intact past tick 2400 against a lighter push
418
+ # (a 34-58 e1 EAST push was still standing at tick 2400 → a
419
+ # wrong-side consolidate snapshotted a WIN), so the EAST push
420
+ # must be heavy enough to RAZE the EAST base before the
421
+ # 2400 floor. 72 e1 does `cons_east` loses every refinery
422
+ # on every seed — while the LIGHT west push stays winnable
423
+ # by the consolidated attack-move force.
424
+ # EAST push split into two stacked actor entries (36+36=72)
425
+ # because the scenario schema caps a single actor count at 50.
426
+ - {type: e1, owner: enemy, position: [155, 21], stance: 3, count: 36, health: 100}
427
+ - {type: e1, owner: enemy, position: [155, 24], stance: 3, count: 36, health: 100}
428
  # Persistent unarmed enemy `fact` marker (anti auto-DRAW).
429
  # Enemy actors do not honour spawn_point, so a single marker
430
  # (no spawn_point) places under either seed.
tests/test_mid_concede_vs_hold.py CHANGED
@@ -8,20 +8,34 @@ properties (spawn_point contract for hard, fail_condition shape,
8
  benchmark anchors), and that the win/fail predicate tree is in the
9
  right band (after_ticks ≤ within_ticks ≤ reachable-tick).
10
 
11
- Recalibration note: the engine combat rebalance hugely strengthened
12
- stationary defenders, breaking the original bar the light/heavy
13
- pushes (4 / 8-12 rifles) no longer threatened the buffed garrisons,
14
- and an `enemy_units_killed` auto-`done` ended the episode (DRAW)
15
- the instant the agent cleared a push, before the survival floor.
16
- The pack was re-tuned: pushes scaled up (WEST 16 / EAST 36-42 /
17
- hard EAST 34), `enemy_units_killed` termination dropped (the win is
18
- a survival-band check), a `not proc:1` fail clause added (a
19
- wrong-side consolidate loses every refinery without it that play
20
- kept a lone fact and silently DREW), a persistent unarmed enemy
21
- `fact` marker added (anti auto-DRAW), the hard survival floor moved
22
- to tick 2400 and its attrition cap to 18. The capability stays
23
- load-bearing — stall / split / oscillate / wrong-side consolidate
24
- all LOSE; only consolidate-on-the-light-side WINS.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  """
26
 
27
  from __future__ import annotations
@@ -135,38 +149,45 @@ def _stall(rs, C):
135
 
136
 
137
  def _cons_west(rs, C):
138
- """Intended policy: commit every mobile unit to the WEST base
139
- (the light-push side). East garrison moves west too."""
 
 
 
 
 
140
  u = rs.get("units_summary", []) or []
141
  cmds = [
142
- C.move_units([str(x["id"])], target_x=26, target_y=20)
143
  for x in u
144
- if x["cell_x"] > 35
145
  ]
146
  return cmds or [C.observe()]
147
 
148
 
149
  def _cons_east(rs, C):
150
- """Wrong-side commit: everything to east (heavy-push side)."""
 
 
 
151
  u = rs.get("units_summary", []) or []
152
  cmds = [
153
- C.move_units([str(x["id"])], target_x=76, target_y=20)
154
  for x in u
155
- if x["cell_x"] < 65
156
  ]
157
  return cmds or [C.observe()]
158
 
159
 
160
  def _split_defend(rs, C):
161
- """Split-defend: half the flex squad to each base, garrisons stay."""
 
162
  u = rs.get("units_summary", []) or []
163
  flex = sorted(
164
  [x for x in u if 38 <= x["cell_x"] <= 65], key=lambda x: x["id"]
165
  )
166
  cmds = []
167
  for i, x in enumerate(flex):
168
- tgt = (26, 20) if i % 2 == 0 else (76, 20)
169
- cmds.append(C.move_units([str(x["id"])], target_x=tgt[0], target_y=tgt[1]))
170
  return cmds or [C.observe()]
171
 
172
 
@@ -176,24 +197,19 @@ class _PanicTC:
176
 
177
 
178
  def _panic_reinforce_factory():
179
- """Reinforce-both-back-and-forth: flip the flex target every turn
180
- (no commitment, no rest). Pure wasted travel."""
 
 
181
  tc = _PanicTC()
182
 
183
  def f(rs, C):
184
  tc.n += 1
185
  u = rs.get("units_summary", []) or []
186
- flex = [
187
- x
188
- for x in u
189
- if 38 <= x["cell_x"] <= 65
190
- or abs(x["cell_x"] - 26) < 5
191
- or abs(x["cell_x"] - 76) < 5
192
- ]
193
- tgt = (26, 20) if tc.n % 2 == 0 else (76, 20)
194
  return [
195
- C.move_units([str(x["id"])], target_x=tgt[0], target_y=tgt[1])
196
- for x in flex
197
  ] or [C.observe()]
198
 
199
  return f
 
8
  benchmark anchors), and that the win/fail predicate tree is in the
9
  right band (after_ticks ≤ within_ticks ≤ reachable-tick).
10
 
11
+ Recalibration note (engine MOVEMENT fixes `attack_unit` on an
12
+ out-of-sight target paths normally instead of teleporting, and a
13
+ moving unit both fires and takes fire en route, respecting stance):
14
+ the fixes shifted combat bench-wide and broke this pack's bar two
15
+ ways. (1) Agent units left at `stance:3` AttackAnything now
16
+ auto-advance to intercept any visible enemy so a STALL policy
17
+ self-played the defence (the flex squad and both garrisons hunted
18
+ the pushes unaided) and won for free. (2) A fully-consolidated
19
+ force that attack-moves into a push is now strong enough to hold
20
+ EITHER base so a wrong-side consolidation onto the (formerly
21
+ unsavable) heavy EAST push WON, collapsing the concede-vs-hold
22
+ discrimination.
23
+
24
+ The pack was re-tuned: every agent mobile unit set to `stance:1`
25
+ ReturnFire (fires back when shot, never advances/initiates — so the
26
+ agent must EXPLICITLY attack-move the consolidated force, making the
27
+ capability load-bearing again); the WEST light push raised 16->20
28
+ and the EAST heavy push raised to 72 (split into two 36-count actor
29
+ entries — the schema caps a single count at 50) so the conceded
30
+ base is genuinely unsavable even by a full consolidation. Earlier
31
+ recalibration still in effect: `enemy_units_killed` termination
32
+ dropped (the win is a survival-band check), a `not proc:1` fail
33
+ clause, a persistent unarmed enemy `fact` marker (anti auto-DRAW),
34
+ the hard survival floor at tick 2400 and its attrition cap at 18.
35
+
36
+ The capability stays load-bearing — stall / split / oscillate /
37
+ wrong-side consolidate all LOSE; only consolidate-on-the-light-side
38
+ AND attack-move into the push WINS.
39
  """
40
 
41
  from __future__ import annotations
 
149
 
150
 
151
  def _cons_west(rs, C):
152
+ """Intended policy: ATTACK-MOVE every mobile unit into the WEST
153
+ push (the light-push side) so the consolidated force actively
154
+ clears the raiders. The agent's units are stance:1 ReturnFire —
155
+ they neither advance nor initiate on their own, so the explicit
156
+ attack-move order is what makes this policy effective. A plain
157
+ `move_units` to a fixed rally point leaves the arrived units
158
+ idle and lets the rifles chip the base buildings down."""
159
  u = rs.get("units_summary", []) or []
160
  cmds = [
161
+ C.attack_move([str(x["id"])], target_x=20, target_y=20)
162
  for x in u
 
163
  ]
164
  return cmds or [C.observe()]
165
 
166
 
167
  def _cons_east(rs, C):
168
+ """Wrong-side commit: attack-move everything into the EAST push
169
+ (the heavy-push side). The 72-rifle heavy push razes the EAST
170
+ base before the force can clear it, and the un-garrisoned WEST
171
+ base falls too — every refinery is lost."""
172
  u = rs.get("units_summary", []) or []
173
  cmds = [
174
+ C.attack_move([str(x["id"])], target_x=84, target_y=20)
175
  for x in u
 
176
  ]
177
  return cmds or [C.observe()]
178
 
179
 
180
  def _split_defend(rs, C):
181
+ """Split-defend: half the flex squad attack-moves to each base,
182
+ garrisons stay. Neither base gets enough reinforcement."""
183
  u = rs.get("units_summary", []) or []
184
  flex = sorted(
185
  [x for x in u if 38 <= x["cell_x"] <= 65], key=lambda x: x["id"]
186
  )
187
  cmds = []
188
  for i, x in enumerate(flex):
189
+ tgt = (20, 20) if i % 2 == 0 else (84, 20)
190
+ cmds.append(C.attack_move([str(x["id"])], target_x=tgt[0], target_y=tgt[1]))
191
  return cmds or [C.observe()]
192
 
193
 
 
197
 
198
 
199
  def _panic_reinforce_factory():
200
+ """Reinforce-both-back-and-forth: flip the attack-move target of
201
+ every unit every turn (no commitment, no rest). Pure wasted
202
+ travel — the force never settles long enough to clear either
203
+ push, so both bases fall."""
204
  tc = _PanicTC()
205
 
206
  def f(rs, C):
207
  tc.n += 1
208
  u = rs.get("units_summary", []) or []
209
+ tgt = (20, 20) if tc.n % 2 == 0 else (84, 20)
 
 
 
 
 
 
 
210
  return [
211
+ C.attack_move([str(x["id"])], target_x=tgt[0], target_y=tgt[1])
212
+ for x in u
213
  ] or [C.observe()]
214
 
215
  return f