yxc20098 commited on
Commit
1544546
·
1 Parent(s): 92a89ff

fix(scenario): combat-suicide-charge-mission — recalibrate after engine movement fixes

Browse files
openra_bench/scenarios/packs/combat-suicide-charge-mission.yaml CHANGED
@@ -5,17 +5,22 @@ meta:
5
  real_world_meaning: >
6
  Forlorn hope / military sacrifice doctrine. A high-value enemy
7
  construction yard (`fact`) sits at the far east of the map,
8
- heavily defended; the agent's small force at the west cannot
9
- survive the engagement. The OBJECTIVE is the building's
10
- destruction survival is NOT required. A "preserve the force"
11
- policy that engages carefully and tries to save units loses on
12
- the clock (the picket grinds attackers down and they never
13
- arrive); only an all-in commit that drives the WHOLE force
14
- straight at the objective and trades every attacker for damage
15
- on the fact actually razes it in time. This is delayed-terminal
16
- credit assignment with an explicit cost-vs-objective trade-off:
17
- every unit lost looks locally negative, but the only successful
18
- plan accepts the loss of all of them.
 
 
 
 
 
19
  robotics_analogue: >
20
  Expendable strike package / single-use intervention: a fleet of
21
  cheap drones must destroy a critical adversary asset under a
@@ -32,25 +37,36 @@ meta:
32
  base_map: rush-hour-arena
33
 
34
  # The discriminator: the enemy fact at (110,20) is guarded by an
35
- # anti-armor picket — an e3 rocket-infantry WALL forward of the
36
- # objective plus a 4tnk heavy tank held back at x=108 guarding the
37
- # fact. A careful "preserve the force" approach (skirmish, retreat,
38
- # regroup, pick off targets) cannot land enough damage on the fact
39
- # before the deadline — the e3 wall outranges and out-damages the
40
- # 2tnk column in a stand-off, and any retreat resets the clock. The
 
 
41
  # intended play commits ALL six tanks plus the four e3 escorts
42
- # straight east, trading the column through the e3 wall and pushing
43
- # the survivors onto the fact. Win is buildings-only; force wipe is
44
- # NOT a fail.
45
  #
46
- # Engine balance pass (armor-class weapon selection + stance fixes):
47
- # the post-fix 4tnk co-located with the e3 wall right next to the
48
- # fact over-killed the strike force before it could reach the
49
- # objective (all-in DREW). Re-tuned: the e3 wall sits forward (easy
50
- # x=104 / medium x=102 / hard x=100) so the strike force trades
51
- # through it with push-room to spare, and the 4tnk is pulled back to
52
- # x=108 every tier so it brawls only the survivors. Picket density
53
- # graduates 2→3→4 e3 across easy→medium→hard.
 
 
 
 
 
 
 
 
 
54
  base:
55
  agent: {faction: allies, cash: 0}
56
  enemy: {faction: soviet, cash: 0, bot_type: guard}
@@ -83,21 +99,18 @@ base:
83
  - {type: e3, owner: agent, position: [8, 22], stance: 1, count: 2}
84
  # ── Defender — `guard` bot, leashed (lunge then snap back). e3
85
  # rockets shred 2tnk armour at range 5; 4tnk heavy brawls close.
86
- # Medium picket = a 3-e3 anti-armor WALL at x=102 (forward of the
87
- # objective so the strike force trades through it while it still
88
- # has push-room) + 1 4tnk HP-sponge at x=108 guarding the fact.
89
- # Spread the e3 wall vertically (y=16/20/24) to cover the west
90
- # approach corridor an all-in commit still cracks it but trades
91
- # roughly half the force in doing so. On EASY, lighter picket
92
- # (2 e3 at x=104); on HARD, denser (4 e3 at x=100). The 4tnk is
93
- # held back at x=108 every tier so it brawls only what survives
94
- # the e3 wall, not the whole column at once (engine balance pass:
95
- # the post-armor-fix 4tnk co-located with the e3 wall over-killed
96
- # the strike force before it could reach the fact).
97
- - {type: e3, owner: enemy, position: [102, 16], stance: 2}
98
- - {type: e3, owner: enemy, position: [102, 20], stance: 2}
99
- - {type: e3, owner: enemy, position: [102, 24], stance: 2}
100
- - {type: 4tnk, owner: enemy, position: [108, 20], stance: 2}
101
  # Objective: the high-value enemy fact at far east (110, 20).
102
  - {type: fact, owner: enemy, position: [110, 20]}
103
  # Sentinel fact — far north corner, never engaged. Keeps the
@@ -107,19 +120,21 @@ base:
107
  - {type: fact, owner: enemy, position: [122, 4]}
108
 
109
  levels:
110
- # ── EASY ── the bare sacrifice skill: light defender (only 2× e3
111
- # + 1× 4tnk). A preserve-the-force plan still times out, but the
112
- # all-in commit razes the fact comfortably.
 
113
  easy:
114
  description: >
115
  A defended enemy construction yard sits at the far east
116
  (110, 20). Your strike package (6 tanks + 4 rocket infantry)
117
- at the west cannot survive the engagement intact. You DO
118
- NOT need to keep your units alive — the objective is the
119
- enemy fact's DESTRUCTION before the deadline. A careful,
120
- preserve-the-force approach times out on the clock; commit
121
- the WHOLE force straight east at the objective, accept the
122
- losses, and raze the fact.
 
123
  overrides:
124
  actors:
125
  - {type: 2tnk, owner: agent, position: [6, 16], stance: 1, count: 2}
@@ -127,9 +142,9 @@ levels:
127
  - {type: 2tnk, owner: agent, position: [6, 24], stance: 1, count: 2}
128
  - {type: e3, owner: agent, position: [8, 18], stance: 1, count: 2}
129
  - {type: e3, owner: agent, position: [8, 22], stance: 1, count: 2}
130
- - {type: e3, owner: enemy, position: [104, 18], stance: 2}
131
- - {type: e3, owner: enemy, position: [104, 22], stance: 2}
132
- - {type: 4tnk, owner: enemy, position: [108, 20], stance: 2}
133
  - {type: fact, owner: enemy, position: [110, 20]}
134
  - {type: fact, owner: enemy, position: [122, 4]}
135
  win_condition:
@@ -145,16 +160,18 @@ levels:
145
  # ── MEDIUM ── +1 controlled variable: heavier defender (3× e3
146
  # wall + 1× 4tnk — the base composition). The preserve-the-force
147
  # trap tightens: any standoff loses ground; the all-in commit
148
- # still wins but trades roughly half the package through the wall.
 
149
  medium:
150
  description: >
151
  Heavier picket (a 3-rocket-infantry wall spread y=16/20/24 at
152
- x=102 + 1 heavy tank held back at x=108) guarding the enemy
153
- fact at (110, 20). Same task: raze the fact before the
154
- deadline; survival is NOT required. The careful,
155
  preserve-the-force approach reliably times out — the e3
156
- outrange your tanks in any standoff. Commit everything,
157
- all-in, straight at the objective.
 
158
  overrides: {}
159
  win_condition:
160
  all_of:
@@ -168,17 +185,19 @@ levels:
168
 
169
  # ── HARD ── +1 controlled variable: seed-chosen staging latitude
170
  # (two spawn_point groups, NORTH y=10 / SOUTH y=30) so the all-in
171
- # vector flips per seed and can't be memorised; picket adds one
172
- # more e3 (heavy on top of the heavy-2tnk pair). Tight deadline.
 
173
  hard:
174
  description: >
175
- Strongest defender on the far-east enemy fact at (110, 20);
176
- your strike package stages from a seed-chosen latitude
177
- (NORTH or SOUTH) so the all-in vector varies per seed. Raze
178
- the fact before the (tight) deadline. Survival is NOT
179
- required — every unit may be spent. The careful preserve
180
- plan and any partial commit both time out; only the
181
- whole-force all-in commit lands enough damage on the fact.
 
182
  overrides:
183
  actors:
184
  # spawn_point 0 — NORTH staging (y=8..12)
@@ -195,17 +214,17 @@ levels:
195
  - {type: e3, owner: agent, position: [8, 31], stance: 1, count: 2, spawn_point: 1}
196
  # Defender — symmetric picket on the objective fact (enemy
197
  # actors don't honour spawn_point — see CLAUDE.md; the same
198
- # picket meets either spawn). Hard's e3 anti-armor wall is the
199
- # densest (4 e3, one per latitude y=14/18/22/26) and the
200
- # furthest forward (x=100) so a flank-staged strike package
201
- # still trades through it with push-room left for the fact.
202
- # The 4tnk HP-sponge is held back at x=108 (guards the fact;
203
- # brawls only the survivors) same idiom as easy/medium.
204
- - {type: e3, owner: enemy, position: [100, 14], stance: 2}
205
- - {type: e3, owner: enemy, position: [100, 18], stance: 2}
206
- - {type: e3, owner: enemy, position: [100, 22], stance: 2}
207
- - {type: e3, owner: enemy, position: [100, 26], stance: 2}
208
- - {type: 4tnk, owner: enemy, position: [108, 20], stance: 2}
209
  - {type: fact, owner: enemy, position: [110, 20]}
210
  - {type: fact, owner: enemy, position: [122, 4]}
211
  win_condition:
 
5
  real_world_meaning: >
6
  Forlorn hope / military sacrifice doctrine. A high-value enemy
7
  construction yard (`fact`) sits at the far east of the map,
8
+ defended by an anti-armor picket; the agent's small strike
9
+ package at the west cannot punch through without heavy losses.
10
+ The OBJECTIVE is the building's destruction keeping the force
11
+ intact is NOT required and is not achievable. A "preserve the
12
+ force" policy that engages carefully and tries to save units
13
+ loses on the clock (the picket grinds the lead element down and
14
+ the rest never arrive); a half-commit that advances and halts
15
+ short of the objective also times out. Only an all-in commit
16
+ that drives the WHOLE force decisively at the objective and
17
+ focus-fires through the picket trading most of the attackers
18
+ for the kill actually razes the fact in time. This is
19
+ delayed-terminal credit assignment with an explicit
20
+ cost-vs-objective trade-off: every unit lost looks locally
21
+ negative, but the only successful plan accepts that the bulk of
22
+ the force is spent and only a remnant survives to land the
23
+ killing blow.
24
  robotics_analogue: >
25
  Expendable strike package / single-use intervention: a fleet of
26
  cheap drones must destroy a critical adversary asset under a
 
37
  base_map: rush-hour-arena
38
 
39
  # The discriminator: the enemy fact at (110,20) is guarded by an
40
+ # anti-armor picket — an e3 rocket-infantry WALL just west of the
41
+ # objective (x=106) plus a 4tnk heavy tank guarding the fact close
42
+ # in. A careful "preserve the force" approach (lead probe, hold the
43
+ # rest, retreat the damaged) cannot land enough damage on the fact
44
+ # before the deadline — the e3 wall outranges and out-damages a
45
+ # single probing 2tnk, and the held-back reserve never engages. A
46
+ # half-commit that advances the whole force but halts SHORT of the
47
+ # objective bleeds units in the open and likewise times out. The
48
  # intended play commits ALL six tanks plus the four e3 escorts
49
+ # straight east, focus-fires (`attack_unit`) through the e3 wall and
50
+ # the 4tnk, and pushes the remnant onto the fact. Win is
51
+ # buildings-only.
52
  #
53
+ # Engine recalibration (movement fixes: moving units take fire en
54
+ # route, `attack_unit` on out-of-sight targets paths normally, no
55
+ # sprint-invincibility). Two consequences had to be designed
56
+ # around: (1) the engine auto-`done`s the episode the moment the
57
+ # AGENT force is fully wiped (a true total-sacrifice collapses to a
58
+ # DRAW, not a win-by-objective) so the all-in must focus-fire hard
59
+ # enough to keep a remnant alive long enough to raze the fact;
60
+ # (2) the engine also auto-`done`s when the last enemy COMBAT unit
61
+ # dies — the SENTINEL fact (an enemy building) keeps the episode
62
+ # alive past picket elimination so within_ticks evaluates on the
63
+ # terminal frame. Re-tuned picket: a 2/3/3-e3 wall at x=106 graded
64
+ # easy→medium→hard, with the 4tnk close in front of the fact (x=109
65
+ # easy/medium; x=112 on hard, where the seed-flank staging needs a
66
+ # flank-robust win). The all-in trades through the picket losing the
67
+ # bulk of the force (≈4 easy / ≈6 medium / a near-total loss on
68
+ # hard) but a remnant razes the fact in budget; stall, careful
69
+ # preserve, and half-commit all time out as real LOSSES.
70
  base:
71
  agent: {faction: allies, cash: 0}
72
  enemy: {faction: soviet, cash: 0, bot_type: guard}
 
99
  - {type: e3, owner: agent, position: [8, 22], stance: 1, count: 2}
100
  # ── Defender — `guard` bot, leashed (lunge then snap back). e3
101
  # rockets shred 2tnk armour at range 5; 4tnk heavy brawls close.
102
+ # Medium picket = a 3-e3 anti-armor WALL at x=106 (spread
103
+ # vertically y=16/20/24 across the approach corridor) + 1 4tnk
104
+ # HP-sponge at x=109 close in front of the objective fact. The
105
+ # all-in commit focus-fires through the wall losing roughly half
106
+ # the force; a probing / standoff / preserve play cannot crack
107
+ # it. On EASY, lighter picket (2 e3 at x=106); on HARD, the
108
+ # 3-e3 wall plus the 4tnk pulled back to x=112 (behind the fact)
109
+ # so the win is robust to the seed-flank staging latitude.
110
+ - {type: e3, owner: enemy, position: [106, 16], stance: 2}
111
+ - {type: e3, owner: enemy, position: [106, 20], stance: 2}
112
+ - {type: e3, owner: enemy, position: [106, 24], stance: 2}
113
+ - {type: 4tnk, owner: enemy, position: [109, 20], stance: 2}
 
 
 
114
  # Objective: the high-value enemy fact at far east (110, 20).
115
  - {type: fact, owner: enemy, position: [110, 20]}
116
  # Sentinel fact — far north corner, never engaged. Keeps the
 
120
  - {type: fact, owner: enemy, position: [122, 4]}
121
 
122
  levels:
123
+ # ── EASY ── the bare sacrifice skill: light defender (2× e3
124
+ # wall at x=106 + 1× 4tnk at x=109). A preserve-the-force plan
125
+ # times out, but the all-in commit razes the fact, losing roughly
126
+ # four of its ten units doing so.
127
  easy:
128
  description: >
129
  A defended enemy construction yard sits at the far east
130
  (110, 20). Your strike package (6 tanks + 4 rocket infantry)
131
+ at the west cannot punch through the picket without heavy
132
+ losses. The objective is the enemy fact's DESTRUCTION before
133
+ the deadline keeping the force intact is neither required
134
+ nor possible. A careful, preserve-the-force approach times
135
+ out on the clock; so does advancing and halting short of the
136
+ objective. Commit the WHOLE force straight east, focus-fire
137
+ through the picket, accept the losses, and raze the fact.
138
  overrides:
139
  actors:
140
  - {type: 2tnk, owner: agent, position: [6, 16], stance: 1, count: 2}
 
142
  - {type: 2tnk, owner: agent, position: [6, 24], stance: 1, count: 2}
143
  - {type: e3, owner: agent, position: [8, 18], stance: 1, count: 2}
144
  - {type: e3, owner: agent, position: [8, 22], stance: 1, count: 2}
145
+ - {type: e3, owner: enemy, position: [106, 17], stance: 2}
146
+ - {type: e3, owner: enemy, position: [106, 23], stance: 2}
147
+ - {type: 4tnk, owner: enemy, position: [109, 20], stance: 2}
148
  - {type: fact, owner: enemy, position: [110, 20]}
149
  - {type: fact, owner: enemy, position: [122, 4]}
150
  win_condition:
 
160
  # ── MEDIUM ── +1 controlled variable: heavier defender (3× e3
161
  # wall + 1× 4tnk — the base composition). The preserve-the-force
162
  # trap tightens: any standoff loses ground; the all-in commit
163
+ # still wins but trades roughly six of its ten units through the
164
+ # wall.
165
  medium:
166
  description: >
167
  Heavier picket (a 3-rocket-infantry wall spread y=16/20/24 at
168
+ x=106 + 1 heavy tank at x=109) guarding the enemy fact at
169
+ (110, 20). Same task: raze the fact before the deadline;
170
+ keeping the force intact is not required. The careful,
171
  preserve-the-force approach reliably times out — the e3
172
+ outrange your tanks in any standoff and so does halting
173
+ short of the objective. Commit everything, all-in,
174
+ focus-fire through the wall, and raze the fact.
175
  overrides: {}
176
  win_condition:
177
  all_of:
 
185
 
186
  # ── HARD ── +1 controlled variable: seed-chosen staging latitude
187
  # (two spawn_point groups, NORTH y=10 / SOUTH y=30) so the all-in
188
+ # vector flips per seed and can't be memorised. Same 3-e3 wall as
189
+ # medium but the 4tnk is pulled back behind the fact (x=112) so
190
+ # the win stays robust to the flank. Tight deadline.
191
  hard:
192
  description: >
193
+ Defended far-east enemy fact at (110, 20); your strike
194
+ package stages from a seed-chosen latitude (NORTH or SOUTH)
195
+ so the all-in vector varies per seed. Raze the fact before
196
+ the (tight) deadline. Keeping the force intact is not
197
+ required — the bulk of it will be spent. The careful preserve
198
+ plan and any partial / halt-short commit both time out; only
199
+ the whole-force all-in commit, focus-firing through the
200
+ picket, lands the killing blow on the fact.
201
  overrides:
202
  actors:
203
  # spawn_point 0 — NORTH staging (y=8..12)
 
214
  - {type: e3, owner: agent, position: [8, 31], stance: 1, count: 2, spawn_point: 1}
215
  # Defender — symmetric picket on the objective fact (enemy
216
  # actors don't honour spawn_point — see CLAUDE.md; the same
217
+ # picket meets either spawn). Hard's e3 anti-armor wall is a
218
+ # 3-e3 wall at x=106 (y=16/20/24) the same wall as medium —
219
+ # but the 4tnk HP-sponge is pulled BACK to x=112 (behind the
220
+ # objective fact). That keeps the all-in WIN robust to the
221
+ # seed-flank staging latitude (NORTH/SOUTH): a flank-staged
222
+ # strike package focus-fires the wall and razes the fact
223
+ # without the 4tnk over-killing it from in front first.
224
+ - {type: e3, owner: enemy, position: [106, 16], stance: 2}
225
+ - {type: e3, owner: enemy, position: [106, 20], stance: 2}
226
+ - {type: e3, owner: enemy, position: [106, 24], stance: 2}
227
+ - {type: 4tnk, owner: enemy, position: [112, 20], stance: 2}
228
  - {type: fact, owner: enemy, position: [110, 20]}
229
  - {type: fact, owner: enemy, position: [122, 4]}
230
  win_condition:
tests/test_combat_suicide_charge_mission.py CHANGED
@@ -1,22 +1,39 @@
1
  """combat-suicide-charge-mission — sacrifice the force to destroy a
2
  high-value enemy objective (forlorn hope / military sacrifice doctrine).
3
 
4
- The bar: an all-in commit that drives the WHOLE force straight at the
5
- enemy fact (110, 20), accepting total force loss, WINS on every level
6
- and every hard seed (1..4). STALL (only observe) and PRESERVE-FORCE
7
- (only the lead tank probes, the rest hold) both LOSE on every level —
8
- non-win is a real reachable timeout LOSS via the `after_ticks` fail
9
- clause (no own_units_gte:1 clause; total force loss is NOT a fail,
10
- this is the whole point of the sacrifice anchor).
11
-
12
- Recalibrated after the engine balance pass (armor-class weapon
13
- selection + stance fixes): the post-fix 4tnk co-located with the e3
14
- wall over-killed the strike force before it could reach the fact,
15
- collapsing the all-in commit to a DRAW on medium/hard. The picket
16
- was re-tuned — the e3 anti-armor wall sits forward of the objective
17
- (easy x=104 / medium x=102 / hard x=100, density 2/3/4 e3) and the
18
- 4tnk HP-sponge is pulled back to x=108 to brawl only the survivors —
19
- restoring the all-in WIN while keeping stall + preserve-force LOSS.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  Validation is scripted (no model / network).
22
  """
@@ -332,12 +349,15 @@ def _stall(rs, Command):
332
 
333
 
334
  def _all_in(rs, Command):
335
- """Commit-all: every agent unit drives straight at the objective
336
- fact at (110, 20). When any enemy comes into vision the whole
337
- force attack_unit's the closest threat, switching to the
338
- objective fact the moment it appears in vision. Tanks + rocket
339
- infantry absorb fire and trade themselves for damage on the
340
- fact. The intended sacrifice play."""
 
 
 
341
  units = rs.get("units_summary", []) or []
342
  if not units:
343
  return [Command.observe()]
@@ -351,7 +371,7 @@ def _all_in(rs, Command):
351
  t = (e.get("type") or "").lower()
352
  if t == "fact" and int(e.get("cell_x", 200)) < 115:
353
  return [Command.attack_unit(ids, str(e["id"]))]
354
- # Otherwise smash the nearest defender first.
355
  tx = sum(int(u["cell_x"]) for u in units) / len(units)
356
  ty = sum(int(u["cell_y"]) for u in units) / len(units)
357
  enemies.sort(
@@ -359,7 +379,21 @@ def _all_in(rs, Command):
359
  + (int(e.get("cell_y", 0)) - ty) ** 2
360
  )
361
  return [Command.attack_unit(ids, str(enemies[0]["id"]))]
362
- return [Command.attack_move(ids, 110, 20)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
 
364
 
365
  def _preserve_force(rs, Command):
@@ -441,3 +475,24 @@ def test_preserve_force_loses(level, seed):
441
  f"(lost={r.signals.units_lost}, "
442
  f"bldgs={r.signals.enemy_buildings_destroyed_types})"
443
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """combat-suicide-charge-mission — sacrifice the force to destroy a
2
  high-value enemy objective (forlorn hope / military sacrifice doctrine).
3
 
4
+ The bar: an all-in commit that drives the WHOLE force decisively at the
5
+ enemy fact (110, 20), focus-firing through the picket and accepting the
6
+ loss of the bulk of the force, WINS on every level and every hard seed
7
+ (1..4). STALL (only observe), PRESERVE-FORCE (only the lead tank
8
+ probes, the rest hold), and STANDOFF (the whole force advances but
9
+ halts short of the objective) all LOSE on every level — non-win is a
10
+ real reachable timeout LOSS via the `after_ticks` fail clause.
11
+
12
+ Recalibrated after the engine movement fixes (moving units take fire
13
+ en route; `attack_unit` on out-of-sight targets paths normally; no
14
+ sprint-invincibility). Two engine behaviours had to be designed
15
+ around both surfaced during this recalibration:
16
+
17
+ * The engine auto-`done`s the episode the moment the AGENT force is
18
+ fully wiped, REGARDLESS of `termination.agent_units_killed`. A
19
+ literal total-sacrifice therefore collapses to a DRAW (the
20
+ `after_ticks` deadline never bites), not a win-by-objective. The
21
+ picket is tuned so the all-in commit focus-fires hard enough to
22
+ keep a remnant alive long enough to raze the fact (loses ≈4 on
23
+ easy / ≈6 on medium / a near-total loss on hard).
24
+ * The engine also auto-`done`s when the last enemy COMBAT unit dies;
25
+ the far-corner SENTINEL fact (an enemy building) keeps the episode
26
+ alive past picket elimination so `within_ticks` evaluates on the
27
+ terminal frame.
28
+
29
+ Picket re-tuned: a 2/3/3-e3 anti-armor wall at x=106 graded
30
+ easy→medium→hard, with the 4tnk close in front of the fact (x=109 on
31
+ easy/medium; x=112 on hard, where the seed-flank staging needs a
32
+ flank-robust win). A `move`-beeline policy is NOT used as a losing
33
+ probe: a plain move onto the objective cell trips the engine
34
+ auto-`done` and collapses to a DRAW (not a real LOSS), so the losing
35
+ probes are stall / careful-preserve / halt-short — all of which leave
36
+ the picket alive and the fact intact and time out as real LOSSES.
37
 
38
  Validation is scripted (no model / network).
39
  """
 
349
 
350
 
351
  def _all_in(rs, Command):
352
+ """Commit-all: every agent unit drives decisively at the
353
+ objective. When any enemy comes into vision the whole force
354
+ `attack_unit`'s a single target focus-firing — and switches to
355
+ the objective fact the moment it appears. Until contact the force
356
+ `attack_move`s to a staging cell SHORT of the fact (106, 20):
357
+ advancing onto the fact's own cell (110, 20) would trip the
358
+ engine auto-`done`. Tanks + rocket infantry absorb fire and
359
+ trade the bulk of the force for the kill on the fact. The
360
+ intended sacrifice play."""
361
  units = rs.get("units_summary", []) or []
362
  if not units:
363
  return [Command.observe()]
 
371
  t = (e.get("type") or "").lower()
372
  if t == "fact" and int(e.get("cell_x", 200)) < 115:
373
  return [Command.attack_unit(ids, str(e["id"]))]
374
+ # Otherwise focus-fire the single nearest defender.
375
  tx = sum(int(u["cell_x"]) for u in units) / len(units)
376
  ty = sum(int(u["cell_y"]) for u in units) / len(units)
377
  enemies.sort(
 
379
  + (int(e.get("cell_y", 0)) - ty) ** 2
380
  )
381
  return [Command.attack_unit(ids, str(enemies[0]["id"]))]
382
+ return [Command.attack_move(ids, 106, 20)]
383
+
384
+
385
+ def _standoff(rs, Command):
386
+ """Half-commit / brute-but-timid: the WHOLE force advances east
387
+ with `attack_move` but halts short of the objective at (90, 20).
388
+ The units bleed in the open against the picket's reach but never
389
+ press onto the fact; the fact never takes damage, the clock runs
390
+ out → LOSS. Discriminates a full decisive commit from a partial
391
+ advance."""
392
+ units = rs.get("units_summary", []) or []
393
+ if not units:
394
+ return [Command.observe()]
395
+ ids = [str(u["id"]) for u in units]
396
+ return [Command.attack_move(ids, 90, 20)]
397
 
398
 
399
  def _preserve_force(rs, Command):
 
475
  f"(lost={r.signals.units_lost}, "
476
  f"bldgs={r.signals.enemy_buildings_destroyed_types})"
477
  )
478
+
479
+
480
+ @pytest.mark.parametrize("level", ["easy", "medium", "hard"])
481
+ @pytest.mark.parametrize("seed", [1, 2, 3, 4])
482
+ def test_standoff_loses(level, seed):
483
+ """Half-commit / brute-but-timid: the whole force advances east
484
+ but halts short of the objective at (90, 20). The force bleeds in
485
+ the open but never presses onto the fact; the fact never takes
486
+ damage, clock expires → LOSS. Discriminates a decisive full
487
+ commit from a partial advance."""
488
+ pytest.importorskip("openra_train")
489
+ from openra_bench.eval_core import run_level
490
+
491
+ c = compile_level(load_pack(PACK_PATH), level)
492
+ r = run_level(c, _standoff, seed=seed)
493
+ assert r.outcome == "loss", (
494
+ f"{level} seed={seed}: standoff (halt-short) must be a real "
495
+ f"timeout LOSS (fact never takes damage), got {r.outcome} "
496
+ f"(lost={r.signals.units_lost}, "
497
+ f"bldgs={r.signals.enemy_buildings_destroyed_types})"
498
+ )