Spaces:
Running
Running
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 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 36 |
-
# objective plus a 4tnk heavy tank
|
| 37 |
-
#
|
| 38 |
-
#
|
| 39 |
-
# before the deadline — the e3 wall outranges and out-damages
|
| 40 |
-
#
|
|
|
|
|
|
|
| 41 |
# intended play commits ALL six tanks plus the four e3 escorts
|
| 42 |
-
# straight east,
|
| 43 |
-
# the
|
| 44 |
-
#
|
| 45 |
#
|
| 46 |
-
# Engine
|
| 47 |
-
#
|
| 48 |
-
#
|
| 49 |
-
#
|
| 50 |
-
#
|
| 51 |
-
#
|
| 52 |
-
#
|
| 53 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 87 |
-
#
|
| 88 |
-
#
|
| 89 |
-
#
|
| 90 |
-
#
|
| 91 |
-
#
|
| 92 |
-
#
|
| 93 |
-
#
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
- {type:
|
| 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 (
|
| 111 |
-
# + 1× 4tnk). A preserve-the-force plan
|
| 112 |
-
# all-in commit razes the fact
|
|
|
|
| 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
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
preserve-the-force approach times
|
| 121 |
-
the
|
| 122 |
-
|
|
|
|
| 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: [
|
| 131 |
-
- {type: e3, owner: enemy, position: [
|
| 132 |
-
- {type: 4tnk, owner: enemy, position: [
|
| 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
|
|
|
|
| 149 |
medium:
|
| 150 |
description: >
|
| 151 |
Heavier picket (a 3-rocket-infantry wall spread y=16/20/24 at
|
| 152 |
-
x=
|
| 153 |
-
|
| 154 |
-
|
| 155 |
preserve-the-force approach reliably times out — the e3
|
| 156 |
-
outrange your tanks in any standoff
|
| 157 |
-
|
|
|
|
| 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
|
| 172 |
-
#
|
|
|
|
| 173 |
hard:
|
| 174 |
description: >
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
the
|
| 179 |
-
required —
|
| 180 |
-
plan and any partial commit both time out; only
|
| 181 |
-
whole-force all-in commit
|
|
|
|
| 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
|
| 199 |
-
#
|
| 200 |
-
#
|
| 201 |
-
#
|
| 202 |
-
#
|
| 203 |
-
#
|
| 204 |
-
|
| 205 |
-
- {type: e3, owner: enemy, position: [
|
| 206 |
-
- {type: e3, owner: enemy, position: [
|
| 207 |
-
- {type: e3, owner: enemy, position: [
|
| 208 |
-
- {type: 4tnk, owner: enemy, position: [
|
| 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
|
| 5 |
-
enemy fact (110, 20),
|
| 6 |
-
|
| 7 |
-
(
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
Recalibrated after the engine
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
objective fact the moment it appears
|
| 339 |
-
|
| 340 |
-
fact
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
)
|