Spaces:
Running
Running
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
|
| 52 |
-
# the artillery, destroy ALL the guns
|
| 53 |
-
#
|
| 54 |
-
#
|
| 55 |
-
#
|
| 56 |
-
#
|
| 57 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 258 |
-
attack_unit the guns FIRST (highest-impact
|
| 259 |
-
battery is silent mop up the infantry
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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 |
+
)
|