yxc20098 commited on
Commit
c52875c
·
1 Parent(s): ae1d089

feat(scenario): combat-target-priority-highvalue — focus high-threat units first (SC2 focus-fire anchor)

Browse files
openra_bench/scenarios/packs/combat-target-priority-highvalue.yaml ADDED
@@ -0,0 +1,345 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # combat-target-priority-highvalue — ACTION capability: threat-weighted
2
+ # target prioritization. A 4-tank squad faces a MIXED enemy cluster:
3
+ # cheap rifle chaff (e1) plus THREE high-threat anti-armour rocket
4
+ # soldiers (e3). The squad must FOCUS THE THREE ROCKET SOLDIERS FIRST
5
+ # — they out-damage the chaff and whittle the squad below the survival
6
+ # cap if left alive. Killing the chaff first leaves the rockets firing
7
+ # through the whole engagement and the squad LOSES.
8
+ #
9
+ # Sibling of combat-focus-fire-priority (same SC2 focus-fire idiom);
10
+ # THIS pack encodes the capability via a SURVIVAL floor
11
+ # (`own_units_gte`) plus an explicit non-combat building anchor
12
+ # (`building_count_gte:fact:1`) — the "preserve the squad" framing
13
+ # rather than the "attrition cap" (`units_lost_lte`) framing. Same
14
+ # anchor family, different predicate idiom.
15
+ #
16
+ # Discrimination (the four script-policy bar — verified deterministic
17
+ # across seeds 1-4 on every level):
18
+ # • stall (only observe): the tanks idle at the staging edge and
19
+ # never close; the kill bar (units_killed_gte:K) is never met and
20
+ # the after_ticks fail clause fires → LOSS.
21
+ # • brute attack_move (attack-move onto the cluster centroid): the
22
+ # engine auto-targets the NEAREST hostile — the e1 chaff screen is
23
+ # posted three columns closer than the rocket soldiers, so the
24
+ # squad chews the chaff first while the three e3s fire anti-armour
25
+ # rockets through the whole engagement; all four tanks are lost
26
+ # → busts `own_units_gte` → LOSS.
27
+ # • kill-chaff-first (attack_unit the e1 chaff one at a time): the
28
+ # three e3 rockets keep landing through the entire chaff mop-up;
29
+ # ~3 tanks die before the rockets are silenced → busts the
30
+ # survival floor → LOSS.
31
+ # • intended focus-threats-first: attack_unit ALL FOUR tanks onto
32
+ # ONE e3 at a time — 4 tank cannons concentrate ~400 HP of armour
33
+ # against ~50 HP of infantry, so each e3 dies in 1-2 decision
34
+ # turns before it can land more than ~1 rocket; with all three
35
+ # rockets silenced inside the first ~3 turns the chaff cannot
36
+ # scratch the squad → mop up the e1s → kill bar met, the squad
37
+ # stays above the survival floor → WIN.
38
+ #
39
+ # Why the spec works (verified against the engine combat sheet):
40
+ # • e3 = anti-tank rocket soldier (Dragon launcher, range ~5, high
41
+ # anti-armour DPS). THREE e3 firing unopposed wipe a 4-tank squad
42
+ # in ~540 ticks (~6 decision turns) — measured. Left firing
43
+ # through a chaff mop-up they reliably kill ~3 tanks.
44
+ # • e1 = basic rifle infantry. Per-shot damage vs 2tnk armour is
45
+ # low; the e1 chaff alone cannot drop the squad below the floor
46
+ # inside the budget once the three e3s are dead.
47
+ # • 4× 2tnk concentrated on a single e3 kill it in 1-2 decision
48
+ # turns; the same fire SPREAD across the cluster kills the first
49
+ # e3 in 4+ turns, granting all three rockets free anti-tank
50
+ # shots. That focus-vs-spread delta is the discrimination.
51
+ # • Measured (deterministic): chaff-first loses 3 tanks, focus-
52
+ # threats-first loses 0, brute loses all 4, on every level.
53
+ #
54
+ # Engine guardrails:
55
+ # • Map: rush-hour-arena (128 × 40, playable x in [2..126], y in
56
+ # [2..38]). Tanks stage at x=48; the enemy cluster holds at
57
+ # x=66-70 (stance:2 Defend — a stationary wall, auto-fires in
58
+ # range but never advances, so the screen+rocket geometry is
59
+ # stable and the rockets fire the moment a tank is in range).
60
+ # The e1 screen is a COMPACT contiguous column so the whole
61
+ # cluster stays inside the tank squad's vision (no straggler
62
+ # slips into the fog and starves the kill count).
63
+ # • Agent `fact` at the staging edge — satisfies the win clause
64
+ # `building_count_gte:fact:1` (a non-combat anchor: the squad
65
+ # must still own its base while it focus-fires) AND, being an
66
+ # agent actor, keeps the agent side alive for the evaluator.
67
+ # • Far-east enemy `fact` at (120,20) prevents engine auto-done
68
+ # when the last cluster unit falls — the kill bar is satisfied on
69
+ # units, not buildings, so the marker keeps the episode running
70
+ # until the win/fail evaluator sees the terminal frame (no DRAW).
71
+ # • after_ticks/within_ticks reachable within max_turns
72
+ # (tick ≤ 93 + 90·(max_turns-1)); within_ticks 2700 < 2703 at
73
+ # max_turns 30; a staller hits after_ticks 2701 and LOSES.
74
+
75
+ meta:
76
+ id: combat-target-priority-highvalue
77
+ title: 'Target Priority — Kill the High-Threat Units FIRST'
78
+ capability: action
79
+ real_world_meaning: >
80
+ Four medium tanks face a mixed enemy cluster at close range:
81
+ a screen of cheap rifle infantry (e1 chaff) backed by THREE
82
+ high-threat anti-armour rocket soldiers (e3). The decision is
83
+ threat-weighted target prioritization: concentrate ALL FOUR
84
+ tanks' fire on the rocket soldiers FIRST (each dies in 1-2
85
+ decision turns to 4-vs-1 fire, before it can land more than a
86
+ couple of anti-tank rockets), THEN mop up the chaff. Engaging
87
+ the chaff first — via attack_move auto-targeting the nearer
88
+ rifle screen, or by explicitly attacking the cheap infantry —
89
+ leaves the three rocket soldiers firing through the entire
90
+ mop-up and they whittle the squad below the survival floor.
91
+ A brute attack-move play and a kill-chaff-first play both LOSE;
92
+ the threat-first focus play WINS.
93
+ robotics_analogue: >
94
+ Military target prioritization under fire — when engaging a
95
+ mixed force, the high-threat asset (anti-armour weapon, AA
96
+ battery, command vehicle) is neutralised FIRST, even when it is
97
+ not the nearest target, because it out-damages the cheap escort.
98
+ Killing the low-threat chaff first lets the priority threat keep
99
+ firing and attrits the friendly force. Threat-weighted
100
+ engagement — not nearest-first — preserves the squad.
101
+ benchmark_anchor:
102
+ - "SC2 focus-fire target priority"
103
+ - "threat-weighted engagement"
104
+ - "military target prioritization"
105
+ author: openra-bench
106
+
107
+ base_map: rush-hour-arena
108
+ starting_cash: 0
109
+
110
+ base:
111
+ agent: {faction: allies, cash: 0}
112
+ enemy: {faction: soviet, cash: 0, bot_type: ''}
113
+ tools: [move_units, attack_unit, attack_move, stop]
114
+ planning: true
115
+ # React the instant contact happens or a tank is lost — combat
116
+ # micro is reactive; without these interrupts the focus policy only
117
+ # re-targets on the next turn boundary and the e3s get free shots.
118
+ interrupts:
119
+ enemy_unit_spotted: true
120
+ own_unit_destroyed: true
121
+ termination: {max_ticks: 6000}
122
+ actors: []
123
+
124
+ levels:
125
+ # ── EASY ─────────────────────────────────────────────────────────
126
+ # Bare threat-priority skill: 4 tanks vs a cluster of 9 e1 chaff +
127
+ # 3 e3 high-threat. FORGIVING survival floor — the squad may lose
128
+ # one tank and still win (own_units_gte:3). A staller LOSES on the
129
+ # kill bar via after_ticks; a brute attack-move spreads onto the
130
+ # chaff and the three e3s wipe the squad; kill-chaff-first loses 3
131
+ # tanks to the rockets and busts the floor. Focus-threats-first
132
+ # keeps all four tanks → WIN.
133
+ easy:
134
+ description: >
135
+ Four medium tanks stage at x=48, y=18..21 with your
136
+ construction yard (fact) behind them at the west edge. To the
137
+ east is a mixed enemy cluster: a screen of NINE rifle infantry
138
+ (e1, cheap chaff) at x=66 backed by THREE rocket soldiers (e3,
139
+ anti-tank Dragon launcher — the high-threat units) at x=69.
140
+ Threat-priority doctrine: concentrate ALL FOUR tanks' fire on
141
+ ONE rocket soldier first (kill it in 1-2 decision turns), then
142
+ the second, then the third, THEN mop up the rifle chaff.
143
+ Engaging the chaff first leaves the rocket soldiers firing
144
+ anti-tank rockets through the whole mop-up and they pick off
145
+ your tanks. Win when 12 enemy units are killed AND at least 3
146
+ tanks survive AND your construction yard still stands, before
147
+ tick 2700.
148
+ overrides:
149
+ actors:
150
+ # Strike force — 4 medium tanks staging mid-west.
151
+ - {type: 2tnk, owner: agent, position: [48, 18], stance: 1}
152
+ - {type: 2tnk, owner: agent, position: [48, 19], stance: 1}
153
+ - {type: 2tnk, owner: agent, position: [48, 20], stance: 1}
154
+ - {type: 2tnk, owner: agent, position: [48, 21], stance: 1}
155
+ # Agent construction yard — the non-combat win anchor
156
+ # (building_count_gte:fact:1); also keeps the agent side
157
+ # alive for the evaluator.
158
+ - {type: fact, owner: agent, position: [4, 20]}
159
+ # Enemy cluster — a COMPACT contiguous screen of NINE cheap
160
+ # rifle infantry along x=66, y=15..23 (the NEAREST hostiles
161
+ # ⇒ engine auto-target picks them under attack_move) backed
162
+ # by THREE rocket soldiers at x=69, inside Dragon range 5 of
163
+ # the front line. stance:2 (Defend) — the cluster holds as a
164
+ # stationary wall and auto-fires the moment a tank is in
165
+ # range. The column is compact so the whole cluster stays in
166
+ # the tank squad's vision (no straggler hides in the fog).
167
+ - {type: e1, owner: enemy, position: [66, 15], stance: 2}
168
+ - {type: e1, owner: enemy, position: [66, 16], stance: 2}
169
+ - {type: e1, owner: enemy, position: [66, 17], stance: 2}
170
+ - {type: e1, owner: enemy, position: [66, 18], stance: 2}
171
+ - {type: e1, owner: enemy, position: [66, 19], stance: 2}
172
+ - {type: e1, owner: enemy, position: [66, 20], stance: 2}
173
+ - {type: e1, owner: enemy, position: [66, 21], stance: 2}
174
+ - {type: e1, owner: enemy, position: [66, 22], stance: 2}
175
+ - {type: e1, owner: enemy, position: [66, 23], stance: 2}
176
+ # THREE rocket soldiers — the high-threat units. Tucked
177
+ # behind the chaff screen at x=69 (3 cells back ⇒ not the
178
+ # nearest target, so attack_move auto-picks the chaff) but
179
+ # still inside Dragon range 5 once the tanks reach contact.
180
+ - {type: e3, owner: enemy, position: [69, 19], stance: 2}
181
+ - {type: e3, owner: enemy, position: [69, 20], stance: 2}
182
+ - {type: e3, owner: enemy, position: [69, 21], stance: 2}
183
+ # Far persistent enemy marker — prevents engine auto-done
184
+ # when the cluster falls so the evaluator sees the terminal
185
+ # frame (no DRAW collapse).
186
+ - {type: fact, owner: enemy, position: [120, 20]}
187
+ win_condition:
188
+ all_of:
189
+ - {units_killed_gte: 12}
190
+ - {own_units_gte: 3}
191
+ - {within_ticks: 2700}
192
+ - {building_count_gte: {type: fact, n: 1}}
193
+ # Fail floor aligned to the win floor (own_units_gte:3) — a squad
194
+ # at own=2 is a real LOSS, not a dead-zone DRAW. easy still
195
+ # FORGIVES one tank lost (own=3 wins); the kill-chaff-first play
196
+ # drops to own≤2 and LOSES here.
197
+ fail_condition:
198
+ any_of:
199
+ - {after_ticks: 2701}
200
+ - {not: {own_units_gte: 3}}
201
+ max_turns: 30
202
+
203
+ # ── MEDIUM ───────────────────────────────────────────────────────
204
+ # +1 axis vs easy: a TIGHTER survival floor — the squad must keep
205
+ # ALL FOUR tanks alive (own_units_gte:4, zero loss). The cluster is
206
+ # the same 9 e1 + 3 e3; the controlled variable is purely the
207
+ # zero-loss floor. A brute and a kill-chaff-first play both LOSE:
208
+ # the three e3s firing through the chaff mop-up score ~3 tank kills
209
+ # and bust the zero-loss floor. The intended focus-threats-first
210
+ # WINS — concentrating all four tanks on each rocket soldier in
211
+ # turn ends it in 1-2 decision ticks before it can land more than
212
+ # ~1 rocket, preserving the whole squad.
213
+ medium:
214
+ description: >
215
+ Four medium tanks stage at x=48, y=18..21 with your
216
+ construction yard behind them. The eastern enemy cluster is a
217
+ compact screen of NINE rifle infantry (e1, cheap chaff) at
218
+ x=66 backed by THREE rocket soldiers (e3, anti-tank Dragon
219
+ launcher — the high-threat units) at x=69. Threat-priority
220
+ doctrine: concentrate ALL FOUR tanks' fire on ONE rocket
221
+ soldier first (kill in 1-2 decision turns), then the second,
222
+ then the third, THEN mop up the rifle chaff. Engaging the
223
+ chaff first — via attack_move auto-targeting the nearer rifle
224
+ screen, or by attacking the cheap infantry first — leaves the
225
+ rocket soldiers firing through the entire mop-up and they kill
226
+ your tanks. Win when 12 enemy units are killed AND all four
227
+ tanks survive AND your construction yard still stands, before
228
+ tick 2700.
229
+ overrides:
230
+ actors:
231
+ - {type: 2tnk, owner: agent, position: [48, 18], stance: 1}
232
+ - {type: 2tnk, owner: agent, position: [48, 19], stance: 1}
233
+ - {type: 2tnk, owner: agent, position: [48, 20], stance: 1}
234
+ - {type: 2tnk, owner: agent, position: [48, 21], stance: 1}
235
+ - {type: fact, owner: agent, position: [4, 20]}
236
+ # Compact contiguous screen of NINE cheap rifle infantry
237
+ # along x=66, y=15..23 (engine auto-target picks the nearest
238
+ # hostile under attack_move).
239
+ - {type: e1, owner: enemy, position: [66, 15], stance: 2}
240
+ - {type: e1, owner: enemy, position: [66, 16], stance: 2}
241
+ - {type: e1, owner: enemy, position: [66, 17], stance: 2}
242
+ - {type: e1, owner: enemy, position: [66, 18], stance: 2}
243
+ - {type: e1, owner: enemy, position: [66, 19], stance: 2}
244
+ - {type: e1, owner: enemy, position: [66, 20], stance: 2}
245
+ - {type: e1, owner: enemy, position: [66, 21], stance: 2}
246
+ - {type: e1, owner: enemy, position: [66, 22], stance: 2}
247
+ - {type: e1, owner: enemy, position: [66, 23], stance: 2}
248
+ # THREE rocket soldiers behind the chaff screen at x=69 —
249
+ # inside Dragon range 5 of the contact line, not the nearest
250
+ # target (e1s at x=66 are 3 cells closer ⇒ engine auto-
251
+ # target picks the chaff under attack_move). With 3 e3s
252
+ # firing through the chaff mop-up a brute or chaff-first
253
+ # play takes ~3 tank kills and busts the zero-loss floor.
254
+ - {type: e3, owner: enemy, position: [69, 19], stance: 2}
255
+ - {type: e3, owner: enemy, position: [69, 20], stance: 2}
256
+ - {type: e3, owner: enemy, position: [69, 21], stance: 2}
257
+ - {type: fact, owner: enemy, position: [120, 20]}
258
+ win_condition:
259
+ all_of:
260
+ - {units_killed_gte: 12}
261
+ - {own_units_gte: 4}
262
+ - {within_ticks: 2700}
263
+ - {building_count_gte: {type: fact, n: 1}}
264
+ # Fail floor aligned to the zero-loss win floor (own_units_gte:4)
265
+ # — any tank lost (own≤3) is a real LOSS, not a dead-zone DRAW.
266
+ fail_condition:
267
+ any_of:
268
+ - {after_ticks: 2701}
269
+ - {not: {own_units_gte: 4}}
270
+ max_turns: 30
271
+
272
+ # ── HARD ────────────────────────────────────────────────���────────
273
+ # +1 axis vs medium: the strike force (and its construction yard)
274
+ # stage from TWO seed-driven spawn_point groups (NORTH corridor
275
+ # y=10..13 OR SOUTH corridor y=27..30) so the engagement geometry
276
+ # varies per seed. The chaff screen (10 e1, y=15..24) + rocket trio
277
+ # is symmetric across y=20 — both spawns face the same priority
278
+ # decision (focus one e3 → next → next → mop up chaff). The
279
+ # zero-loss survival floor from medium is retained.
280
+ hard:
281
+ description: >
282
+ Four medium tanks stage at ONE of two corridors (NORTH
283
+ y=10..13 OR SOUTH y=27..30, chosen by seed, anti-
284
+ memorisation) with your construction yard behind them. The
285
+ eastern enemy cluster is a compact screen of TEN rifle
286
+ infantry (e1, cheap chaff) at x=66 backed by THREE rocket
287
+ soldiers (e3, anti-tank — the high-threat units) at x=69.
288
+ Threat-priority doctrine: concentrate ALL FOUR tanks on ONE
289
+ rocket soldier at a time — kill it cleanly, then the next,
290
+ then the third, THEN mop up the rifle chaff. Starting on the
291
+ rifle chaff leaves the rockets firing through the whole
292
+ engagement and they kill your tanks. Win when 13 enemy units
293
+ are killed AND all four tanks survive AND your construction
294
+ yard still stands, before tick 2700.
295
+ overrides:
296
+ actors:
297
+ # spawn_point 0 — NORTH staging corridor (y=10..13).
298
+ - {type: 2tnk, owner: agent, position: [48, 10], stance: 1, spawn_point: 0}
299
+ - {type: 2tnk, owner: agent, position: [48, 11], stance: 1, spawn_point: 0}
300
+ - {type: 2tnk, owner: agent, position: [48, 12], stance: 1, spawn_point: 0}
301
+ - {type: 2tnk, owner: agent, position: [48, 13], stance: 1, spawn_point: 0}
302
+ - {type: fact, owner: agent, position: [4, 11], spawn_point: 0}
303
+ # spawn_point 1 — SOUTH staging corridor (y=27..30).
304
+ - {type: 2tnk, owner: agent, position: [48, 27], stance: 1, spawn_point: 1}
305
+ - {type: 2tnk, owner: agent, position: [48, 28], stance: 1, spawn_point: 1}
306
+ - {type: 2tnk, owner: agent, position: [48, 29], stance: 1, spawn_point: 1}
307
+ - {type: 2tnk, owner: agent, position: [48, 30], stance: 1, spawn_point: 1}
308
+ - {type: fact, owner: agent, position: [4, 29], spawn_point: 1}
309
+ # Compact contiguous screen of TEN cheap rifle infantry along
310
+ # x=66, y=15..24 — symmetric across y=19.5 so either spawn
311
+ # faces an equivalent decision. Engine auto-target picks the
312
+ # nearest hostile under attack_move ⇒ this chaff line is what
313
+ # a brute engages first.
314
+ - {type: e1, owner: enemy, position: [66, 15], stance: 2}
315
+ - {type: e1, owner: enemy, position: [66, 16], stance: 2}
316
+ - {type: e1, owner: enemy, position: [66, 17], stance: 2}
317
+ - {type: e1, owner: enemy, position: [66, 18], stance: 2}
318
+ - {type: e1, owner: enemy, position: [66, 19], stance: 2}
319
+ - {type: e1, owner: enemy, position: [66, 20], stance: 2}
320
+ - {type: e1, owner: enemy, position: [66, 21], stance: 2}
321
+ - {type: e1, owner: enemy, position: [66, 22], stance: 2}
322
+ - {type: e1, owner: enemy, position: [66, 23], stance: 2}
323
+ - {type: e1, owner: enemy, position: [66, 24], stance: 2}
324
+ # THREE rocket soldiers behind the chaff screen at x=69 —
325
+ # inside Dragon range 5 of the contact line, symmetric across
326
+ # y=20 so either spawn faces an equivalent focus-fire
327
+ # decision. Starting on the chaff lets all three fire through
328
+ # the whole mop-up and bust the zero-loss floor.
329
+ - {type: e3, owner: enemy, position: [69, 19], stance: 2}
330
+ - {type: e3, owner: enemy, position: [69, 20], stance: 2}
331
+ - {type: e3, owner: enemy, position: [69, 21], stance: 2}
332
+ - {type: fact, owner: enemy, position: [120, 20]}
333
+ win_condition:
334
+ all_of:
335
+ - {units_killed_gte: 13}
336
+ - {own_units_gte: 4}
337
+ - {within_ticks: 2700}
338
+ - {building_count_gte: {type: fact, n: 1}}
339
+ # Fail floor aligned to the zero-loss win floor (own_units_gte:4)
340
+ # — any tank lost (own≤3) is a real LOSS, not a dead-zone DRAW.
341
+ fail_condition:
342
+ any_of:
343
+ - {after_ticks: 2701}
344
+ - {not: {own_units_gte: 4}}
345
+ max_turns: 30
tests/test_combat_target_priority_highvalue.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """combat-target-priority-highvalue pack — full no-cheat validation.
2
+
3
+ Wave-11 ACTION pack: threat-weighted target prioritization (SC2
4
+ focus-fire target priority / military target prioritization anchor).
5
+ A 4-tank squad faces a mixed enemy cluster — a screen of cheap rifle
6
+ chaff (e1) backed by THREE high-threat anti-armour rocket soldiers
7
+ (e3). The squad must FOCUS THE ROCKET SOLDIERS FIRST: 4-vs-1
8
+ concentrated fire deletes each e3 in 1-2 decision turns before it can
9
+ land more than ~1 rocket. Killing the chaff first leaves all three
10
+ rockets firing through the entire mop-up and they whittle the squad
11
+ below the survival floor.
12
+
13
+ Win = units_killed_gte:K AND own_units_gte:F AND within_ticks:2700
14
+ AND building_count_gte:fact:1 (a non-combat anchor: the squad must
15
+ still own its construction yard while it focus-fires). Fail floors
16
+ are aligned to the win floor so every non-timeout outcome is a real
17
+ WIN or LOSS — no dead-zone DRAW.
18
+
19
+ Bar (per CLAUDE.md), verified deterministic across seeds 1-4 on
20
+ every level:
21
+ • stall (observe only) → LOSS every level/seed (after_ticks).
22
+ • brute attack_move → LOSS (auto-targets the chaff screen;
23
+ the three e3s wipe the squad).
24
+ • kill-chaff-first → LOSS (rockets fire through the chaff
25
+ mop-up and bust the survival floor).
26
+ • focus-threats-first → WIN every level/seed (all three e3s
27
+ silenced inside the first ~3 turns, squad preserved).
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import pytest
33
+
34
+ pytest.importorskip("openra_train", reason="Rust env wheel not installed")
35
+
36
+ from openra_bench.eval_core import run_level
37
+ from openra_bench.scenarios import load_pack
38
+ from openra_bench.scenarios.loader import PACKS_DIR, compile_level
39
+
40
+ PACK = PACKS_DIR / "combat-target-priority-highvalue.yaml"
41
+ LEVELS = ("easy", "medium", "hard")
42
+ SEEDS = (1, 2, 3, 4)
43
+ # enemy cluster advance target — the chaff column, mid-y.
44
+ _ADV = (66, 20)
45
+
46
+
47
+ # ── helpers ───────────────────────────────────────────────────────
48
+
49
+
50
+ def _tank_ids(obs):
51
+ return [
52
+ str(u["id"])
53
+ for u in (obs.get("units_summary") or [])
54
+ if str(u.get("type", "")).lower() == "2tnk"
55
+ ]
56
+
57
+
58
+ def _enemy_units(obs):
59
+ """(id, type) for every visible enemy combat unit."""
60
+ out = []
61
+ for e in (obs.get("enemy_summary") or []):
62
+ t = str(e.get("type", "")).lower()
63
+ if t in ("e1", "e3"):
64
+ out.append((str(e.get("id")), t))
65
+ return out
66
+
67
+
68
+ # ── policies ──────────────────────────────────────────────────────
69
+
70
+
71
+ def _stall_policy():
72
+ """Do nothing — the kill bar is never met, after_ticks LOSS."""
73
+ def pol(obs, Cmd):
74
+ return [Cmd.observe()]
75
+ return pol
76
+
77
+
78
+ def _brute_policy():
79
+ """attack_move onto the cluster centroid — the engine auto-targets
80
+ the NEAREST hostile (the chaff screen), so the three e3s fire
81
+ through the whole engagement and wipe the squad."""
82
+ def pol(obs, Cmd):
83
+ ids = _tank_ids(obs)
84
+ if not ids:
85
+ return [Cmd.observe()]
86
+ es = [
87
+ e for e in (obs.get("enemy_summary") or [])
88
+ if str(e.get("type", "")).lower() in ("e1", "e3")
89
+ ]
90
+ if es:
91
+ cx = sum(e["cell_x"] for e in es) // len(es)
92
+ cy = sum(e["cell_y"] for e in es) // len(es)
93
+ return [Cmd.attack_move(ids, cx, cy)]
94
+ return [Cmd.attack_move(ids, _ADV[0], _ADV[1])]
95
+ return pol
96
+
97
+
98
+ def _focus_policy(first: str):
99
+ """attack_unit, prioritising the `first` unit type. first='e1' is
100
+ the kill-chaff-first trap; first='e3' is the intended threat-first
101
+ focus play."""
102
+ def pol(obs, Cmd):
103
+ ids = _tank_ids(obs)
104
+ if not ids:
105
+ return [Cmd.observe()]
106
+ es = _enemy_units(obs)
107
+ prio = [e for e in es if e[1] == first]
108
+ rest = [e for e in es if e[1] != first]
109
+ if prio:
110
+ return [Cmd.attack_unit(ids, prio[0][0])]
111
+ if rest:
112
+ return [Cmd.attack_unit(ids, rest[0][0])]
113
+ # no enemy in view — close to contact range.
114
+ return [Cmd.attack_move(ids, _ADV[0], _ADV[1])]
115
+ return pol
116
+
117
+
118
+ # ── tests ─────────────────────────────────────────────────────────
119
+
120
+
121
+ @pytest.mark.parametrize("level", LEVELS)
122
+ @pytest.mark.parametrize("seed", SEEDS)
123
+ def test_stall_loses(level, seed):
124
+ """A do-nothing policy must LOSE on the deadline — no draw."""
125
+ c = compile_level(load_pack(PACK), level)
126
+ res = run_level(c, _stall_policy(), seed=seed)
127
+ assert res.outcome == "loss", (
128
+ f"{level}/seed{seed}: stall must LOSE, got {res.outcome}"
129
+ )
130
+
131
+
132
+ @pytest.mark.parametrize("level", LEVELS)
133
+ @pytest.mark.parametrize("seed", SEEDS)
134
+ def test_brute_attack_move_loses(level, seed):
135
+ """A brute attack-move play auto-targets the chaff screen; the
136
+ three rocket soldiers fire through the engagement and wipe the
137
+ squad — a real LOSS, not a draw."""
138
+ c = compile_level(load_pack(PACK), level)
139
+ res = run_level(c, _brute_policy(), seed=seed)
140
+ assert res.outcome == "loss", (
141
+ f"{level}/seed{seed}: brute must LOSE, got {res.outcome}"
142
+ )
143
+
144
+
145
+ @pytest.mark.parametrize("level", LEVELS)
146
+ @pytest.mark.parametrize("seed", SEEDS)
147
+ def test_kill_chaff_first_loses(level, seed):
148
+ """Explicitly attacking the cheap e1 chaff first leaves the three
149
+ rockets firing through the whole mop-up; the squad drops below the
150
+ survival floor — a real LOSS."""
151
+ c = compile_level(load_pack(PACK), level)
152
+ res = run_level(c, _focus_policy("e1"), seed=seed)
153
+ assert res.outcome == "loss", (
154
+ f"{level}/seed{seed}: kill-chaff-first must LOSE, got {res.outcome}"
155
+ )
156
+
157
+
158
+ @pytest.mark.parametrize("level", LEVELS)
159
+ @pytest.mark.parametrize("seed", SEEDS)
160
+ def test_focus_threats_first_wins(level, seed):
161
+ """The intended capability — concentrate all four tanks on the
162
+ rocket soldiers FIRST — must WIN every level and seed."""
163
+ c = compile_level(load_pack(PACK), level)
164
+ res = run_level(c, _focus_policy("e3"), seed=seed)
165
+ assert res.outcome == "win", (
166
+ f"{level}/seed{seed}: focus-threats-first must WIN, got {res.outcome}"
167
+ )
168
+
169
+
170
+ def test_hard_agent_spawn_axis_has_two_groups():
171
+ """The hard tier must define ≥2 agent-side spawn_point groups
172
+ (the seed-driven staging-corridor axis); the strike force and its
173
+ construction yard are duplicated under each group."""
174
+ c = compile_level(load_pack(PACK), "hard")
175
+ agent_sps = {
176
+ a.spawn_point
177
+ for a in c.scenario.actors
178
+ if a.owner == "agent" and a.spawn_point is not None
179
+ }
180
+ assert len(agent_sps) >= 2, (
181
+ f"hard needs ≥2 agent spawn_point groups, got {sorted(agent_sps)}"
182
+ )
183
+
184
+
185
+ def test_tick_budget_alignment():
186
+ """within_ticks / after_ticks must be reachable inside max_turns
187
+ (tick ≤ 93 + 90·(max_turns-1)) so the deadline actually bites."""
188
+ for level in LEVELS:
189
+ c = compile_level(load_pack(PACK), level)
190
+ max_tick = 93 + 90 * (c.max_turns - 1)
191
+ win_clauses = c.win_condition.all_of or []
192
+ within = next(
193
+ (cl["within_ticks"] for cl in win_clauses if "within_ticks" in cl),
194
+ None,
195
+ )
196
+ assert within is not None and within <= max_tick, (
197
+ f"{level}: within_ticks {within} not reachable by {max_tick}"
198
+ )
199
+ fail_clauses = c.fail_condition.any_of or []
200
+ after = next(
201
+ (cl["after_ticks"] for cl in fail_clauses if "after_ticks" in cl),
202
+ None,
203
+ )
204
+ assert after is not None and after <= max_tick, (
205
+ f"{level}: after_ticks {after} not reachable by {max_tick}"
206
+ )