Spaces:
Running
fix(scenario): combat-kite-jeep-vs-tank — recalibrate after engine movement fixes
Browse filesThe OpenRA-Rust movement fixes (moving units fire and take fire en
route; attack_unit on out-of-sight targets paths normally) regressed
this pack on every tier: the easy 1tnk-light-tank variant went fully
degenerate (every policy, stall included, killed the weak light tank
for free) and the close-range trade shifted so a static stand-and-
shoot now trades exactly one raider for the kill — beating the kite
cycle under the old own_units_gte:2 survival bar.
Recalibration:
- easy: replace the 1tnk light tank with the same 3tnk Soviet heavy
used on medium/hard, staged closer (x=70) so the kite-band is
widest; raiders stage off-axis (y=10) for the reactive window.
- survival bar own_units_gte:3 on EVERY tier (was :1 easy, :2
medium/hard): kiting kills the heavy losing zero raiders, every
non-kite policy loses >=1, so 'lose no raider' is the load-bearing
teeth.
- the easy->medium->hard axis is now kite-band width: easy heavy at
x=70 (band ~5-7), medium at x=80 (band ~5-6), hard at x=80 + the
two seed-flipped spawn corridors (only the ~5-cell trigger
generalises).
Bar verified every level x seeds 1-4: stall / stand-and-shoot / brute
attack_move all LOSE; intended kite cycle WINS. The stand-and-shoot
test is now a strict LOSS on all tiers (was an xfail on the pre-fix
engine).
|
@@ -97,73 +97,88 @@ base:
|
|
| 97 |
|
| 98 |
levels:
|
| 99 |
# ── EASY ────────────────────────────────────────────────────────────
|
| 100 |
-
# Bare kite skill
|
| 101 |
-
# (
|
| 102 |
-
#
|
| 103 |
-
#
|
| 104 |
-
#
|
| 105 |
-
#
|
| 106 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
easy:
|
| 108 |
description: >
|
| 109 |
-
Three fast medium-tank raiders (2tnk)
|
| 110 |
-
must kill ONE enemy
|
| 111 |
-
your raiders from centre-east
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
|
|
|
| 122 |
overrides:
|
| 123 |
actors:
|
| 124 |
-
# RAIDERS — 3 medium tanks spread across three cells
|
| 125 |
-
#
|
| 126 |
-
#
|
| 127 |
-
|
| 128 |
-
- {type: 2tnk, owner: agent, position: [
|
| 129 |
-
- {type: 2tnk, owner: agent, position: [
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
| 132 |
# Persistent far-east enemy fact — prevents engine auto-done
|
| 133 |
# on enemy unit wipe collapsing the run to DRAW.
|
| 134 |
- {type: fact, owner: enemy, position: [124, 20]}
|
| 135 |
win_condition:
|
| 136 |
all_of:
|
| 137 |
- {units_killed_gte: 1}
|
| 138 |
-
- {own_units_gte:
|
| 139 |
- {within_ticks: 4500}
|
| 140 |
fail_condition:
|
| 141 |
any_of:
|
| 142 |
- {after_ticks: 4501}
|
| 143 |
-
- {not: {own_units_gte:
|
| 144 |
max_turns: 51
|
| 145 |
|
| 146 |
# ── MEDIUM ──────────────────────────────────────────────────────────
|
| 147 |
-
# +1 controlled variable vs easy: the heavy
|
| 148 |
-
#
|
| 149 |
-
#
|
| 150 |
-
#
|
| 151 |
-
#
|
| 152 |
-
#
|
|
|
|
|
|
|
| 153 |
medium:
|
| 154 |
description: >
|
| 155 |
-
Three medium-tank raiders (2tnk)
|
| 156 |
-
kill ONE enemy HEAVY tank (3tnk)
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
runs out. The only winning play is
|
| 161 |
-
tank closes within ~
|
| 162 |
-
along the lane, then attack_unit the tank from
|
| 163 |
-
close-range window; repeat. Keep
|
| 164 |
-
|
| 165 |
-
the
|
| 166 |
-
shoot, and brute attack_move all LOSE.
|
|
|
|
| 167 |
overrides:
|
| 168 |
actors:
|
| 169 |
# Raiders stage off the tank's centroid latitude (y=10
|
|
@@ -173,19 +188,19 @@ levels:
|
|
| 173 |
- {type: 2tnk, owner: agent, position: [28, 9], stance: 1}
|
| 174 |
- {type: 2tnk, owner: agent, position: [30, 10], stance: 1}
|
| 175 |
- {type: 2tnk, owner: agent, position: [28, 11], stance: 1}
|
| 176 |
-
# Heavy tank on the mid-latitude
|
| 177 |
-
# centroid.
|
| 178 |
- {type: 3tnk, owner: enemy, position: [80, 20], stance: 2}
|
| 179 |
- {type: fact, owner: enemy, position: [124, 20]}
|
| 180 |
win_condition:
|
| 181 |
all_of:
|
| 182 |
- {units_killed_gte: 1}
|
| 183 |
-
- {own_units_gte:
|
| 184 |
- {within_ticks: 4500}
|
| 185 |
fail_condition:
|
| 186 |
any_of:
|
| 187 |
- {after_ticks: 4501}
|
| 188 |
-
- {not: {own_units_gte:
|
| 189 |
max_turns: 51
|
| 190 |
|
| 191 |
# ── HARD ────────────────────────────────────────────────────────────
|
|
@@ -198,6 +213,11 @@ levels:
|
|
| 198 |
# opening cannot generalise. The heavy tank is centred mid-map
|
| 199 |
# on the mid-latitude (y=20) between the two corridors so both
|
| 200 |
# spawns face a symmetric pursuit geometry from the hunt bot.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
hard:
|
| 202 |
description: >
|
| 203 |
Three medium-tank raiders (2tnk) stage at one of two
|
|
@@ -207,11 +227,11 @@ levels:
|
|
| 207 |
actively HUNTING your raiders — whichever corridor you start
|
| 208 |
in, the tank closes on you. The cannon out-trades raider
|
| 209 |
weapons at close range; the only winning play is the KITE
|
| 210 |
-
cycle — when the tank closes within ~
|
| 211 |
-
raiders
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
overrides:
|
| 216 |
actors:
|
| 217 |
# spawn_point 0 — NORTH corridor (y=10)
|
|
@@ -229,10 +249,10 @@ levels:
|
|
| 229 |
win_condition:
|
| 230 |
all_of:
|
| 231 |
- {units_killed_gte: 1}
|
| 232 |
-
- {own_units_gte:
|
| 233 |
- {within_ticks: 3600}
|
| 234 |
fail_condition:
|
| 235 |
any_of:
|
| 236 |
- {after_ticks: 3601}
|
| 237 |
-
- {not: {own_units_gte:
|
| 238 |
max_turns: 41
|
|
|
|
| 97 |
|
| 98 |
levels:
|
| 99 |
# ── EASY ────────────────────────────────────────────────────────────
|
| 100 |
+
# Bare kite skill. Recalibrated 2026-05-20 after the OpenRA-Rust
|
| 101 |
+
# movement fixes (moving units fire AND take fire en route; attack_
|
| 102 |
+
# unit on out-of-sight targets paths normally). Those fixes made the
|
| 103 |
+
# old 1tnk-light-tank variant fully degenerate — every policy
|
| 104 |
+
# (including pure stall) killed the weak light tank for free — so the
|
| 105 |
+
# heavy is now the same 3tnk Soviet heavy used on medium/hard. The
|
| 106 |
+
# easy AXIS is the kite-band WIDTH: the heavy stages CLOSER (x≈70 vs
|
| 107 |
+
# x≈80 on medium) so it has less closing distance, the hunt-bot
|
| 108 |
+
# centroid chase lags more, and the kite cycle has a forgiving
|
| 109 |
+
# reactive window — the intended kite wins across a trigger band of
|
| 110 |
+
# ~5..7 cells. On medium the band narrows to ~5..6; on hard (with the
|
| 111 |
+
# seed-flipped spawn) only the ~5-cell trigger generalises.
|
| 112 |
+
# The bar: kiting kills the heavy WITHOUT losing a raider; standing
|
| 113 |
+
# still trades exactly one raider for the kill, so own_units_gte:3
|
| 114 |
+
# (lose ZERO raiders) is the load-bearing teeth — stall / stand /
|
| 115 |
+
# brute all LOSE, only the kite cycle keeps all three raiders.
|
| 116 |
easy:
|
| 117 |
description: >
|
| 118 |
+
Three fast medium-tank raiders (2tnk) stage off-axis at the
|
| 119 |
+
centre-west (y≈10) and must kill ONE enemy HEAVY tank (3tnk)
|
| 120 |
+
that is actively HUNTING your raiders from centre-east
|
| 121 |
+
(x≈70, y=20). The heavy tank's cannon out-trades raider weapons
|
| 122 |
+
at close range and can one-shot a raider; the only winning play
|
| 123 |
+
is the KITE cycle — when the tank closes into ~5 cells, MOVE
|
| 124 |
+
your raiders away along the lane, then attack_unit the tank from
|
| 125 |
+
outside its close-range window; repeat. Keep ALL THREE raiders
|
| 126 |
+
alive (kiting kills the heavy losing none; standing still trades
|
| 127 |
+
one raider for the kill). Stalling (observe only) LOSES — the
|
| 128 |
+
hunt-bot heavy closes and grinds a raider down. Standing still
|
| 129 |
+
and trading shots LOSES — the cannon out-trades raider weapons
|
| 130 |
+
and one raider falls. Brute attack_move east LOSES (same — no
|
| 131 |
+
disengage). Before tick 4500.
|
| 132 |
overrides:
|
| 133 |
actors:
|
| 134 |
+
# RAIDERS — 3 medium tanks spread across three cells off the
|
| 135 |
+
# heavy's centroid latitude (y≈10, not the heavy's y=20) so the
|
| 136 |
+
# hunt-bot must close DIAGONALLY — the y-axis lag gives the
|
| 137 |
+
# kite cycle its reactive window. Spread avoids retreat-pinning.
|
| 138 |
+
- {type: 2tnk, owner: agent, position: [28, 9], stance: 1}
|
| 139 |
+
- {type: 2tnk, owner: agent, position: [30, 10], stance: 1}
|
| 140 |
+
- {type: 2tnk, owner: agent, position: [28, 11], stance: 1}
|
| 141 |
+
# THE HEAVY — 3tnk Soviet heavy, staged CLOSER (x=70) than on
|
| 142 |
+
# medium/hard so the kite band is widest (the easy axis).
|
| 143 |
+
- {type: 3tnk, owner: enemy, position: [70, 20], stance: 2}
|
| 144 |
# Persistent far-east enemy fact — prevents engine auto-done
|
| 145 |
# on enemy unit wipe collapsing the run to DRAW.
|
| 146 |
- {type: fact, owner: enemy, position: [124, 20]}
|
| 147 |
win_condition:
|
| 148 |
all_of:
|
| 149 |
- {units_killed_gte: 1}
|
| 150 |
+
- {own_units_gte: 3}
|
| 151 |
- {within_ticks: 4500}
|
| 152 |
fail_condition:
|
| 153 |
any_of:
|
| 154 |
- {after_ticks: 4501}
|
| 155 |
+
- {not: {own_units_gte: 3}}
|
| 156 |
max_turns: 51
|
| 157 |
|
| 158 |
# ── MEDIUM ──────────────────────────────────────────────────────────
|
| 159 |
+
# +1 controlled variable vs easy: the heavy stages FURTHER east
|
| 160 |
+
# (x≈80 vs x≈70 on easy), so it has more closing distance and the
|
| 161 |
+
# kite-band narrows from ~5..7 cells down to ~5..6 — a sloppier
|
| 162 |
+
# proximity threshold no longer wins. The 3tnk heavy and the
|
| 163 |
+
# own_units_gte:3 force-preservation bar are the same as easy: kiting
|
| 164 |
+
# kills the heavy losing ZERO raiders, standing still trades exactly
|
| 165 |
+
# one raider for the kill, so the survival bar is the load-bearing
|
| 166 |
+
# teeth. Stall, stand-and-shoot, and brute attack_move all LOSE.
|
| 167 |
medium:
|
| 168 |
description: >
|
| 169 |
+
Three medium-tank raiders (2tnk) stage off-axis at the
|
| 170 |
+
centre-west (y≈10) and must kill ONE enemy HEAVY tank (3tnk)
|
| 171 |
+
that is actively HUNTING your raiders from centre-east
|
| 172 |
+
(x≈80, y=20). The heavy tank's cannon out-trades raider weapons
|
| 173 |
+
at close range and can one-shot a raider; standing still loses a
|
| 174 |
+
raider before the tank's HP runs out. The only winning play is
|
| 175 |
+
the KITE cycle — when the tank closes within ~5 cells, MOVE your
|
| 176 |
+
raiders away along the lane, then attack_unit the tank from
|
| 177 |
+
outside its close-range window; repeat. Keep ALL THREE raiders
|
| 178 |
+
alive — a stand-and-shoot trades one raider for the kill and
|
| 179 |
+
busts the survival bar; only the kite cycle preserves the whole
|
| 180 |
+
stack. Stall, stand-and-shoot, and brute attack_move all LOSE.
|
| 181 |
+
Before tick 4500.
|
| 182 |
overrides:
|
| 183 |
actors:
|
| 184 |
# Raiders stage off the tank's centroid latitude (y=10
|
|
|
|
| 188 |
- {type: 2tnk, owner: agent, position: [28, 9], stance: 1}
|
| 189 |
- {type: 2tnk, owner: agent, position: [30, 10], stance: 1}
|
| 190 |
- {type: 2tnk, owner: agent, position: [28, 11], stance: 1}
|
| 191 |
+
# Heavy tank on the mid-latitude, staged further east than on
|
| 192 |
+
# easy — hunts the y=10 raider centroid.
|
| 193 |
- {type: 3tnk, owner: enemy, position: [80, 20], stance: 2}
|
| 194 |
- {type: fact, owner: enemy, position: [124, 20]}
|
| 195 |
win_condition:
|
| 196 |
all_of:
|
| 197 |
- {units_killed_gte: 1}
|
| 198 |
+
- {own_units_gte: 3}
|
| 199 |
- {within_ticks: 4500}
|
| 200 |
fail_condition:
|
| 201 |
any_of:
|
| 202 |
- {after_ticks: 4501}
|
| 203 |
+
- {not: {own_units_gte: 3}}
|
| 204 |
max_turns: 51
|
| 205 |
|
| 206 |
# ── HARD ────────────────────────────────────────────────────────────
|
|
|
|
| 213 |
# opening cannot generalise. The heavy tank is centred mid-map
|
| 214 |
# on the mid-latitude (y=20) between the two corridors so both
|
| 215 |
# spawns face a symmetric pursuit geometry from the hunt bot.
|
| 216 |
+
# With the seed-flipped spawn the kite-band narrows further: only a
|
| 217 |
+
# ~5-cell trigger generalises across BOTH corridors. The
|
| 218 |
+
# own_units_gte:3 force-preservation bar is the same as easy/medium
|
| 219 |
+
# — kiting kills the heavy losing zero raiders, every other policy
|
| 220 |
+
# loses at least one.
|
| 221 |
hard:
|
| 222 |
description: >
|
| 223 |
Three medium-tank raiders (2tnk) stage at one of two
|
|
|
|
| 227 |
actively HUNTING your raiders — whichever corridor you start
|
| 228 |
in, the tank closes on you. The cannon out-trades raider
|
| 229 |
weapons at close range; the only winning play is the KITE
|
| 230 |
+
cycle — when the tank closes within ~5 cells, MOVE your
|
| 231 |
+
raiders away along your lane, then attack_unit the tank from
|
| 232 |
+
outside its close-range window; repeat. Keep ALL THREE raiders
|
| 233 |
+
alive AND finish before tick 3600. Stall, stand-and-shoot, and
|
| 234 |
+
brute attack_move all LOSE.
|
| 235 |
overrides:
|
| 236 |
actors:
|
| 237 |
# spawn_point 0 — NORTH corridor (y=10)
|
|
|
|
| 249 |
win_condition:
|
| 250 |
all_of:
|
| 251 |
- {units_killed_gte: 1}
|
| 252 |
+
- {own_units_gte: 3}
|
| 253 |
- {within_ticks: 3600}
|
| 254 |
fail_condition:
|
| 255 |
any_of:
|
| 256 |
- {after_ticks: 3601}
|
| 257 |
+
- {not: {own_units_gte: 3}}
|
| 258 |
max_turns: 41
|
|
@@ -1,26 +1,35 @@
|
|
| 1 |
"""combat-kite-jeep-vs-tank — kite a slow heavy unit with fast raiders.
|
| 2 |
|
| 3 |
-
Bar
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
"""
|
| 25 |
|
| 26 |
from __future__ import annotations
|
|
@@ -65,14 +74,18 @@ def _ctx(units_xy=(), tick=1000, killed=0, lost=0):
|
|
| 65 |
|
| 66 |
def test_predicates_easy():
|
| 67 |
c = compile_level(load_pack(PACK_PATH), "easy")
|
| 68 |
-
raiders3 = [(28,
|
| 69 |
|
| 70 |
-
# Intended: 1 kill (
|
| 71 |
assert evaluate(c.win_condition, _ctx(raiders3, tick=3000, killed=1, lost=0))
|
| 72 |
-
#
|
| 73 |
-
assert evaluate(
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
# Past deadline → real loss, reachable within max_turns
|
| 77 |
assert evaluate(c.fail_condition, _ctx(raiders3, tick=4502, killed=0, lost=0))
|
| 78 |
assert 4501 <= 93 + 90 * (c.max_turns - 1), (
|
|
@@ -80,21 +93,19 @@ def test_predicates_easy():
|
|
| 80 |
)
|
| 81 |
|
| 82 |
|
| 83 |
-
def
|
| 84 |
c = compile_level(load_pack(PACK_PATH), "medium")
|
| 85 |
-
raiders3 = [(28,
|
| 86 |
raiders2 = raiders3[:2]
|
| 87 |
-
raiders1 = raiders3[:1]
|
| 88 |
|
| 89 |
-
# Intended: 1 kill,
|
| 90 |
assert evaluate(c.win_condition, _ctx(raiders3, tick=3000, killed=1, lost=0))
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
assert not evaluate(c.win_condition, _ctx(raiders1, tick=3000, killed=1, lost=2))
|
| 94 |
# 0 kills → predicate fails
|
| 95 |
assert not evaluate(c.win_condition, _ctx(raiders3, tick=3000, killed=0, lost=0))
|
| 96 |
-
# 1 raider
|
| 97 |
-
assert evaluate(c.fail_condition, _ctx(
|
| 98 |
# Past deadline → real loss, reachable
|
| 99 |
assert evaluate(c.fail_condition, _ctx(raiders3, tick=4502, killed=0, lost=0))
|
| 100 |
assert 4501 <= 93 + 90 * (c.max_turns - 1)
|
|
@@ -104,11 +115,11 @@ def test_predicates_hard_tighter_deadline_and_survival_bar():
|
|
| 104 |
c = compile_level(load_pack(PACK_PATH), "hard")
|
| 105 |
raiders3 = [(28, 9), (30, 10), (28, 11)]
|
| 106 |
|
| 107 |
-
# Intended: 1 kill,
|
| 108 |
assert evaluate(c.win_condition, _ctx(raiders3, tick=3000, killed=1, lost=0))
|
| 109 |
-
# 1 raider
|
| 110 |
assert not evaluate(
|
| 111 |
-
c.win_condition, _ctx(raiders3[:
|
| 112 |
)
|
| 113 |
# Past the tighter deadline → real loss, reachable
|
| 114 |
assert evaluate(c.fail_condition, _ctx(raiders3, tick=3602, killed=0, lost=0))
|
|
@@ -146,6 +157,26 @@ def test_pack_compiles_and_meta_fields_populated():
|
|
| 146 |
assert c.win_condition is not None and c.fail_condition is not None
|
| 147 |
|
| 148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
def test_timeout_loss_is_reachable_on_every_level():
|
| 150 |
"""No draw degeneracy: the after_ticks deadline fits inside
|
| 151 |
max_turns on every level (∼90 ticks/turn ⇒ 93 + 90·(max_turns-1))."""
|
|
@@ -164,10 +195,12 @@ def test_timeout_loss_is_reachable_on_every_level():
|
|
| 164 |
# KITE_TRIGGER cells of a raider, move that raider RETREAT_DIST cells
|
| 165 |
# AWAY from the tank along the lane; otherwise attack_unit the
|
| 166 |
# nearest tank. The cycle is purely reactive — no memory, derived
|
| 167 |
-
# each turn from geometry.
|
|
|
|
|
|
|
| 168 |
|
| 169 |
-
KITE_TRIGGER =
|
| 170 |
-
RETREAT_DIST =
|
| 171 |
|
| 172 |
|
| 173 |
def _tanks(enemies):
|
|
@@ -179,15 +212,15 @@ def _tanks(enemies):
|
|
| 179 |
|
| 180 |
|
| 181 |
def _stall_policy(rs, Command):
|
| 182 |
-
"""Stall: only observe.
|
| 183 |
-
|
| 184 |
return [Command.observe()]
|
| 185 |
|
| 186 |
|
| 187 |
def _stand_and_shoot_policy(rs, Command):
|
| 188 |
-
"""Stand at staging, attack_unit the
|
| 189 |
-
|
| 190 |
-
|
| 191 |
units = rs.get("units_summary", []) or []
|
| 192 |
enemies = rs.get("enemy_summary", []) or []
|
| 193 |
tanks = _tanks(enemies)
|
|
@@ -210,8 +243,8 @@ def _stand_and_shoot_policy(rs, Command):
|
|
| 210 |
|
| 211 |
def _brute_attack_move_policy(rs, Command):
|
| 212 |
"""Brute: one attack_move order eastward. No disengage; the
|
| 213 |
-
column meets the hunt-bot heavy and
|
| 214 |
-
trade as stand-and-shoot."""
|
| 215 |
units = rs.get("units_summary", []) or []
|
| 216 |
if not units:
|
| 217 |
return [Command.observe()]
|
|
@@ -266,16 +299,16 @@ def _intended_kite_policy(rs, Command):
|
|
| 266 |
return cmds
|
| 267 |
|
| 268 |
|
| 269 |
-
@pytest.mark.parametrize("level", ["medium", "hard"])
|
| 270 |
def test_stall_policy_loses(level):
|
| 271 |
-
"""Stall must LOSE on
|
| 272 |
-
|
|
|
|
| 273 |
pytest.importorskip("openra_train")
|
| 274 |
from openra_bench.eval_core import run_level
|
| 275 |
|
| 276 |
c = compile_level(load_pack(PACK_PATH), level)
|
| 277 |
-
|
| 278 |
-
for s in seeds:
|
| 279 |
res = run_level(c, _stall_policy, seed=s)
|
| 280 |
assert res.outcome == "loss", (
|
| 281 |
f"{level} seed={s}: stall must LOSE; got {res.outcome} "
|
|
@@ -283,17 +316,16 @@ def test_stall_policy_loses(level):
|
|
| 283 |
)
|
| 284 |
|
| 285 |
|
| 286 |
-
@pytest.mark.parametrize("level", ["medium", "hard"])
|
| 287 |
def test_brute_attack_move_loses(level):
|
| 288 |
"""Brute attack_move east must LOSE — no disengage; the hunt-bot
|
| 289 |
-
heavy closes to point-blank and out-trades the column
|
| 290 |
-
survival bar is met."""
|
| 291 |
pytest.importorskip("openra_train")
|
| 292 |
from openra_bench.eval_core import run_level
|
| 293 |
|
| 294 |
c = compile_level(load_pack(PACK_PATH), level)
|
| 295 |
-
|
| 296 |
-
for s in seeds:
|
| 297 |
res = run_level(c, _brute_attack_move_policy, seed=s)
|
| 298 |
assert res.outcome == "loss", (
|
| 299 |
f"{level} seed={s}: brute attack_move must LOSE; got "
|
|
@@ -302,57 +334,38 @@ def test_brute_attack_move_loses(level):
|
|
| 302 |
)
|
| 303 |
|
| 304 |
|
| 305 |
-
@pytest.mark.
|
| 306 |
-
reason=(
|
| 307 |
-
"Engine note (verified 2026-05-19): a tightly-focused stand-and-"
|
| 308 |
-
"shoot (attack_unit on the heavy, hold position at the off-axis "
|
| 309 |
-
"staging) trades exactly 1 raider for the kill on current engine "
|
| 310 |
-
"numbers — clears the own_units_gte:2 bar with 2 survivors. The "
|
| 311 |
-
"KITE policy is strictly STRONGER on the rigor axis (preserves "
|
| 312 |
-
"more raider HP and more flexible against multi-tank variants) "
|
| 313 |
-
"but both pass the bar as currently stated. Bar-tightening to "
|
| 314 |
-
"own_units_gte:3 is engine-blocked: the kite cycle cannot "
|
| 315 |
-
"preserve all 3 raiders under hunt-bot close-range geometry. "
|
| 316 |
-
"Tracked for a follow-up engine pass that makes heavy close-range "
|
| 317 |
-
"volleys AoE the raider stack."
|
| 318 |
-
),
|
| 319 |
-
strict=False,
|
| 320 |
-
)
|
| 321 |
-
@pytest.mark.parametrize("level", ["medium", "hard"])
|
| 322 |
def test_stand_and_shoot_loses(level):
|
| 323 |
-
"""Stand-and-shoot
|
| 324 |
-
cannon out-trades raider weapons head-on.
|
| 325 |
-
|
|
|
|
|
|
|
| 326 |
pytest.importorskip("openra_train")
|
| 327 |
from openra_bench.eval_core import run_level
|
| 328 |
|
| 329 |
c = compile_level(load_pack(PACK_PATH), level)
|
| 330 |
-
|
| 331 |
-
for s in seeds:
|
| 332 |
res = run_level(c, _stand_and_shoot_policy, seed=s)
|
| 333 |
assert res.outcome == "loss", (
|
| 334 |
-
f"{level} seed={s}: stand-and-shoot
|
| 335 |
f"{res.outcome} killed={res.signals.units_killed} "
|
| 336 |
f"lost={res.signals.units_lost}"
|
| 337 |
)
|
| 338 |
|
| 339 |
|
| 340 |
-
@pytest.mark.parametrize("level", ["medium", "hard"])
|
| 341 |
def test_intended_kite_wins(level):
|
| 342 |
"""Intended kite cycle (reactive move-away + attack_unit) — the
|
| 343 |
spec's load-bearing decision: each turn, if the heavy is within
|
| 344 |
-
KITE_TRIGGER cells, retreat along the lane; otherwise
|
| 345 |
-
the nearest tank. Verified WINNING on every
|
| 346 |
-
|
| 347 |
-
y=10 corridor, heavy on y=20) — the y-axis lag in the hunt-bot
|
| 348 |
-
centroid chase gives the kite cycle a reactive window.
|
| 349 |
-
"""
|
| 350 |
pytest.importorskip("openra_train")
|
| 351 |
from openra_bench.eval_core import run_level
|
| 352 |
|
| 353 |
c = compile_level(load_pack(PACK_PATH), level)
|
| 354 |
-
|
| 355 |
-
for s in seeds:
|
| 356 |
res = run_level(c, _intended_kite_policy, seed=s)
|
| 357 |
assert res.outcome == "win", (
|
| 358 |
f"{level} seed={s}: intended kite should WIN, got {res.outcome} "
|
|
|
|
| 1 |
"""combat-kite-jeep-vs-tank — kite a slow heavy unit with fast raiders.
|
| 2 |
|
| 3 |
+
Bar (recalibrated 2026-05-20 after the OpenRA-Rust engine movement
|
| 4 |
+
fixes — moving units now fire AND take fire en route, and attack_unit
|
| 5 |
+
on an out-of-sight target paths normally instead of teleporting).
|
| 6 |
+
Those fixes (a) made the old 1tnk-light-tank easy variant fully
|
| 7 |
+
degenerate — every policy, pure stall included, killed the weak light
|
| 8 |
+
tank for free — and (b) shifted the close-range trade so a static
|
| 9 |
+
attack_unit stand now trades exactly ONE raider for the kill. The
|
| 10 |
+
recalibration:
|
| 11 |
+
|
| 12 |
+
• easy now uses the same 3tnk Soviet heavy as medium/hard, staged
|
| 13 |
+
CLOSER (x≈70) so the kite-band is widest (the easy difficulty
|
| 14 |
+
axis is band width, not a weaker enemy);
|
| 15 |
+
• the survival bar is own_units_gte:3 on EVERY tier — kiting kills
|
| 16 |
+
the heavy losing ZERO raiders, every non-kite policy loses ≥1, so
|
| 17 |
+
"lose no raider" is the load-bearing teeth.
|
| 18 |
+
|
| 19 |
+
The four script-policy proxies, every level, seeds 1-4:
|
| 20 |
+
|
| 21 |
+
• stall (observe only) → LOSS — the hunt-bot heavy closes and
|
| 22 |
+
grinds a raider down (killed 0, lost 1).
|
| 23 |
+
• stand-and-shoot → LOSS — attack_unit the heavy without
|
| 24 |
+
disengaging; the cannon out-trades raider weapons at close range
|
| 25 |
+
and one raider falls (killed 0-1, lost 1).
|
| 26 |
+
• brute attack_move east → LOSS — no disengage; the column
|
| 27 |
+
meets the heavy point-blank and loses a raider (killed 0, lost
|
| 28 |
+
1-2).
|
| 29 |
+
• intended kite cycle → WIN — when the heavy is within ~5
|
| 30 |
+
cells move the raiders away along the lane, else attack_unit it;
|
| 31 |
+
repeat. Kills the heavy keeping all three raiders (killed 1,
|
| 32 |
+
lost 0).
|
| 33 |
"""
|
| 34 |
|
| 35 |
from __future__ import annotations
|
|
|
|
| 74 |
|
| 75 |
def test_predicates_easy():
|
| 76 |
c = compile_level(load_pack(PACK_PATH), "easy")
|
| 77 |
+
raiders3 = [(28, 9), (30, 10), (28, 11)]
|
| 78 |
|
| 79 |
+
# Intended: 1 kill (the heavy), all 3 raiders alive, in time → WIN
|
| 80 |
assert evaluate(c.win_condition, _ctx(raiders3, tick=3000, killed=1, lost=0))
|
| 81 |
+
# 1 loss (only 2 raiders) → win fails (the bar is own_units_gte:3)
|
| 82 |
+
assert not evaluate(
|
| 83 |
+
c.win_condition, _ctx(raiders3[:2], tick=3000, killed=1, lost=1)
|
| 84 |
+
)
|
| 85 |
+
# 0 kills → win fails
|
| 86 |
+
assert not evaluate(c.win_condition, _ctx(raiders3, tick=3000, killed=0, lost=0))
|
| 87 |
+
# 1 raider lost → fail clause fires (own_units_gte:3 busts)
|
| 88 |
+
assert evaluate(c.fail_condition, _ctx(raiders3[:2], tick=3000, killed=1, lost=1))
|
| 89 |
# Past deadline → real loss, reachable within max_turns
|
| 90 |
assert evaluate(c.fail_condition, _ctx(raiders3, tick=4502, killed=0, lost=0))
|
| 91 |
assert 4501 <= 93 + 90 * (c.max_turns - 1), (
|
|
|
|
| 93 |
)
|
| 94 |
|
| 95 |
|
| 96 |
+
def test_predicates_medium_force_preservation_bar():
|
| 97 |
c = compile_level(load_pack(PACK_PATH), "medium")
|
| 98 |
+
raiders3 = [(28, 9), (30, 10), (28, 11)]
|
| 99 |
raiders2 = raiders3[:2]
|
|
|
|
| 100 |
|
| 101 |
+
# Intended: 1 kill, all 3 raiders alive → WIN
|
| 102 |
assert evaluate(c.win_condition, _ctx(raiders3, tick=3000, killed=1, lost=0))
|
| 103 |
+
# 1 raider lost → predicate fails (need ≥3)
|
| 104 |
+
assert not evaluate(c.win_condition, _ctx(raiders2, tick=3000, killed=1, lost=1))
|
|
|
|
| 105 |
# 0 kills → predicate fails
|
| 106 |
assert not evaluate(c.win_condition, _ctx(raiders3, tick=3000, killed=0, lost=0))
|
| 107 |
+
# 1 raider lost → fail clause fires
|
| 108 |
+
assert evaluate(c.fail_condition, _ctx(raiders2, tick=3000, killed=1, lost=1))
|
| 109 |
# Past deadline → real loss, reachable
|
| 110 |
assert evaluate(c.fail_condition, _ctx(raiders3, tick=4502, killed=0, lost=0))
|
| 111 |
assert 4501 <= 93 + 90 * (c.max_turns - 1)
|
|
|
|
| 115 |
c = compile_level(load_pack(PACK_PATH), "hard")
|
| 116 |
raiders3 = [(28, 9), (30, 10), (28, 11)]
|
| 117 |
|
| 118 |
+
# Intended: 1 kill, all 3 alive, in time → WIN
|
| 119 |
assert evaluate(c.win_condition, _ctx(raiders3, tick=3000, killed=1, lost=0))
|
| 120 |
+
# 1 raider lost → predicate fails (need ≥3)
|
| 121 |
assert not evaluate(
|
| 122 |
+
c.win_condition, _ctx(raiders3[:2], tick=3000, killed=1, lost=1)
|
| 123 |
)
|
| 124 |
# Past the tighter deadline → real loss, reachable
|
| 125 |
assert evaluate(c.fail_condition, _ctx(raiders3, tick=3602, killed=0, lost=0))
|
|
|
|
| 157 |
assert c.win_condition is not None and c.fail_condition is not None
|
| 158 |
|
| 159 |
|
| 160 |
+
def test_force_preservation_bar_is_lose_zero_on_every_tier():
|
| 161 |
+
"""The recalibrated bar is own_units_gte:3 on every level — a
|
| 162 |
+
policy that loses even ONE of the three raiders fails the win and
|
| 163 |
+
trips the fail clause. This is what makes the kite cycle (lose
|
| 164 |
+
zero) load-bearing against a stand-and-shoot (lose one)."""
|
| 165 |
+
pack = load_pack(PACK_PATH)
|
| 166 |
+
raiders3 = [(28, 9), (30, 10), (28, 11)]
|
| 167 |
+
for lvl in ("easy", "medium", "hard"):
|
| 168 |
+
c = compile_level(pack, lvl)
|
| 169 |
+
# all 3 alive + a kill → WIN
|
| 170 |
+
assert evaluate(c.win_condition, _ctx(raiders3, tick=2000, killed=1))
|
| 171 |
+
# exactly one raider lost → NOT a win, and fail clause fires
|
| 172 |
+
assert not evaluate(
|
| 173 |
+
c.win_condition, _ctx(raiders3[:2], tick=2000, killed=1, lost=1)
|
| 174 |
+
)
|
| 175 |
+
assert evaluate(
|
| 176 |
+
c.fail_condition, _ctx(raiders3[:2], tick=2000, killed=1, lost=1)
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
def test_timeout_loss_is_reachable_on_every_level():
|
| 181 |
"""No draw degeneracy: the after_ticks deadline fits inside
|
| 182 |
max_turns on every level (∼90 ticks/turn ⇒ 93 + 90·(max_turns-1))."""
|
|
|
|
| 195 |
# KITE_TRIGGER cells of a raider, move that raider RETREAT_DIST cells
|
| 196 |
# AWAY from the tank along the lane; otherwise attack_unit the
|
| 197 |
# nearest tank. The cycle is purely reactive — no memory, derived
|
| 198 |
+
# each turn from geometry. KITE_TRIGGER=5 is the proximity threshold
|
| 199 |
+
# that generalises across all tiers and both hard spawn corridors
|
| 200 |
+
# (the recalibrated kite band).
|
| 201 |
|
| 202 |
+
KITE_TRIGGER = 5
|
| 203 |
+
RETREAT_DIST = 12
|
| 204 |
|
| 205 |
|
| 206 |
def _tanks(enemies):
|
|
|
|
| 212 |
|
| 213 |
|
| 214 |
def _stall_policy(rs, Command):
|
| 215 |
+
"""Stall: only observe. The hunt-bot heavy closes on the idle
|
| 216 |
+
raider stack and grinds a raider down → own_units_gte:3 busts."""
|
| 217 |
return [Command.observe()]
|
| 218 |
|
| 219 |
|
| 220 |
def _stand_and_shoot_policy(rs, Command):
|
| 221 |
+
"""Stand at staging, attack_unit the heavy when visible — never
|
| 222 |
+
disengage. The cannon out-trades raider weapons head-on; one
|
| 223 |
+
raider falls and the own_units_gte:3 survival bar fails."""
|
| 224 |
units = rs.get("units_summary", []) or []
|
| 225 |
enemies = rs.get("enemy_summary", []) or []
|
| 226 |
tanks = _tanks(enemies)
|
|
|
|
| 243 |
|
| 244 |
def _brute_attack_move_policy(rs, Command):
|
| 245 |
"""Brute: one attack_move order eastward. No disengage; the
|
| 246 |
+
column meets the hunt-bot heavy and loses a raider in the same
|
| 247 |
+
close-range trade as stand-and-shoot."""
|
| 248 |
units = rs.get("units_summary", []) or []
|
| 249 |
if not units:
|
| 250 |
return [Command.observe()]
|
|
|
|
| 299 |
return cmds
|
| 300 |
|
| 301 |
|
| 302 |
+
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
|
| 303 |
def test_stall_policy_loses(level):
|
| 304 |
+
"""Stall must LOSE on every level/seed — the hunt-bot heavy closes
|
| 305 |
+
on the idle raider stack and grinds a raider down (own_units_gte:3
|
| 306 |
+
busts)."""
|
| 307 |
pytest.importorskip("openra_train")
|
| 308 |
from openra_bench.eval_core import run_level
|
| 309 |
|
| 310 |
c = compile_level(load_pack(PACK_PATH), level)
|
| 311 |
+
for s in (1, 2, 3, 4):
|
|
|
|
| 312 |
res = run_level(c, _stall_policy, seed=s)
|
| 313 |
assert res.outcome == "loss", (
|
| 314 |
f"{level} seed={s}: stall must LOSE; got {res.outcome} "
|
|
|
|
| 316 |
)
|
| 317 |
|
| 318 |
|
| 319 |
+
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
|
| 320 |
def test_brute_attack_move_loses(level):
|
| 321 |
"""Brute attack_move east must LOSE — no disengage; the hunt-bot
|
| 322 |
+
heavy closes to point-blank and out-trades the column, costing a
|
| 323 |
+
raider before the survival bar is met."""
|
| 324 |
pytest.importorskip("openra_train")
|
| 325 |
from openra_bench.eval_core import run_level
|
| 326 |
|
| 327 |
c = compile_level(load_pack(PACK_PATH), level)
|
| 328 |
+
for s in (1, 2, 3, 4):
|
|
|
|
| 329 |
res = run_level(c, _brute_attack_move_policy, seed=s)
|
| 330 |
assert res.outcome == "loss", (
|
| 331 |
f"{level} seed={s}: brute attack_move must LOSE; got "
|
|
|
|
| 334 |
)
|
| 335 |
|
| 336 |
|
| 337 |
+
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
def test_stand_and_shoot_loses(level):
|
| 339 |
+
"""Stand-and-shoot must LOSE on every level/seed — the heavy tank's
|
| 340 |
+
cannon out-trades raider weapons head-on. With the recalibrated
|
| 341 |
+
own_units_gte:3 bar a stand trades exactly one raider for the kill
|
| 342 |
+
and busts the survival cap (was an xfail on the pre-fix engine,
|
| 343 |
+
now a strict LOSS)."""
|
| 344 |
pytest.importorskip("openra_train")
|
| 345 |
from openra_bench.eval_core import run_level
|
| 346 |
|
| 347 |
c = compile_level(load_pack(PACK_PATH), level)
|
| 348 |
+
for s in (1, 2, 3, 4):
|
|
|
|
| 349 |
res = run_level(c, _stand_and_shoot_policy, seed=s)
|
| 350 |
assert res.outcome == "loss", (
|
| 351 |
+
f"{level} seed={s}: stand-and-shoot must LOSE; got "
|
| 352 |
f"{res.outcome} killed={res.signals.units_killed} "
|
| 353 |
f"lost={res.signals.units_lost}"
|
| 354 |
)
|
| 355 |
|
| 356 |
|
| 357 |
+
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
|
| 358 |
def test_intended_kite_wins(level):
|
| 359 |
"""Intended kite cycle (reactive move-away + attack_unit) — the
|
| 360 |
spec's load-bearing decision: each turn, if the heavy is within
|
| 361 |
+
KITE_TRIGGER (~5) cells, retreat along the lane; otherwise
|
| 362 |
+
attack_unit the nearest tank. Verified WINNING on every level and
|
| 363 |
+
every seed (1..4) — kills the heavy keeping all three raiders."""
|
|
|
|
|
|
|
|
|
|
| 364 |
pytest.importorskip("openra_train")
|
| 365 |
from openra_bench.eval_core import run_level
|
| 366 |
|
| 367 |
c = compile_level(load_pack(PACK_PATH), level)
|
| 368 |
+
for s in (1, 2, 3, 4):
|
|
|
|
| 369 |
res = run_level(c, _intended_kite_policy, seed=s)
|
| 370 |
assert res.outcome == "win", (
|
| 371 |
f"{level} seed={s}: intended kite should WIN, got {res.outcome} "
|