yxc20098 commited on
Commit
74217dc
·
1 Parent(s): 8ac1451

fix(scenario): def-counter-battery — recalibrate after engine movement fixes

Browse files
openra_bench/scenarios/packs/def-counter-battery.yaml CHANGED
@@ -48,13 +48,21 @@
48
  # reached (the run ends the moment the fact falls, capping the
49
  # kill count well below K) → LOSS.
50
  #
51
- # • intended counter-battery strike (drive the tanks straight to
52
- # the artillery, destroy ALL the guns FIRST, THEN mop up the
53
- # infantry screen): the guns die in ~1-2 decision turns of
54
- # concentrated tank fire (arty hp75 vs ~88-132 tank dps); once
55
- # the battery is silent the fact stops taking damage and
56
- # survives, and the strike force has the rest of the budget to
57
- # clear the screen for the kill bar WIN.
 
 
 
 
 
 
 
 
58
  #
59
  # WHY THE SPEC WORKS (engine combat sheet, per CLAUDE.md / prompt_v2):
60
  # - `arty` = artillery: $600 hp75 rng7c dps45 sight5c. Range 7c
@@ -75,7 +83,12 @@
75
  # - `2tnk` = medium tank: hp400 rng4.75c dps22. Four to six tanks
76
  # concentrate enough fire to one-shot-class the fragile arty;
77
  # infantry cannot meaningfully block tank movement, so the
78
- # strike can drive through the screen to reach the guns.
 
 
 
 
 
79
  # - The kill bar K is set ABOVE the number of kills a screen-first
80
  # policy can rack up before the fact falls (the run ends on the
81
  # fact's destruction), but BELOW the full enemy roster — the
 
48
  # reached (the run ends the moment the fact falls, capping the
49
  # kill count well below K) → LOSS.
50
  #
51
+ # • intended counter-battery strike (drive the tanks straight
52
+ # THROUGH the screen to the artillery, destroy ALL the guns
53
+ # FIRST, THEN mop up the infantry screen): post the engine
54
+ # movement fix, `attack_unit` no longer teleport-chases an
55
+ # out-of-sight target it closes at the tank's real Mobile
56
+ # speed, identical to a plain `move`. The strike therefore has
57
+ # to PUSH THROUGH the passive screen with a straight move
58
+ # order; the moment the guns come into sight, concentrated
59
+ # tank fire silences the fragile battery (arty hp75) in ~1-2
60
+ # decision turns. Once the battery is silent the fact stops
61
+ # taking damage and survives, and the strike force spends the
62
+ # rest of the budget clearing the screen for the kill bar →
63
+ # WIN. A strike that STOPS to trade with the screen en route
64
+ # (an `attack_unit` on a screen rifleman) never reaches the
65
+ # guns in time — the artillery razes the fact first.
66
  #
67
  # WHY THE SPEC WORKS (engine combat sheet, per CLAUDE.md / prompt_v2):
68
  # - `arty` = artillery: $600 hp75 rng7c dps45 sight5c. Range 7c
 
83
  # - `2tnk` = medium tank: hp400 rng4.75c dps22. Four to six tanks
84
  # concentrate enough fire to one-shot-class the fragile arty;
85
  # infantry cannot meaningfully block tank movement, so the
86
+ # strike can drive THROUGH the screen to reach the guns — but
87
+ # only with a plain `move` order. Since the engine movement fix
88
+ # a moving tank fires opportunistically en route yet does NOT
89
+ # abandon its move, so the straight drive is not delayed by the
90
+ # passive screen; an explicit `attack_unit` on a screen unit,
91
+ # by contrast, halts the tanks to grind it.
92
  # - The kill bar K is set ABOVE the number of kills a screen-first
93
  # policy can rack up before the fact falls (the run ends on the
94
  # fact's destruction), but BELOW the full enemy roster — the
tests/test_def_counter_battery.py CHANGED
@@ -254,10 +254,21 @@ def _screen_first(rs, Command):
254
 
255
 
256
  def _counter_battery_strike(rs, Command):
257
- """The intended play — drive the tanks toward the artillery and
258
- attack_unit the guns FIRST (highest-impact threat); only once the
259
- battery is silent mop up the infantry screen. Saving the fact
260
- leaves the rest of the budget to clear the kill bar → WIN."""
 
 
 
 
 
 
 
 
 
 
 
261
  ts = _tanks(rs)
262
  if not ts:
263
  return [Command.observe()]
@@ -265,12 +276,31 @@ def _counter_battery_strike(rs, Command):
265
  ids = [str(t["id"]) for t in ts]
266
  artys = [e for e in es if str(e.get("type", "")).lower() == "arty"]
267
  if artys:
 
268
  return [Command.attack_unit(ids, str(artys[0]["id"]))]
 
 
 
 
 
 
 
 
 
269
  e1s = [e for e in es if str(e.get("type", "")).lower() == "e1"]
270
  if e1s:
271
  return [Command.attack_unit(ids, str(e1s[0]["id"]))]
 
 
 
 
 
 
 
 
 
272
  ay = int(sum(t["cell_y"] for t in ts) / len(ts))
273
- return [Command.move_units(ids, 27, ay)]
274
 
275
 
276
  @pytest.mark.parametrize("level", ["easy", "medium", "hard"])
@@ -333,3 +363,21 @@ def test_screen_first_loses(level, seed):
333
  f"razes the fact before the screen is cleared), got "
334
  f"{r.outcome} (kills={r.signals.units_killed})"
335
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
 
256
  def _counter_battery_strike(rs, Command):
257
+ """The intended play — drive the tanks PAST the infantry screen to
258
+ the rear battery and attack_unit the guns FIRST (highest-impact
259
+ threat); only once the battery is silent mop up the infantry
260
+ screen for the kill bar. Saving the fact leaves the rest of the
261
+ budget to clear the kill bar → WIN.
262
+
263
+ Post engine-movement-fix: `attack_unit` no longer teleport-chases
264
+ an out-of-sight target — it closes distance at the tank's real
265
+ Mobile speed. The strike must therefore PUSH THROUGH the screen
266
+ with `move_units` (a plain move, which the engine still resolves
267
+ at full speed) rather than `attack_unit`-ing a screen rifleman
268
+ en route — stopping to trade with the screen lets the artillery
269
+ raze the fact before the guns are reached. Only once the tanks
270
+ are deep east (past the screen column) and the guns are silenced
271
+ does the policy pivot to mopping the screen."""
272
  ts = _tanks(rs)
273
  if not ts:
274
  return [Command.observe()]
 
276
  ids = [str(t["id"]) for t in ts]
277
  artys = [e for e in es if str(e.get("type", "")).lower() == "arty"]
278
  if artys:
279
+ # Guns in sight — kill the battery FIRST, concentrated fire.
280
  return [Command.attack_unit(ids, str(artys[0]["id"]))]
281
+ ax = sum(t.get("cell_x", 0) for t in ts) / len(ts)
282
+ ay = int(sum(t["cell_y"] for t in ts) / len(ts))
283
+ if ax < 26:
284
+ # Still closing on the battery — drive STRAIGHT through the
285
+ # screen toward the rear (a plain move, not an attack order,
286
+ # so the tanks don't stop to grind the passive picket line).
287
+ return [Command.move_units(ids, 29, ay)]
288
+ # Deep east with no gun in sight — the battery is dead; mop the
289
+ # infantry screen for the kill bar.
290
  e1s = [e for e in es if str(e.get("type", "")).lower() == "e1"]
291
  if e1s:
292
  return [Command.attack_unit(ids, str(e1s[0]["id"]))]
293
+ return [Command.observe()]
294
+
295
+
296
+ def _wrong_path(rs, Command):
297
+ """Wrong route — drive the tanks AWAY from the battery (back west).
298
+ The guns are never engaged; the artillery razes the fact → LOSS."""
299
+ ts = _tanks(rs)
300
+ if not ts:
301
+ return [Command.observe()]
302
  ay = int(sum(t["cell_y"] for t in ts) / len(ts))
303
+ return [Command.move_units([str(t["id"]) for t in ts], 8, ay)]
304
 
305
 
306
  @pytest.mark.parametrize("level", ["easy", "medium", "hard"])
 
363
  f"razes the fact before the screen is cleared), got "
364
  f"{r.outcome} (kills={r.signals.units_killed})"
365
  )
366
+
367
+
368
+ @pytest.mark.parametrize("level", ["easy", "medium", "hard"])
369
+ @pytest.mark.parametrize("seed", [1, 2, 3, 4])
370
+ def test_wrong_path_loses(level, seed):
371
+ """The wrong-route play — driving the tanks away from the battery
372
+ instead of toward it — must LOSE on every level and seed: the guns
373
+ are never engaged and the artillery razes the fact."""
374
+ pytest.importorskip("openra_train")
375
+ from openra_bench.eval_core import run_level
376
+
377
+ c = compile_level(load_pack(PACK_PATH), level)
378
+ r = run_level(c, _wrong_path, seed=seed)
379
+ assert r.outcome == "loss", (
380
+ f"{level} seed={seed}: wrong-path must LOSE (the artillery "
381
+ f"razes the fact while the tanks drive the wrong way), got "
382
+ f"{r.outcome} (kills={r.signals.units_killed})"
383
+ )