yxc20098 commited on
Commit
17a49cd
·
1 Parent(s): 4aa42cb

fix(scenario): combat-hold-chokepoint — recalibrate after engine movement fixes

Browse files

The OpenRA-Rust movement fixes (moving units fire and take fire en
route; attack_unit on out-of-sight targets paths normally) made a
static auto-firing squad strong enough that the easy tier was
beatable by a pure stall: the pre-placed stance:2 squad ground a
12-tank trickle to the kill quota of 8 with no agent input.

Recalibration:
- easy: enemy force 12 -> 14 light tanks, kill quota 8 -> 9 so only
active focus-fire of the frontmost tank clears the bar; idle spread
auto-fire is worn down first (stall now killed 7-8 < 9, lost 3-4).
- hard: kill quota 8 -> 9 (matches the comment header and medium).
- all tiers: added a units_in_region win clause requiring >=3 tanks
anchored in the choke region (53,20,r12). This makes the hold
load-bearing — a wrong-path retreat WEST into the open could still
scrape the quota on some hard seeds, but now fails the geometry.

Bar verified every level x seeds 1-4: stall / brute-east /
wrong-path-west all LOSE; intended hold-the-choke WINS.

openra_bench/scenarios/packs/combat-hold-chokepoint.yaml CHANGED
@@ -157,29 +157,31 @@ base:
157
 
158
  levels:
159
  # ── EASY ────────────────────────────────────────────────────────────
160
- # Bare chokepoint-hold skill. 4x 2tnk vs a force of 12 light tanks
161
  # (1tnk) hunting from the east. Holding the 3-cell corridor caps the
162
- # enemy to a 3-abreast trickle — the squad out-trades it. Charging
163
- # into the open east, or letting the enemy fan into the open west,
164
- # gets the squad surrounded. Win: kill >=8, keep >=3 tanks, fact
165
- # alive, before tick 5400.
166
  easy:
167
  description: >
168
  Four medium tanks (2tnk) hold a narrow corridor. Two silo walls
169
  split the map; the ONLY path between the west and east halves is
170
  a 3-cell-tall lane (y=19,20,21) running x=44..96. Your squad is
171
  pre-placed inside the corridor at the choke (around x=52..53). A
172
- force of TWELVE enemy light tanks (1tnk) stages in the EAST OPEN
173
- and hunts your squad — every enemy drives at the nearest tank
174
- and pursues. Win by destroying at least EIGHT enemy units while
175
- keeping at least THREE of your tanks alive and your construction
176
- yard intact, before tick 5400. HOLD THE CORRIDOR: keep your four
177
- tanks anchored in the lane and concentrate their fire on the
178
- frontmost enemy the walls confine the attackers to a 3-abreast
179
- trickle you can grind down. Charging EAST out of the corridor
180
- into the open, or pulling WEST and letting the enemy fan out,
181
- lets all twelve enemies surround you at once and focus-fire your
182
- squad down.
 
 
183
  overrides:
184
  actors:
185
  # SQUAD — 4 medium tanks PRE-PLACED at the choke, inside the
@@ -195,9 +197,15 @@ levels:
195
  # fight. building_count_gte:fact:1 in the win clause; razed
196
  # only if the enemy breaks the hold and reaches the west base.
197
  - {type: fact, owner: agent, position: [10, 20]}
198
- # ENEMY FORCE — 12 light tanks hunting from the east open.
199
  # They converge on the squad and pursue; the corridor funnels
200
- # them to a 3-abreast trickle through the lane.
 
 
 
 
 
 
201
  - {type: 1tnk, owner: enemy, position: [104, 19], stance: 3, count: 1}
202
  - {type: 1tnk, owner: enemy, position: [104, 20], stance: 3, count: 1}
203
  - {type: 1tnk, owner: enemy, position: [104, 21], stance: 3, count: 1}
@@ -210,6 +218,8 @@ levels:
210
  - {type: 1tnk, owner: enemy, position: [110, 19], stance: 3, count: 1}
211
  - {type: 1tnk, owner: enemy, position: [110, 20], stance: 3, count: 1}
212
  - {type: 1tnk, owner: enemy, position: [110, 21], stance: 3, count: 1}
 
 
213
  # Persistent far-east enemy fact — absorbs engine auto-done on
214
  # enemy-wipe so the terminal frame is evaluated.
215
  - {type: fact, owner: enemy, position: [122, 20]}
@@ -2020,9 +2030,15 @@ levels:
2020
  - {type: silo, owner: enemy, position: [ 96, 38], stance: 0}
2021
  win_condition:
2022
  all_of:
2023
- - {units_killed_gte: 8}
2024
  - {own_units_gte: 3}
2025
  - {building_count_gte: {type: fact, n: 1}}
 
 
 
 
 
 
2026
  - {within_ticks: 5400}
2027
  fail_condition:
2028
  any_of:
@@ -3892,6 +3908,8 @@ levels:
3892
  - {units_killed_gte: 9}
3893
  - {own_units_gte: 3}
3894
  - {building_count_gte: {type: fact, n: 1}}
 
 
3895
  - {within_ticks: 5400}
3896
  fail_condition:
3897
  any_of:
@@ -7594,9 +7612,15 @@ levels:
7594
  - {type: silo, owner: enemy, position: [ 96, 38], stance: 0, spawn_point: 1}
7595
  win_condition:
7596
  all_of:
7597
- - {units_killed_gte: 8}
7598
  - {own_units_gte: 3}
7599
  - {building_count_gte: {type: fact, n: 1}}
 
 
 
 
 
 
7600
  - {within_ticks: 5400}
7601
  fail_condition:
7602
  any_of:
 
157
 
158
  levels:
159
  # ── EASY ────────────────────────────────────────────────────────────
160
+ # Bare chokepoint-hold skill. 4x 2tnk vs a force of 14 light tanks
161
  # (1tnk) hunting from the east. Holding the 3-cell corridor caps the
162
+ # enemy to a 3-abreast trickle — concentrated fire out-trades it.
163
+ # Charging into the open east, or letting the enemy fan into the open
164
+ # west, gets the squad surrounded. Win: kill >=9, keep >=3 tanks in
165
+ # the choke region, fact alive, before tick 5400.
166
  easy:
167
  description: >
168
  Four medium tanks (2tnk) hold a narrow corridor. Two silo walls
169
  split the map; the ONLY path between the west and east halves is
170
  a 3-cell-tall lane (y=19,20,21) running x=44..96. Your squad is
171
  pre-placed inside the corridor at the choke (around x=52..53). A
172
+ force of FOURTEEN enemy light tanks (1tnk) stages in the EAST
173
+ OPEN and hunts your squad — every enemy drives at the nearest
174
+ tank and pursues. Win by destroying at least NINE enemy units
175
+ while keeping at least THREE of your tanks alive AND anchored in
176
+ the corridor, with your construction yard intact, before tick
177
+ 5400. HOLD THE CORRIDOR: keep your four tanks anchored in the
178
+ lane and concentrate their fire on the frontmost enemy the
179
+ walls confine the attackers to a 3-abreast trickle you can grind
180
+ down. Sitting idle and letting the squad spread its fire across
181
+ the front is too slow the force wears you down first. Charging
182
+ EAST out of the corridor into the open, or pulling WEST and
183
+ letting the enemy fan out, lets the whole force surround you at
184
+ once and focus-fire your squad down.
185
  overrides:
186
  actors:
187
  # SQUAD — 4 medium tanks PRE-PLACED at the choke, inside the
 
197
  # fight. building_count_gte:fact:1 in the win clause; razed
198
  # only if the enemy breaks the hold and reaches the west base.
199
  - {type: fact, owner: agent, position: [10, 20]}
200
+ # ENEMY FORCE — 14 light tanks hunting from the east open.
201
  # They converge on the squad and pursue; the corridor funnels
202
+ # them to a 3-abreast trickle through the lane. (Recalibrated
203
+ # 2026-05-20: bumped 12→14 after the engine movement fixes —
204
+ # a pre-placed stance:2 squad on pure auto-fire could grind a
205
+ # 12-tank trickle to the old kill quota unaided, so a stall
206
+ # policy won for free. 14 attackers + a quota of 9 means only
207
+ # active focus-fire of the frontmost tank clears the bar; the
208
+ # spread auto-fire of an idle squad is worn down first.)
209
  - {type: 1tnk, owner: enemy, position: [104, 19], stance: 3, count: 1}
210
  - {type: 1tnk, owner: enemy, position: [104, 20], stance: 3, count: 1}
211
  - {type: 1tnk, owner: enemy, position: [104, 21], stance: 3, count: 1}
 
218
  - {type: 1tnk, owner: enemy, position: [110, 19], stance: 3, count: 1}
219
  - {type: 1tnk, owner: enemy, position: [110, 20], stance: 3, count: 1}
220
  - {type: 1tnk, owner: enemy, position: [110, 21], stance: 3, count: 1}
221
+ - {type: 1tnk, owner: enemy, position: [106, 18], stance: 3, count: 1}
222
+ - {type: 1tnk, owner: enemy, position: [108, 18], stance: 3, count: 1}
223
  # Persistent far-east enemy fact — absorbs engine auto-done on
224
  # enemy-wipe so the terminal frame is evaluated.
225
  - {type: fact, owner: enemy, position: [122, 20]}
 
2030
  - {type: silo, owner: enemy, position: [ 96, 38], stance: 0}
2031
  win_condition:
2032
  all_of:
2033
+ - {units_killed_gte: 9}
2034
  - {own_units_gte: 3}
2035
  - {building_count_gte: {type: fact, n: 1}}
2036
+ # HOLD THE CORRIDOR — ≥3 tanks must still be inside the choke
2037
+ # region at the win evaluation. A squad that charged east
2038
+ # (brute) or pulled west into the open (wrong-path) fails the
2039
+ # geometry even if it scraped the kill quota; only a squad
2040
+ # anchored in the lane satisfies it.
2041
+ - {units_in_region_gte: {x: 53, y: 20, radius: 12, n: 3}}
2042
  - {within_ticks: 5400}
2043
  fail_condition:
2044
  any_of:
 
3908
  - {units_killed_gte: 9}
3909
  - {own_units_gte: 3}
3910
  - {building_count_gte: {type: fact, n: 1}}
3911
+ # HOLD THE CORRIDOR — ≥3 tanks anchored in the choke region.
3912
+ - {units_in_region_gte: {x: 53, y: 20, radius: 12, n: 3}}
3913
  - {within_ticks: 5400}
3914
  fail_condition:
3915
  any_of:
 
7612
  - {type: silo, owner: enemy, position: [ 96, 38], stance: 0, spawn_point: 1}
7613
  win_condition:
7614
  all_of:
7615
+ - {units_killed_gte: 9}
7616
  - {own_units_gte: 3}
7617
  - {building_count_gte: {type: fact, n: 1}}
7618
+ # HOLD THE CORRIDOR — ≥3 tanks anchored in the choke region.
7619
+ # Critical on hard: with the seed-flipped enemy cluster a
7620
+ # wrong-path retreat WEST into the open could still scrape the
7621
+ # kill quota on some seeds; the geometry clause makes that a
7622
+ # LOSS — the squad must be in the lane, not just alive.
7623
+ - {units_in_region_gte: {x: 53, y: 20, radius: 12, n: 3}}
7624
  - {within_ticks: 5400}
7625
  fail_condition:
7626
  any_of:
tests/test_combat_hold_chokepoint.py CHANGED
@@ -8,18 +8,30 @@ corridor, 4 medium tanks grind a 12-16 strong light-tank force down
8
  piecemeal; the same squad fighting in the open is surrounded and
9
  focus-fired down.
10
 
11
- Bar (verified 2026-05-20, every level, seeds 1-4):
 
 
 
 
 
 
12
  • stall (only observe) -> LOSS. The pre-placed squad
13
  auto-fires (stance:2) but SPREADS its fire across the 3-abreast
14
- front instead of concentrating it, kills too slowly, and is worn
15
- down -> own_units_gte:2 busts (killed ~3-4, lost 3).
 
16
  • brute attack_move east -> LOSS. The squad charges out of the
17
  corridor into the open; the whole force converges and focus-fires
18
- all 4 tanks -> loss cap busted before any kills (killed 0, lost 3).
 
 
 
 
 
19
  • intended hold-the-choke -> WIN. The squad stays anchored in
20
  the corridor and focus-fires the frontmost enemy; the 3-abreast
21
- funnel is ground down; the squad keeps >=3 tanks and hits the
22
- kill quota (killed 8-10, lost 1).
23
 
24
  Engine note: `silo` walls block PATHING but not weapons fire, so the
25
  chokepoint works by CONFINING the attacker to the 3-row lane (capping
@@ -46,8 +58,11 @@ PACK_PATH = PACKS / "combat-hold-chokepoint.yaml"
46
  # ── unit-level predicate checks ──────────────────────────────────────
47
 
48
 
49
- def _ctx(n_units, tick=1000, killed=0, fact=1):
50
- """Synthesize a WinContext for predicate-level checks."""
 
 
 
51
  import types
52
 
53
  sig = types.SimpleNamespace(
@@ -63,20 +78,23 @@ def _ctx(n_units, tick=1000, killed=0, fact=1):
63
  return WinContext(
64
  signals=sig,
65
  render_state={
66
- "units_summary": [{"cell_x": 52, "cell_y": 20}] * n_units,
 
 
67
  },
68
  )
69
 
70
 
71
  def test_predicates_easy():
72
  c = compile_level(load_pack(PACK_PATH), "easy")
73
- # 3 tanks alive, 8 kills, fact alive, in time -> WIN
74
- assert evaluate(c.win_condition, _ctx(3, tick=3000, killed=8))
 
75
  assert evaluate(c.win_condition, _ctx(4, tick=3000, killed=12))
76
  # only 2 tanks alive -> own_units_gte:3 fails
77
  assert not evaluate(c.win_condition, _ctx(2, tick=3000, killed=12))
78
- # kill quota unmet -> win fails
79
- assert not evaluate(c.win_condition, _ctx(4, tick=3000, killed=7))
80
  # fact razed -> win fails
81
  assert not evaluate(c.win_condition, _ctx(4, tick=3000, killed=12, fact=0))
82
  # 1 tank left -> fail clause fires (not own_units_gte:2)
@@ -91,9 +109,10 @@ def test_predicates_easy():
91
 
92
 
93
  def test_predicates_medium_and_hard():
94
- for lvl, quota in (("medium", 9), ("hard", 8)):
95
  c = compile_level(load_pack(PACK_PATH), lvl)
96
- # meeting the quota with >=3 tanks and fact alive -> WIN
 
97
  assert evaluate(c.win_condition, _ctx(3, tick=3000, killed=quota))
98
  # one short of the quota -> win fails
99
  assert not evaluate(
@@ -105,6 +124,27 @@ def test_predicates_medium_and_hard():
105
  assert evaluate(c.fail_condition, _ctx(4, tick=5402, killed=quota))
106
 
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  def test_timeout_loss_is_reachable_on_every_level():
109
  """No draw degeneracy: after_ticks 5401 fits inside max_turns on
110
  every level (~90 ticks/turn ⇒ 93 + 90·(max_turns-1))."""
@@ -170,6 +210,21 @@ def _brute_east_policy(rs, Command):
170
  ]
171
 
172
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  def _intended_hold_policy(rs, Command):
174
  """Intended hold-the-choke: keep the squad anchored in the corridor
175
  and FOCUS-FIRE the frontmost (lowest cell_x) enemy with every tank.
@@ -194,8 +249,10 @@ def _intended_hold_policy(rs, Command):
194
  @pytest.mark.parametrize("level", ["easy", "medium", "hard"])
195
  def test_stall_policy_loses(level):
196
  """Stall must LOSE on every level/seed — the pre-placed squad
197
- auto-fires but spreads fire instead of concentrating it, so it is
198
- worn down before the kill quota is met -> own_units_gte:2 busts."""
 
 
199
  pytest.importorskip("openra_train")
200
  from openra_bench.eval_core import run_level
201
 
@@ -208,6 +265,25 @@ def test_stall_policy_loses(level):
208
  )
209
 
210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  @pytest.mark.parametrize("level", ["easy", "medium", "hard"])
212
  def test_brute_east_policy_loses(level):
213
  """Brute attack_move east out of the corridor must LOSE — the squad
@@ -230,8 +306,8 @@ def test_intended_hold_wins(level):
230
  """The intended hold-the-choke policy (anchor in the corridor +
231
  focus-fire the frontmost enemy) WINS on every level/seed — the
232
  3-cell corridor caps the larger force to a 3-abreast trickle the
233
- squad grinds down, keeping >=3 tanks (verified 2026-05-20:
234
- killed 8-10, lost 1)."""
235
  pytest.importorskip("openra_train")
236
  from openra_bench.eval_core import run_level
237
 
 
8
  piecemeal; the same squad fighting in the open is surrounded and
9
  focus-fired down.
10
 
11
+ Bar (recalibrated 2026-05-20 after the OpenRA-Rust engine movement
12
+ fixes — moving units now fire and take fire en route, and attack_unit
13
+ on an out-of-sight target paths normally. Those fixes made a static
14
+ auto-firing squad strong enough that the old 12-tank / quota-8 easy
15
+ tier was beatable by a pure stall; the recalibration bumps easy to 14
16
+ attackers + quota 9 and adds a corridor-geometry win clause):
17
+
18
  • stall (only observe) -> LOSS. The pre-placed squad
19
  auto-fires (stance:2) but SPREADS its fire across the 3-abreast
20
+ front instead of concentrating it, kills too slowly (~7-8 < the
21
+ quota of 9), and is worn down -> own_units_gte:2 busts AND the
22
+ kill quota is unmet (killed 7-8, lost 3-4).
23
  • brute attack_move east -> LOSS. The squad charges out of the
24
  corridor into the open; the whole force converges and focus-fires
25
+ all 4 tanks -> loss cap busted before the kill quota (killed 0-1,
26
+ lost 3-4), and the squad is no longer in the choke region.
27
+ • wrong-path retreat WEST -> LOSS. Pulling the squad west out of
28
+ the corridor into the open: even when it scrapes the kill quota
29
+ the survival cap busts and the units_in_region geometry clause
30
+ fails (the squad is at x~20, not in the lane).
31
  • intended hold-the-choke -> WIN. The squad stays anchored in
32
  the corridor and focus-fires the frontmost enemy; the 3-abreast
33
+ funnel is ground down; the squad keeps >=3 tanks in the choke
34
+ region and hits the kill quota (killed 9-10, lost 0-1).
35
 
36
  Engine note: `silo` walls block PATHING but not weapons fire, so the
37
  chokepoint works by CONFINING the attacker to the 3-row lane (capping
 
58
  # ── unit-level predicate checks ──────────────────────────────────────
59
 
60
 
61
+ def _ctx(n_units, tick=1000, killed=0, fact=1, pos=(52, 20)):
62
+ """Synthesize a WinContext for predicate-level checks. `pos` is the
63
+ cell every own unit sits at — defaults to the choke (52,20) so the
64
+ units_in_region win clause is satisfied; pass an out-of-corridor
65
+ cell to exercise the geometry teeth."""
66
  import types
67
 
68
  sig = types.SimpleNamespace(
 
78
  return WinContext(
79
  signals=sig,
80
  render_state={
81
+ "units_summary": [
82
+ {"cell_x": pos[0], "cell_y": pos[1]}
83
+ ] * n_units,
84
  },
85
  )
86
 
87
 
88
  def test_predicates_easy():
89
  c = compile_level(load_pack(PACK_PATH), "easy")
90
+ # 3 tanks alive (in the choke region), 9 kills, fact alive, in
91
+ # time -> WIN
92
+ assert evaluate(c.win_condition, _ctx(3, tick=3000, killed=9))
93
  assert evaluate(c.win_condition, _ctx(4, tick=3000, killed=12))
94
  # only 2 tanks alive -> own_units_gte:3 fails
95
  assert not evaluate(c.win_condition, _ctx(2, tick=3000, killed=12))
96
+ # kill quota unmet (8 < 9) -> win fails
97
+ assert not evaluate(c.win_condition, _ctx(4, tick=3000, killed=8))
98
  # fact razed -> win fails
99
  assert not evaluate(c.win_condition, _ctx(4, tick=3000, killed=12, fact=0))
100
  # 1 tank left -> fail clause fires (not own_units_gte:2)
 
109
 
110
 
111
  def test_predicates_medium_and_hard():
112
+ for lvl, quota in (("medium", 9), ("hard", 9)):
113
  c = compile_level(load_pack(PACK_PATH), lvl)
114
+ # meeting the quota with >=3 tanks in the choke and fact alive
115
+ # -> WIN
116
  assert evaluate(c.win_condition, _ctx(3, tick=3000, killed=quota))
117
  # one short of the quota -> win fails
118
  assert not evaluate(
 
124
  assert evaluate(c.fail_condition, _ctx(4, tick=5402, killed=quota))
125
 
126
 
127
+ def test_corridor_geometry_clause_bites():
128
+ """The units_in_region win clause makes the hold load-bearing: a
129
+ squad that met the kill quota and survival cap but abandoned the
130
+ corridor (charged east or pulled west into the open) still fails
131
+ the win — only a squad anchored in the choke satisfies it."""
132
+ for lvl, quota in (("easy", 9), ("medium", 9), ("hard", 9)):
133
+ c = compile_level(load_pack(PACK_PATH), lvl)
134
+ # Anchored in the choke (52,20) -> WIN
135
+ assert evaluate(
136
+ c.win_condition, _ctx(4, tick=3000, killed=quota, pos=(52, 20))
137
+ )
138
+ # Pulled WEST into the open (x=20) -> geometry clause fails
139
+ assert not evaluate(
140
+ c.win_condition, _ctx(4, tick=3000, killed=quota, pos=(20, 20))
141
+ )
142
+ # Charged EAST out of the corridor (x=100) -> geometry fails
143
+ assert not evaluate(
144
+ c.win_condition, _ctx(4, tick=3000, killed=quota, pos=(100, 20))
145
+ )
146
+
147
+
148
  def test_timeout_loss_is_reachable_on_every_level():
149
  """No draw degeneracy: after_ticks 5401 fits inside max_turns on
150
  every level (~90 ticks/turn ⇒ 93 + 90·(max_turns-1))."""
 
210
  ]
211
 
212
 
213
+ def _wrong_path_west_policy(rs, Command):
214
+ """Wrong-path: pull the squad WEST out of the corridor into the
215
+ open ground behind the choke. Even when the retreating squad
216
+ auto-fires down enough pursuers to scrape the kill quota, it ends
217
+ outside the choke region (geometry clause fails) and the survival
218
+ cap busts -> LOSS on every level/seed."""
219
+ units = rs.get("units_summary", []) or []
220
+ if not units:
221
+ return [Command.observe()]
222
+ return [
223
+ Command.move_units([str(u["id"])], target_x=20, target_y=u["cell_y"])
224
+ for u in units
225
+ ]
226
+
227
+
228
  def _intended_hold_policy(rs, Command):
229
  """Intended hold-the-choke: keep the squad anchored in the corridor
230
  and FOCUS-FIRE the frontmost (lowest cell_x) enemy with every tank.
 
249
  @pytest.mark.parametrize("level", ["easy", "medium", "hard"])
250
  def test_stall_policy_loses(level):
251
  """Stall must LOSE on every level/seed — the pre-placed squad
252
+ auto-fires but spreads fire instead of concentrating it, so it
253
+ kills too slowly (below the quota of 9) and is worn down before
254
+ the quota is met -> own_units_gte:2 busts AND the kill quota is
255
+ unmet (recalibrated 2026-05-20: 14-tank easy / quota 9)."""
256
  pytest.importorskip("openra_train")
257
  from openra_bench.eval_core import run_level
258
 
 
265
  )
266
 
267
 
268
+ @pytest.mark.parametrize("level", ["easy", "medium", "hard"])
269
+ def test_wrong_path_west_policy_loses(level):
270
+ """Wrong-path retreat WEST out of the corridor must LOSE on every
271
+ level/seed — the squad abandons the choke; the survival cap busts
272
+ and/or the units_in_region geometry win clause fails (the squad
273
+ sits in the open at x~20, not in the lane)."""
274
+ pytest.importorskip("openra_train")
275
+ from openra_bench.eval_core import run_level
276
+
277
+ c = compile_level(load_pack(PACK_PATH), level)
278
+ for s in (1, 2, 3, 4):
279
+ res = run_level(c, _wrong_path_west_policy, seed=s)
280
+ assert res.outcome == "loss", (
281
+ f"{level} seed={s}: wrong-path west must LOSE; got "
282
+ f"{res.outcome} killed={res.signals.units_killed} "
283
+ f"lost={res.signals.units_lost}"
284
+ )
285
+
286
+
287
  @pytest.mark.parametrize("level", ["easy", "medium", "hard"])
288
  def test_brute_east_policy_loses(level):
289
  """Brute attack_move east out of the corridor must LOSE — the squad
 
306
  """The intended hold-the-choke policy (anchor in the corridor +
307
  focus-fire the frontmost enemy) WINS on every level/seed — the
308
  3-cell corridor caps the larger force to a 3-abreast trickle the
309
+ squad grinds down, keeping >=3 tanks anchored in the choke
310
+ region (recalibrated 2026-05-20: killed 9-10, lost 0-1)."""
311
  pytest.importorskip("openra_train")
312
  from openra_bench.eval_core import run_level
313