yxc20098 commited on
Commit
aa72b64
·
1 Parent(s): d1ac2fa

fix(scenario): combat-prevent-retreat — recalibrate after engine balance fixes

Browse files

The engine balance fixes (armor-class weapon selection, stance
semantics) inverted this pack's combat math: the e3 cluster no
longer bled a frontal column charge, and the cluster (with no
explicit stance) advanced across the map instead of holding post.

Recalibration:
- Pin the centre cluster with an explicit stance:2 (Defend) so it
is a stationary firewall, not an advancing mob.
- Pack the cluster compactly (e3-heavy) so a brute attack-move
column drives into concentrated e3 fire and loses >=2 tanks,
busting the survival cap; easy kill bar 5 -> 6.
- The intended cut-off play now focus-fires (attack_unit) with the
three engaging tanks — concentrated fire clears the cluster
losing <=1, where a spread attack-move charge bleeds out.

Bar restored: stall / brute_east / brute_centre / east_only all
LOSE, intended WINS, every level x seed 1-4.

openra_bench/scenarios/packs/combat-prevent-retreat.yaml CHANGED
@@ -17,49 +17,53 @@
17
  # smoke runs, all seeds 1–4 × every tier):
18
  # - 4× medium tanks (2tnk) pre-placed at the west edge.
19
  # - Mixed enemy infantry cluster at the centre column (x=60):
20
- # easy 3× e1 + 2× e3 (5 enemies)
21
- # medium 4× e1 + 3× e3 (7 enemies)
22
- # hard 5× e1 + 3× e3 (8 enemies)
23
  # `e3` (rocket infantry) is anti-armour and BITES tanks within ~5
24
- # cells of the cluster a frontal column charge through the cluster
25
- # bleeds the tanks below the survival cap. e1 (small-arms ~3) is
26
- # filler that makes up the kill bar so the cluster can't be one-
27
- # shotted by overwhelming firepower.
28
- # - The enemy is STATIONARY (no `bot_type`, default stance: return
29
- # fire only). This is critical: a `hunt` bot would chase the lone
30
- # cutoff tank as it flanks, breaking the off-axis safe-route
31
- # assumption AND making the "stall lets the enemy walk into you"
32
- # degeneracy trivially satisfiable.
 
 
 
 
 
33
  # - The win predicate requires THREE things AT THE SAME EVALUATION:
34
  # (1) `units_killed_gte: K` — all centre enemies dead
35
  # (2) `units_in_region_gte:{85,20,r=8,n=1}` — cutoff tank present
36
  # (3) `own_units_gte: 3` — at least 3 tanks alive
37
  # plus `within_ticks: 4500` to deny stallers.
38
  #
39
- # Discrimination bar (six scripted-policy proxies in the test file;
40
  # every level × every seed 1–4 must hold):
41
  # • stall (only observe) → LOSS. No kills, no in-region,
42
  # timeout fires.
43
- # • brute attack_move-east-through-cluster (all 4 tanks toward 85,20)
44
- # LOSS. The column passes
45
- # through the e3 cluster within Dragon range and loses ≥2 tanks
46
- # en route → `own_units_gte:3` fails. The kill bar may or may not
47
- # be met (depends on tier) but survival busts either way.
48
  # • brute attack_move-centre (all 4 tanks toward 60,20) → LOSS.
49
- # The column does engage and may even kill all centre enemies on
50
- # easy/medium, BUT NO TANK IS IN THE EAST CUT-OFF REGION the
51
- # `units_in_region_gte` clause is never satisfied after_ticks
52
- # LOSS.
53
  # • east-only (send ONE tank east, hold the rest) → LOSS. One tank
54
  # can't kill the cluster; held tanks don't engage → kill bar
55
- # fails. Even on easy with 5 enemies, a solo east tank can't
56
- # pull the kill bar in the time budget.
57
  # • intended cut-off-then-engage (NORTH route via y=5..10 OR SOUTH
58
  # route via y=30..35) → WIN. The cutoff tank routes off-axis (out
59
  # of e3 Dragon range), arrives at (85,20). The remaining 3 tanks
60
- # attack-move the centre, kill the e1+e3 cluster, lose at most 1
61
- # tank (well within `own_units_gte:3`). Kill bar AND in-region AND
62
- # survival all satisfied WIN.
 
63
  #
64
  # Engine guardrails (per CLAUDE.md):
65
  # - Map: rush-hour-arena (128 × 40, playable x ∈ [2..126], y ∈ [2..38]).
@@ -69,7 +73,7 @@
69
  # engine would auto-`done` on enemy-elimination before the
70
  # `units_in_region_gte` predicate evaluates).
71
  # - Per CLAUDE.md (60,20) is not on the silent-fail list (the cited
72
- # problems are (50,20), (60,28), (90,30)); the e1/e3 cluster
73
  # spans (60, 16..23) on hard, no member at a known-bad cell.
74
  # - All deadlines (`after_ticks 4501`) are reachable within max_turns
75
  # (~90 ticks/turn ⇒ tick ≤ 93 + 90·(max_turns−1) = 5403 on
@@ -94,11 +98,10 @@ meta:
94
  tanks from the west. The win predicate explicitly requires the
95
  agent to have ESTABLISHED an eastern cut-off (≥1 own unit at
96
  (85,20,r=8)) at the moment the kill bar is met AND to have ≥3
97
- tanks alive. Brute charging without the cut-off either bleeds
98
- the force under the survival cap (attack-move east through the
99
- cluster) or wins the centre fight with no tank east (attack-move
100
- to the centre) — both LOSE. Stalling never opens fire, also a
101
- LOSS on the clock.
102
  robotics_analogue: >
103
  Multi-agent strike geometry: the placement of a "blocker" /
104
  "anvil" unit on the opposite side of the target BEFORE the main
@@ -132,25 +135,27 @@ base:
132
 
133
  levels:
134
  # ── EASY ──────────────────────────────────────────────────────────
135
- # Bare encirclement skill: 5-unit cluster (3× e1 + 2× e3 anti-armour)
136
- # at the centre. The 2× e3 are enough to punish a frontal-column
137
- # charge but not enough to overwhelm 3 attacking tanks from the
138
- # west. Generous tick budget.
139
  easy:
140
  description: >
141
  Four medium tanks (2tnk) stage at the west edge (x=6, y=18..21).
142
- A five-unit enemy infantry cluster holds the centre at (60,
143
- y=18..22): 3× e1 (rifle, small-arms) + 2× e3 (rocket, anti-
144
- armour). To win the encirclement you must POSITION ≥1 tank in
145
- the east cut-off region (85, 20, r=8) AND kill all 5 enemies
146
- AND keep ≥3 tanks alive, before tick 4500. Detach ONE tank on
147
- a flank route (via y=5..10 or y=30..35 to stay out of e3
148
- Dragon range) and run it east to (85, 20); engage the centre
149
- with the remaining THREE tanks. A head-on attack-move-east
150
- through the cluster bleeds tanks below the survival cap and
151
- LOSES; an attack-move-centre kills the e1+e3 but no tank
152
- reaches the east region also LOSES. Stall LOSES on the
153
- clock.
 
 
154
  overrides:
155
  actors:
156
  # Strike force — 4 medium tanks staged at the west edge.
@@ -160,13 +165,22 @@ levels:
160
  - {type: 2tnk, owner: agent, position: [6, 19], stance: 1}
161
  - {type: 2tnk, owner: agent, position: [6, 20], stance: 1}
162
  - {type: 2tnk, owner: agent, position: [6, 21], stance: 1}
163
- # ENEMY centre cluster — 3× e1 + 2× e3. Stationary defenders
164
- # (no bot_type); stance defaults to return-fire when attacked.
165
- - {type: e1, owner: enemy, position: [60, 18]}
166
- - {type: e3, owner: enemy, position: [60, 19]}
167
- - {type: e1, owner: enemy, position: [60, 20]}
168
- - {type: e3, owner: enemy, position: [60, 21]}
169
- - {type: e1, owner: enemy, position: [60, 22]}
 
 
 
 
 
 
 
 
 
170
  # SENTINEL — unarmed enemy `fact` far in the north corner.
171
  # Keeps the episode alive past enemy-cluster elimination so
172
  # the in-region predicate evaluates on the terminal frame
@@ -175,7 +189,7 @@ levels:
175
  - {type: fact, owner: enemy, position: [120, 4]}
176
  win_condition:
177
  all_of:
178
- - {units_killed_gte: 5}
179
  - {units_in_region_gte: {x: 85, y: 20, radius: 8, n: 1}}
180
  - {own_units_gte: 3}
181
  - {within_ticks: 4500}
@@ -186,33 +200,37 @@ levels:
186
  max_turns: 60
187
 
188
  # ── MEDIUM ────────────────────────────────────────────────────────
189
- # +1 controlled variable vs easy: the enemy cluster gains TWO more
190
- # units (one e1 + one e3 → 7 total at y=17..23). Kill bar 7 makes
191
- # the east-only solo-tank play decisively too slow; the e3 density
192
- # makes brute-charge-east decisively bleed.
193
  medium:
194
  description: >
195
  Four medium tanks (2tnk) stage at the west edge (x=6, y=18..21).
196
  A seven-unit enemy infantry cluster holds the centre at (60,
197
- y=17..23): 4× e1 + 3× e3 (rocket anti-armour). To win you must
198
- POSITION ≥1 tank in the east cut-off region (85, 20, r=8) AND
199
- kill all 7 enemies AND keep ≥3 tanks alive, before tick 4500.
200
- Detach ONE tank on a flank route (via y=5..10 or y=30..35) and
201
- run it east to (85, 20); engage the centre with the remaining
202
- three. Head-on, east-only, and stall all LOSE.
 
 
203
  overrides:
204
  actors:
205
  - {type: 2tnk, owner: agent, position: [6, 18], stance: 1}
206
  - {type: 2tnk, owner: agent, position: [6, 19], stance: 1}
207
  - {type: 2tnk, owner: agent, position: [6, 20], stance: 1}
208
  - {type: 2tnk, owner: agent, position: [6, 21], stance: 1}
209
- - {type: e1, owner: enemy, position: [60, 17]}
210
- - {type: e3, owner: enemy, position: [60, 18]}
211
- - {type: e1, owner: enemy, position: [60, 19]}
212
- - {type: e3, owner: enemy, position: [60, 20]}
213
- - {type: e1, owner: enemy, position: [60, 21]}
214
- - {type: e3, owner: enemy, position: [60, 22]}
215
- - {type: e1, owner: enemy, position: [60, 23]}
 
 
216
  - {type: fact, owner: enemy, position: [120, 4]}
217
  win_condition:
218
  all_of:
@@ -243,14 +261,15 @@ levels:
243
  Four medium tanks (2tnk) stage at ONE of two west-edge corridors
244
  (NORTH y=12..15 OR SOUTH y=25..28, chosen by seed, anti-
245
  memorisation). An eight-unit enemy infantry cluster holds the
246
- centre at (60, y=16..23): 5× e1 + 3× e3 (rocket anti-armour).
247
- To win you must POSITION ≥1 tank in the east cut-off region
248
- (85, 20, r=8) AND kill all 8 enemies AND keep ≥3 tanks alive,
249
- before tick 4500. From the NORTH spawn run the cutoff tank via
250
- y=5..10 to (85, 20); from the SOUTH spawn run it via y=30..35.
251
- Engage the centre with the remaining three tanks descending
252
- onto the cluster from the staging latitude. Head-on, east-only,
253
- and stall all LOSE on every seed.
 
254
  overrides:
255
  actors:
256
  # spawn_point 0 — NORTH staging corridor (y=12..15). Already
@@ -266,15 +285,16 @@ levels:
266
  - {type: 2tnk, owner: agent, position: [6, 26], stance: 1, spawn_point: 1}
267
  - {type: 2tnk, owner: agent, position: [6, 27], stance: 1, spawn_point: 1}
268
  - {type: 2tnk, owner: agent, position: [6, 28], stance: 1, spawn_point: 1}
269
- # 8× cluster symmetric across y=20 at the centre.
270
- - {type: e1, owner: enemy, position: [60, 16]}
271
- - {type: e3, owner: enemy, position: [60, 17]}
272
- - {type: e1, owner: enemy, position: [60, 18]}
273
- - {type: e3, owner: enemy, position: [60, 19]}
274
- - {type: e1, owner: enemy, position: [60, 20]}
275
- - {type: e3, owner: enemy, position: [60, 21]}
276
- - {type: e1, owner: enemy, position: [60, 22]}
277
- - {type: e1, owner: enemy, position: [60, 23]}
 
278
  - {type: fact, owner: enemy, position: [120, 4]}
279
  win_condition:
280
  all_of:
 
17
  # smoke runs, all seeds 1–4 × every tier):
18
  # - 4× medium tanks (2tnk) pre-placed at the west edge.
19
  # - Mixed enemy infantry cluster at the centre column (x=60):
20
+ # easy 3× e3 + 3× e1 (6 enemies, y=17..22)
21
+ # medium 3× e3 + 4× e1 (7 enemies, y=16..22)
22
+ # hard 3× e3 + 5× e1 (8 enemies, y=16..23)
23
  # `e3` (rocket infantry) is anti-armour and BITES tanks within ~5
24
+ # cells of the cluster. The cluster is packed COMPACTLY so a
25
+ # frontal column charge entering e3 range (x≈59) takes
26
+ # concentrated fire from every e3 at once and bleeds ≥2 tanks
27
+ # below the survival cap before it can fight back. e1 (small-arms)
28
+ # is filler that makes up the kill bar.
29
+ # - The enemy cluster carries an EXPLICIT `stance: 2` (Defend)
30
+ # stationary, auto-fires on the closest in-range foe but never
31
+ # advances. This is critical: with the engine's four-stance
32
+ # semantics the default (omitted-stance) behaviour is no longer
33
+ # reliably stationary; an advancing cluster would chase the lone
34
+ # cutoff tank as it flanks (breaking the off-axis safe-route
35
+ # assumption) AND make the "stall lets the enemy walk into you"
36
+ # degeneracy trivially satisfiable. Explicit stance:2 pins the
37
+ # cluster as a stationary firewall.
38
  # - The win predicate requires THREE things AT THE SAME EVALUATION:
39
  # (1) `units_killed_gte: K` — all centre enemies dead
40
  # (2) `units_in_region_gte:{85,20,r=8,n=1}` — cutoff tank present
41
  # (3) `own_units_gte: 3` — at least 3 tanks alive
42
  # plus `within_ticks: 4500` to deny stallers.
43
  #
44
+ # Discrimination bar (five scripted-policy proxies in the test file;
45
  # every level × every seed 1–4 must hold):
46
  # • stall (only observe) → LOSS. No kills, no in-region,
47
  # timeout fires.
48
+ # • brute attack_move-east (all 4 tanks toward 85,20) → LOSS. The
49
+ # column drives straight into the stationary e3 wall, takes
50
+ # concentrated fire and loses ≥2 tanks → `own_units_gte:3` fails.
 
 
51
  # • brute attack_move-centre (all 4 tanks toward 60,20) → LOSS.
52
+ # The column engages but loses ≥2 tanks to the concentrated e3
53
+ # fire (no flank, no off-axis cutoff) AND no tank reaches the
54
+ # east cut-off region both the survival cap and the
55
+ # `units_in_region_gte` clause fail → LOSS.
56
  # • east-only (send ONE tank east, hold the rest) → LOSS. One tank
57
  # can't kill the cluster; held tanks don't engage → kill bar
58
+ # fails. A solo east tank driving past the cluster is shredded by
59
+ # the e3 fire.
60
  # • intended cut-off-then-engage (NORTH route via y=5..10 OR SOUTH
61
  # route via y=30..35) → WIN. The cutoff tank routes off-axis (out
62
  # of e3 Dragon range), arrives at (85,20). The remaining 3 tanks
63
+ # STOP and focus-fire (attack_unit) the cluster one target at a
64
+ # time, kill the e3+e1 cluster, lose at most 1 tank (well within
65
+ # `own_units_gte:3`). Kill bar AND in-region AND survival all
66
+ # satisfied → WIN.
67
  #
68
  # Engine guardrails (per CLAUDE.md):
69
  # - Map: rush-hour-arena (128 × 40, playable x ∈ [2..126], y ∈ [2..38]).
 
73
  # engine would auto-`done` on enemy-elimination before the
74
  # `units_in_region_gte` predicate evaluates).
75
  # - Per CLAUDE.md (60,20) is not on the silent-fail list (the cited
76
+ # problems are (50,20), (60,28), (90,30)); the e3/e1 cluster
77
  # spans (60, 16..23) on hard, no member at a known-bad cell.
78
  # - All deadlines (`after_ticks 4501`) are reachable within max_turns
79
  # (~90 ticks/turn ⇒ tick ≤ 93 + 90·(max_turns−1) = 5403 on
 
98
  tanks from the west. The win predicate explicitly requires the
99
  agent to have ESTABLISHED an eastern cut-off (≥1 own unit at
100
  (85,20,r=8)) at the moment the kill bar is met AND to have ≥3
101
+ tanks alive. Brute charging without the cut-off bleeds the force
102
+ under the survival cap — a column attack-moving into the compact
103
+ stationary e3 wall loses ≥2 tanks whether it heads east or for
104
+ the centre. Stalling never opens fire, also a LOSS on the clock.
 
105
  robotics_analogue: >
106
  Multi-agent strike geometry: the placement of a "blocker" /
107
  "anvil" unit on the opposite side of the target BEFORE the main
 
135
 
136
  levels:
137
  # ── EASY ──────────────────────────────────────────────────────────
138
+ # Bare encirclement skill: 6-unit cluster (3× e3 anti-armour +
139
+ # 3× e1 filler) packed compactly at the centre. The 3× e3 punish a
140
+ # frontal-column charge (≥2 tanks lost) but 3 focus-firing tanks
141
+ # from the west clear the cluster losing ≤1. Generous tick budget.
142
  easy:
143
  description: >
144
  Four medium tanks (2tnk) stage at the west edge (x=6, y=18..21).
145
+ A six-unit enemy infantry cluster holds the centre at (60,
146
+ y=17..22): 3× e3 (rocket, anti-armour) + 3× e1 (rifle, small-
147
+ arms), packed compactly and stationary (Defend stance). To win
148
+ the encirclement you must POSITION ≥1 tank in the east cut-off
149
+ region (85, 20, r=8) AND kill all 6 enemies AND keep ≥3 tanks
150
+ alive, before tick 4500. Detach ONE tank on a flank route (via
151
+ y=5..10 or y=30..35 to stay out of e3 Dragon range) and run it
152
+ east to (85, 20); engage the centre with the remaining THREE
153
+ tanks, STOPPING to focus-fire the cluster. A head-on attack-
154
+ move-east column charge runs straight into the stationary e3
155
+ wall, takes concentrated fire and bleeds ≥2 tanks below the
156
+ survival cap — LOSES; an attack-move-centre kills the cluster
157
+ but no tank reaches the east region — also LOSES. Stall LOSES
158
+ on the clock.
159
  overrides:
160
  actors:
161
  # Strike force — 4 medium tanks staged at the west edge.
 
165
  - {type: 2tnk, owner: agent, position: [6, 19], stance: 1}
166
  - {type: 2tnk, owner: agent, position: [6, 20], stance: 1}
167
  - {type: 2tnk, owner: agent, position: [6, 21], stance: 1}
168
+ # ENEMY centre cluster — 3× e3 (anti-armour) + 3× e1
169
+ # (filler) packed compactly at y=17..22. EXPLICIT stance:2
170
+ # (Defend) — stationary, auto-fires on the closest in-range
171
+ # foe but never advances. The compact stationary e3 wall is
172
+ # what bleeds a frontal column charge: a brute attack-move
173
+ # column entering e3 range at x≈59 takes concentrated fire
174
+ # from every e3 at once and loses ≥2 tanks before it can
175
+ # fight back. Three tanks that STOP and focus-fire (the
176
+ # intended play, the cutoff tank flanking off-axis) clear
177
+ # the cluster losing ≤1.
178
+ - {type: e3, owner: enemy, position: [60, 17], stance: 2}
179
+ - {type: e1, owner: enemy, position: [60, 18], stance: 2}
180
+ - {type: e3, owner: enemy, position: [60, 19], stance: 2}
181
+ - {type: e1, owner: enemy, position: [60, 20], stance: 2}
182
+ - {type: e3, owner: enemy, position: [60, 21], stance: 2}
183
+ - {type: e1, owner: enemy, position: [60, 22], stance: 2}
184
  # SENTINEL — unarmed enemy `fact` far in the north corner.
185
  # Keeps the episode alive past enemy-cluster elimination so
186
  # the in-region predicate evaluates on the terminal frame
 
189
  - {type: fact, owner: enemy, position: [120, 4]}
190
  win_condition:
191
  all_of:
192
+ - {units_killed_gte: 6}
193
  - {units_in_region_gte: {x: 85, y: 20, radius: 8, n: 1}}
194
  - {own_units_gte: 3}
195
  - {within_ticks: 4500}
 
200
  max_turns: 60
201
 
202
  # ── MEDIUM ────────────────────────────────────────────────────────
203
+ # +1 controlled variable vs easy: the enemy cluster gains one more
204
+ # e1 ( e3 + e1 → 7 total, compact at y=16..22). Kill bar 7
205
+ # makes the east-only solo-tank play decisively too slow; the
206
+ # compact stationary e3 wall makes brute-charge decisively bleed.
207
  medium:
208
  description: >
209
  Four medium tanks (2tnk) stage at the west edge (x=6, y=18..21).
210
  A seven-unit enemy infantry cluster holds the centre at (60,
211
+ y=16..22): 3× e3 (rocket anti-armour) + e1, packed compactly
212
+ and stationary (Defend stance). To win you must POSITION ≥1
213
+ tank in the east cut-off region (85, 20, r=8) AND kill all 7
214
+ enemies AND keep ≥3 tanks alive, before tick 4500. Detach ONE
215
+ tank on a flank route (via y=5..10 or y=30..35) and run it east
216
+ to (85, 20); engage the centre with the remaining three,
217
+ stopping to focus-fire the cluster. Head-on, east-only, and
218
+ stall all LOSE.
219
  overrides:
220
  actors:
221
  - {type: 2tnk, owner: agent, position: [6, 18], stance: 1}
222
  - {type: 2tnk, owner: agent, position: [6, 19], stance: 1}
223
  - {type: 2tnk, owner: agent, position: [6, 20], stance: 1}
224
  - {type: 2tnk, owner: agent, position: [6, 21], stance: 1}
225
+ # Stationary Defend (stance:2) cluster e3 anti-armour
226
+ # interleaved with e1 filler, compact at y=16..22.
227
+ - {type: e3, owner: enemy, position: [60, 16], stance: 2}
228
+ - {type: e1, owner: enemy, position: [60, 17], stance: 2}
229
+ - {type: e3, owner: enemy, position: [60, 18], stance: 2}
230
+ - {type: e1, owner: enemy, position: [60, 19], stance: 2}
231
+ - {type: e3, owner: enemy, position: [60, 20], stance: 2}
232
+ - {type: e1, owner: enemy, position: [60, 21], stance: 2}
233
+ - {type: e1, owner: enemy, position: [60, 22], stance: 2}
234
  - {type: fact, owner: enemy, position: [120, 4]}
235
  win_condition:
236
  all_of:
 
261
  Four medium tanks (2tnk) stage at ONE of two west-edge corridors
262
  (NORTH y=12..15 OR SOUTH y=25..28, chosen by seed, anti-
263
  memorisation). An eight-unit enemy infantry cluster holds the
264
+ centre at (60, y=16..23): 3× e3 (rocket anti-armour) + 5× e1,
265
+ packed compactly and stationary (Defend stance). To win you
266
+ must POSITION ≥1 tank in the east cut-off region (85, 20, r=8)
267
+ AND kill all 8 enemies AND keep ≥3 tanks alive, before tick
268
+ 4500. From the NORTH spawn run the cutoff tank via y=5..10 to
269
+ (85, 20); from the SOUTH spawn run it via y=30..35. Engage the
270
+ centre with the remaining three tanks, stopping to focus-fire
271
+ the cluster. Head-on, east-only, and stall all LOSE on every
272
+ seed.
273
  overrides:
274
  actors:
275
  # spawn_point 0 — NORTH staging corridor (y=12..15). Already
 
285
  - {type: 2tnk, owner: agent, position: [6, 26], stance: 1, spawn_point: 1}
286
  - {type: 2tnk, owner: agent, position: [6, 27], stance: 1, spawn_point: 1}
287
  - {type: 2tnk, owner: agent, position: [6, 28], stance: 1, spawn_point: 1}
288
+ # 8× cluster symmetric across y=20 e3 anti-armour
289
+ # interleaved with e1 filler, stationary Defend stance.
290
+ - {type: e1, owner: enemy, position: [60, 16], stance: 2}
291
+ - {type: e3, owner: enemy, position: [60, 17], stance: 2}
292
+ - {type: e1, owner: enemy, position: [60, 18], stance: 2}
293
+ - {type: e3, owner: enemy, position: [60, 19], stance: 2}
294
+ - {type: e1, owner: enemy, position: [60, 20], stance: 2}
295
+ - {type: e3, owner: enemy, position: [60, 21], stance: 2}
296
+ - {type: e1, owner: enemy, position: [60, 22], stance: 2}
297
+ - {type: e1, owner: enemy, position: [60, 23], stance: 2}
298
  - {type: fact, owner: enemy, position: [120, 4]}
299
  win_condition:
300
  all_of:
tests/test_combat_prevent_retreat.py CHANGED
@@ -1,11 +1,11 @@
1
  """combat-prevent-retreat — cut-off encirclement before engaging.
2
 
3
- The bar: 4× medium tanks (2tnk) start at the west edge; a mixed e1+e3
4
- infantry cluster holds the centre (60, 16..23). The win predicate
5
- requires:
6
 
7
  • `units_killed_gte: K` — the full cluster wiped
8
- (K=5 easy / 7 medium / 8 hard)
9
  • `units_in_region_gte:{85,20,r=8,n=1}` — ≥1 own tank in the east
10
  cut-off region at evaluation time
11
  • `own_units_gte: 3` — at least 3 tanks survive
@@ -14,26 +14,31 @@ requires:
14
  This combination forces the Cannae idiom: ONE flank tank routes
15
  off-axis (via y=5..10 from a NORTH spawn or y=30..35 from a SOUTH
16
  spawn, out of e3 Dragon range) to take the eastern anvil; the
17
- remaining THREE engage the centre from the west. The cluster's
18
- anti-armour e3s punish a head-on column charge (`brute_east` loses
19
- ≥2 tanks survival cap busts); a centre-only charge leaves no tank
20
- east (in-region clause fails); a solo east tank can't pull the kill
21
- bar in budget.
 
 
22
 
23
  Discrimination (verified per-level × per-seed 1–4):
24
 
25
  • stall (observe-only) — LOSS on every level/seed
26
  (no kills, deadline fires).
27
  • brute_east (attack_move all to 85,20)
28
- — LOSS: column bled by e3
29
- Dragons; survival cap busts.
 
 
30
  • brute_centre (attack_move all to 60,20)
31
- — LOSS: cluster killed but
32
- no tank east → in-region
33
- clause fails after_ticks.
 
34
  • east_only (move 1 east, hold rest)— LOSS: kill bar never met
35
  (1 tank vs cluster too slow).
36
- • intended (cut-off then engage) — WIN every level/seed.
37
 
38
  Validation is scripted (no model / network)."""
39
  from __future__ import annotations
@@ -112,7 +117,7 @@ def test_predicates_per_level():
112
  pack = load_pack(PACK_PATH)
113
  # (kill_bar, surv_min, within, after_fail)
114
  expectations = {
115
- "easy": (5, 3, 4500, 4501),
116
  "medium": (7, 3, 4500, 4501),
117
  "hard": (8, 3, 4500, 4501),
118
  }
@@ -236,6 +241,27 @@ def test_enemy_is_static_no_bot_type():
236
  )
237
 
238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  def test_hard_has_two_spawn_point_groups():
240
  """Hard-tier curation: ≥2 distinct agent spawn_point groups so the
241
  seed round-robins which staging corridor (north y=12 or south y=28)
@@ -267,17 +293,19 @@ def _stall(rs, Command):
267
 
268
 
269
  def _brute_east(rs, Command):
270
- """All 4 tanks attack-move EAST through the cluster toward (85,20).
271
- The column passes within e3 Dragon range and bleeds below the
272
- survival cap → LOSS on every level."""
 
273
  ids = _tank_ids(rs)
274
  return [Command.attack_move(ids, 85, 20)] if ids else [Command.observe()]
275
 
276
 
277
  def _brute_centre(rs, Command):
278
- """All 4 tanks attack-move the centre cluster at (60,20). May clear
279
- the cluster (kills bar met) but NO TANK IS IN THE EAST REGION at
280
- evaluation time in-region clause fails after_ticks LOSS."""
 
281
  ids = _tank_ids(rs)
282
  return [Command.attack_move(ids, 60, 20)] if ids else [Command.observe()]
283
 
@@ -300,7 +328,10 @@ def _make_intended():
300
  the actual spawn (NORTH spawn → off-y=5; SOUTH spawn → off-y=35),
301
  pick the cutoff tank closest to that latitude, route it via three
302
  waypoints out of e3 Dragon range to (85,20). The remaining three
303
- tanks attack-move the centre cluster from the west."""
 
 
 
304
  state = {}
305
 
306
  def policy(rs, Command):
@@ -333,7 +364,17 @@ def _make_intended():
333
  ):
334
  state["phase"] += 1
335
  if others:
336
- cmds.append(Command.attack_move(others, 60, 20))
 
 
 
 
 
 
 
 
 
 
337
  return cmds or [Command.observe()]
338
 
339
  return policy
@@ -356,9 +397,10 @@ def test_stall_loses(level, seed):
356
  @pytest.mark.parametrize("level", ["easy", "medium", "hard"])
357
  @pytest.mark.parametrize("seed", [1, 2, 3, 4])
358
  def test_brute_east_loses(level, seed):
359
- """Attack-move all tanks east through the cluster. The e3 Dragon
360
- fire bleeds the column below the survival cap `own_units_gte:3`
361
- fails LOSS on every level/seed."""
 
362
  pytest.importorskip("openra_train")
363
  from openra_bench.eval_core import run_level
364
 
@@ -374,9 +416,10 @@ def test_brute_east_loses(level, seed):
374
  @pytest.mark.parametrize("level", ["easy", "medium", "hard"])
375
  @pytest.mark.parametrize("seed", [1, 2, 3, 4])
376
  def test_brute_centre_loses(level, seed):
377
- """Attack-move all tanks to the centre cluster (60,20). Even when
378
- the cluster is wiped (kill bar met), NO TANK IS IN THE EAST CUT-
379
- OFF REGION in-region clause fails after_ticks LOSS."""
 
380
  pytest.importorskip("openra_train")
381
  from openra_bench.eval_core import run_level
382
 
 
1
  """combat-prevent-retreat — cut-off encirclement before engaging.
2
 
3
+ The bar: 4× medium tanks (2tnk) start at the west edge; a compact,
4
+ stationary (Defend stance:2) e3+e1 infantry cluster holds the centre
5
+ (60, 16..23). The win predicate requires:
6
 
7
  • `units_killed_gte: K` — the full cluster wiped
8
+ (K=6 easy / 7 medium / 8 hard)
9
  • `units_in_region_gte:{85,20,r=8,n=1}` — ≥1 own tank in the east
10
  cut-off region at evaluation time
11
  • `own_units_gte: 3` — at least 3 tanks survive
 
14
  This combination forces the Cannae idiom: ONE flank tank routes
15
  off-axis (via y=5..10 from a NORTH spawn or y=30..35 from a SOUTH
16
  spawn, out of e3 Dragon range) to take the eastern anvil; the
17
+ remaining THREE engage the centre from the west, STOPPING to
18
+ focus-fire (`attack_unit`) the cluster one target at a time. The
19
+ compact stationary e3 wall punishes a head-on column charge
20
+ (`brute_east` / `brute_centre` drive into concentrated e3 fire and
21
+ lose ≥2 tanks → survival cap busts); a centre-only charge also
22
+ leaves no tank east (in-region clause fails); a solo east tank
23
+ can't pull the kill bar in budget.
24
 
25
  Discrimination (verified per-level × per-seed 1–4):
26
 
27
  • stall (observe-only) — LOSS on every level/seed
28
  (no kills, deadline fires).
29
  • brute_east (attack_move all to 85,20)
30
+ — LOSS: column drives into
31
+ the stationary e3 wall and
32
+ loses ≥2 tanks; survival
33
+ cap busts.
34
  • brute_centre (attack_move all to 60,20)
35
+ — LOSS: column bled by the
36
+ e3 wall AND no tank east →
37
+ survival + in-region both
38
+ fail → LOSS.
39
  • east_only (move 1 east, hold rest)— LOSS: kill bar never met
40
  (1 tank vs cluster too slow).
41
+ • intended (cut-off then focus-fire)— WIN every level/seed.
42
 
43
  Validation is scripted (no model / network)."""
44
  from __future__ import annotations
 
117
  pack = load_pack(PACK_PATH)
118
  # (kill_bar, surv_min, within, after_fail)
119
  expectations = {
120
+ "easy": (6, 3, 4500, 4501),
121
  "medium": (7, 3, 4500, 4501),
122
  "hard": (8, 3, 4500, 4501),
123
  }
 
241
  )
242
 
243
 
244
+ def test_enemy_cluster_carries_explicit_defend_stance():
245
+ """The cluster carries an EXPLICIT stance:2 (Defend) — stationary,
246
+ auto-fires in range, never advances. The engine's four-stance
247
+ semantics mean an omitted stance is no longer reliably stationary
248
+ (an advancing cluster would chase the cutoff tank and let a
249
+ staller win); the explicit stance:2 pins the e3 wall in place."""
250
+ pack = load_pack(PACK_PATH)
251
+ for lvl in ("easy", "medium", "hard"):
252
+ c = compile_level(pack, lvl)
253
+ cluster = [
254
+ a for a in c.scenario.actors
255
+ if a.owner == "enemy" and a.type in ("e1", "e3")
256
+ ]
257
+ assert cluster, f"{lvl}: no cluster declared"
258
+ for a in cluster:
259
+ assert a.stance == 2, (
260
+ f"{lvl}: cluster member {a.type}@{a.position} must carry "
261
+ f"explicit stance:2 (Defend); got stance={a.stance}"
262
+ )
263
+
264
+
265
  def test_hard_has_two_spawn_point_groups():
266
  """Hard-tier curation: ≥2 distinct agent spawn_point groups so the
267
  seed round-robins which staging corridor (north y=12 or south y=28)
 
293
 
294
 
295
  def _brute_east(rs, Command):
296
+ """All 4 tanks attack-move EAST toward (85,20). The column drives
297
+ straight into the compact stationary e3 wall, takes concentrated
298
+ fire and bleeds ≥2 tanks below the survival cap → LOSS on every
299
+ level."""
300
  ids = _tank_ids(rs)
301
  return [Command.attack_move(ids, 85, 20)] if ids else [Command.observe()]
302
 
303
 
304
  def _brute_centre(rs, Command):
305
+ """All 4 tanks attack-move the centre cluster at (60,20). The
306
+ column is bled by the stationary e3 wall (≥2 tanks lost) AND no
307
+ tank is in the east cut-off region survival cap and in-region
308
+ clause both fail → LOSS."""
309
  ids = _tank_ids(rs)
310
  return [Command.attack_move(ids, 60, 20)] if ids else [Command.observe()]
311
 
 
328
  the actual spawn (NORTH spawn → off-y=5; SOUTH spawn → off-y=35),
329
  pick the cutoff tank closest to that latitude, route it via three
330
  waypoints out of e3 Dragon range to (85,20). The remaining three
331
+ tanks STOP and focus-fire (attack_unit) the centre cluster one
332
+ target at a time — concentrated fire clears the compact e3 wall
333
+ losing ≤1 tank, where an attack-move spread charge would bleed
334
+ ≥2."""
335
  state = {}
336
 
337
  def policy(rs, Command):
 
364
  ):
365
  state["phase"] += 1
366
  if others:
367
+ ens = [
368
+ e for e in (rs.get("enemy_summary") or [])
369
+ if str(e.get("type", "")).lower() in ("e1", "e3")
370
+ ]
371
+ if ens:
372
+ # Focus-fire the western-most cluster member with all
373
+ # three engaging tanks (concentrated fire).
374
+ ens.sort(key=lambda e: int(e["cell_x"]))
375
+ cmds.append(Command.attack_unit(others, str(ens[0]["id"])))
376
+ else:
377
+ cmds.append(Command.attack_move(others, 60, 20))
378
  return cmds or [Command.observe()]
379
 
380
  return policy
 
397
  @pytest.mark.parametrize("level", ["easy", "medium", "hard"])
398
  @pytest.mark.parametrize("seed", [1, 2, 3, 4])
399
  def test_brute_east_loses(level, seed):
400
+ """Attack-move all tanks east toward (85,20). The column drives
401
+ into the compact stationary e3 wall; concentrated Dragon fire
402
+ bleeds it below the survival cap → `own_units_gte:3` fails →
403
+ LOSS on every level/seed."""
404
  pytest.importorskip("openra_train")
405
  from openra_bench.eval_core import run_level
406
 
 
416
  @pytest.mark.parametrize("level", ["easy", "medium", "hard"])
417
  @pytest.mark.parametrize("seed", [1, 2, 3, 4])
418
  def test_brute_centre_loses(level, seed):
419
+ """Attack-move all tanks to the centre cluster (60,20). The column
420
+ is bled by the stationary e3 wall (≥2 tanks lost) AND no tank
421
+ reaches the east cut-off region survival cap and in-region
422
+ clause both fail → LOSS."""
423
  pytest.importorskip("openra_train")
424
  from openra_bench.eval_core import run_level
425