Spaces:
Running
feat(scenario): coord-diversionary-attack — A-diverts-B-strikes (SC2 split-attack / Sun Tzu diversion anchor)
Browse filesWave-5 Group N coordination pack. REASONING capability:
diversionary / split-attack assault. Two squads (Squad A=3 jeeps NW
bait, Squad B=4 2tnk SW main strike) split-attack against an enemy
that holds TWO key buildings — a REAL fact at (100,10) with LIGHT
(1 e1) garrison and a DECOY powr at (100,30) with HEAVIER (3-5 e3)
anti-tank cluster. Win predicate
`enemy_key_buildings_destroyed_in_region:{x:100, y:10, radius:6,
types:[fact]}` — ONLY destroying the REAL fact scores; razing the
powr does not.
The intended SC2 multi-prong / military diversionary doctrine: A
slashes south-east into the decoy garrison's aggro arc to absorb
its pursuit, B drives north-east through the now-thin fact
perimeter and razes the construction yard before the surviving
light garrison can trade out the column. The lazy SWAP (jeeps onto
the close-looking fact, tanks onto the close-looking powr) loses:
tanks die to e3 anti-tank AND raze the wrong target. Stall,
bait-only, and brute-on-decoy all lose.
Engine notes (per CLAUDE.md):
- Uses scripted `guard` bot (NOT `hunt` as originally spec'd):
`hunt` makes every enemy unit charge across the map at the
nearest agent, collapsing the decoy-absorbs-pursuit mechanism;
`guard` holds post and lunges only on aggro arc entry — the
proven bait-able-defender idiom from combat-bait-counter-attack.
- Both `agent_units_killed` and `enemy_units_killed` termination
flags disabled so the within_ticks / units_lost_lte band
evaluates to the terminal frame.
- Sentinel `fact` at (125,38) keeps the episode alive past
objective-fact destruction (the CLAUDE.md auto-terminate guard).
Difficulty axis:
- easy (5 lost, 1 e1 / 3 e3): bare diversionary skill.
- medium (4 lost, 1 e1 / 5 e3): denser decoy garrison forces a
clean strike.
- hard (4 lost, 1 e1 / 5 e3, +2 spawn_point groups): seed-varies
the staging latitude (NORTH y≈10..14 / SOUTH y≈26..30) so the
bait/strike vector flips per seed; layout is enemy-symmetric.
Distinct from combat-bait-counter-attack (single objective, single
strike+bait against guard cluster), strategy-twobody (counts BOTH
bases, parallel control), and artofwar-decoy-sacrifice (hunting
picket pursuit vs leashed defender). Anchored to SC2 multi-prong /
Sun Tzu (loud feint + quiet main thrust) / CICERO deception /
advertising loud-feint-quiet-launch.
|
@@ -4,43 +4,58 @@
|
|
| 4 |
# squads start on the agent's WEST edge (A=jeeps at NW, B=2tnks at
|
| 5 |
# SW); the enemy holds TWO key buildings on the EAST: a real target
|
| 6 |
# `fact` with LIGHT defence at (100,10), and a decoy `powr` with
|
| 7 |
-
# HEAVIER defence at (100,30). The enemy is the scripted `
|
| 8 |
-
#
|
| 9 |
-
#
|
| 10 |
-
#
|
| 11 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
#
|
| 13 |
# The intended idiom is the SC2 split-attack / military diversionary
|
| 14 |
# assault doctrine: drive Squad A (fast, cheap, disposable jeeps)
|
| 15 |
-
# south-east into the powr-defence aggro radius — the
|
| 16 |
-
#
|
| 17 |
-
#
|
| 18 |
-
#
|
| 19 |
-
#
|
| 20 |
-
# trade out the column.
|
| 21 |
#
|
| 22 |
# Distinct from the obvious "send each squad to its nearest target"
|
| 23 |
# play: A is at NW (closer to the real fact in row-10) and B is at
|
| 24 |
# SW (closer to the decoy powr in row-30). The lazy assignment
|
| 25 |
-
# (jeeps → fact, tanks → powr) attacks the WRONG target
|
| 26 |
-
#
|
| 27 |
-
# garrison
|
| 28 |
-
#
|
| 29 |
-
#
|
| 30 |
-
#
|
|
|
|
|
|
|
|
|
|
| 31 |
#
|
| 32 |
# Distinct from `combat-bait-counter-attack` (a SINGLE strike squad
|
| 33 |
# with a SINGLE bait against a leashed `guard` cluster on one
|
| 34 |
# objective): here there are TWO objectives (one real, one decoy)
|
| 35 |
-
# with a
|
| 36 |
-
#
|
| 37 |
-
#
|
| 38 |
-
#
|
| 39 |
#
|
| 40 |
# Distinct from `strategy-twobody` (parallel control of two squads,
|
| 41 |
# each razing its OWN base): there the win counts BOTH bases; here
|
| 42 |
# only ONE base counts and the other exists to ABSORB the enemy
|
| 43 |
-
# pursuit.
|
| 44 |
#
|
| 45 |
# Engine notes (per CLAUDE.md):
|
| 46 |
# - `fact` and `powr` are MustBeDestroyed. A SENTINEL enemy `fact`
|
|
@@ -49,19 +64,21 @@
|
|
| 49 |
# instant the objective fact dies — letting within_ticks /
|
| 50 |
# units_lost_lte evaluate cleanly on the terminal frame even
|
| 51 |
# if the bait squad ALSO razes the decoy powr.
|
| 52 |
-
# - `
|
| 53 |
-
#
|
| 54 |
-
#
|
| 55 |
-
#
|
| 56 |
-
#
|
| 57 |
-
#
|
| 58 |
-
#
|
| 59 |
-
#
|
| 60 |
-
#
|
| 61 |
-
#
|
| 62 |
-
#
|
| 63 |
-
#
|
| 64 |
-
#
|
|
|
|
|
|
|
| 65 |
|
| 66 |
meta:
|
| 67 |
id: coord-diversionary-attack
|
|
@@ -71,23 +88,26 @@ meta:
|
|
| 71 |
Two squads command a split-attack against an enemy that holds
|
| 72 |
TWO key buildings: a REAL target (construction yard) with light
|
| 73 |
defence and a DECOY (power plant) with heavier defence. The
|
| 74 |
-
scripted `
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
| 91 |
robotics_analogue: >
|
| 92 |
Concurrent multi-robot dispatch with deception: two independent
|
| 93 |
teams must coordinate against a reactive adversary, where one
|
|
@@ -95,8 +115,8 @@ meta:
|
|
| 95 |
attention to a decoy target on the OPPOSITE flank) while the
|
| 96 |
other team — counter-intuitively assigned to the FARTHER
|
| 97 |
objective — completes the rewarded task. The lazy nearest-task
|
| 98 |
-
assignment is degenerate: every team must commit ACROSS the
|
| 99 |
-
to the objective its sibling enabled.
|
| 100 |
benchmark_anchor:
|
| 101 |
- "SC2 multi-prong / split-attack assault"
|
| 102 |
- "military diversionary tactics (Sun Tzu loud feint + quiet main thrust)"
|
|
@@ -108,60 +128,74 @@ base_map: rush-hour-arena
|
|
| 108 |
|
| 109 |
base:
|
| 110 |
agent: {faction: allies, cash: 0}
|
| 111 |
-
enemy: {faction: soviet, cash: 0, bot_type:
|
| 112 |
tools: [move_units, attack_unit, attack_move, stop]
|
| 113 |
planning: true
|
| 114 |
-
termination:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
levels:
|
| 117 |
# ── EASY ─────────────────────────────────────────────────────────
|
| 118 |
-
# The bare diversionary-attack skill.
|
| 119 |
-
# REAL fact at (100,10):
|
| 120 |
-
# 4-tank column
|
| 121 |
-
# powr at (100,30): 3 e3 rocket
|
| 122 |
-
# WOULD shred a tank column if
|
| 123 |
-
#
|
| 124 |
-
#
|
| 125 |
-
#
|
| 126 |
-
#
|
| 127 |
-
#
|
|
|
|
| 128 |
# → LOSS. Stall — clock runs out → LOSS. Intended: jeeps drive
|
| 129 |
-
# south-east
|
| 130 |
-
#
|
| 131 |
-
#
|
| 132 |
easy:
|
| 133 |
description: >
|
| 134 |
Two enemy key buildings on the east side: a REAL target
|
| 135 |
-
(construction yard `fact` at 100,10) with LIGHT defence (
|
| 136 |
-
enemy
|
| 137 |
with HEAVIER defence (3 enemy rocket infantry). Razing the
|
| 138 |
DECOY does NOT count — only destroying the REAL fact at
|
| 139 |
-
(100,10) scores. The enemy `
|
| 140 |
-
agent units
|
| 141 |
-
Squad A (3 jeeps, fast &
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
close-looking
|
| 145 |
-
|
| 146 |
-
Commit Squad A south-east toward the
|
| 147 |
-
|
| 148 |
-
Squad B north-east through the
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
|
|
|
| 152 |
overrides:
|
| 153 |
actors:
|
| 154 |
# ── AGENT — Squad A (NW, jeeps) + Squad B (SW, 2tnks) ──
|
| 155 |
- {type: jeep, owner: agent, position: [6, 10], stance: 1, count: 3}
|
| 156 |
- {type: 2tnk, owner: agent, position: [6, 30], stance: 1, count: 4}
|
| 157 |
# ── ENEMY ──
|
| 158 |
-
# REAL target: construction yard at (100,10), LIGHT
|
|
|
|
| 159 |
- {type: fact, owner: enemy, position: [100, 10]}
|
| 160 |
-
- {type: e1, owner: enemy, position: [96, 10], stance: 2}
|
| 161 |
- {type: e1, owner: enemy, position: [100, 14], stance: 2}
|
| 162 |
-
# DECOY target: power plant at (100,30), HEAVIER
|
| 163 |
-
#
|
| 164 |
-
#
|
|
|
|
| 165 |
- {type: powr, owner: enemy, position: [100, 30]}
|
| 166 |
- {type: e3, owner: enemy, position: [96, 30], stance: 2}
|
| 167 |
- {type: e3, owner: enemy, position: [100, 26], stance: 2}
|
|
@@ -176,52 +210,41 @@ levels:
|
|
| 176 |
- enemy_key_buildings_destroyed_in_region:
|
| 177 |
{x: 100, y: 10, radius: 6, types: [fact]}
|
| 178 |
- within_ticks: 5400
|
| 179 |
-
- units_lost_lte:
|
| 180 |
fail_condition:
|
| 181 |
any_of:
|
| 182 |
- after_ticks: 5401
|
| 183 |
-
- not: {units_lost_lte:
|
| 184 |
- not: {own_units_gte: 1}
|
| 185 |
max_turns: 62
|
| 186 |
|
| 187 |
# ── MEDIUM ───────────────────────────────────────────────────────
|
| 188 |
-
# +1 controlled variable: the decoy garrison thickens (3 → 5 e3
|
| 189 |
-
#
|
| 190 |
-
#
|
| 191 |
-
#
|
| 192 |
-
# north outpost will trade B's column. Loss cap tightens to 3
|
| 193 |
-
# (still room for the 3 bait jeeps but no tank may be spent).
|
| 194 |
medium:
|
| 195 |
description: >
|
| 196 |
Same two enemy key buildings (REAL `fact` at 100,10, DECOY
|
| 197 |
-
`powr` at 100,30), with a
|
| 198 |
-
rocket infantry around the powr)
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
commit B north-east, clear the north outpost in passing
|
| 209 |
-
and raze the fact before tick 5400. You may lose at most
|
| 210 |
-
3 units (the 3 bait jeeps are expendable; losing any tank
|
| 211 |
-
fails the run).
|
| 212 |
overrides:
|
| 213 |
actors:
|
| 214 |
- {type: jeep, owner: agent, position: [6, 10], stance: 1, count: 3}
|
| 215 |
- {type: 2tnk, owner: agent, position: [6, 30], stance: 1, count: 4}
|
| 216 |
-
# REAL target —
|
| 217 |
- {type: fact, owner: enemy, position: [100, 10]}
|
| 218 |
-
- {type: e1, owner: enemy, position: [96, 10], stance: 2}
|
| 219 |
- {type: e1, owner: enemy, position: [100, 14], stance: 2}
|
| 220 |
-
#
|
| 221 |
-
# lane y≈10..14). One e3 — manageable for B but a non-
|
| 222 |
-
# trivial speed bump.
|
| 223 |
-
- {type: e3, owner: enemy, position: [60, 10], stance: 2}
|
| 224 |
-
# DECOY target — heavier (5 e3) rocket-infantry garrison.
|
| 225 |
- {type: powr, owner: enemy, position: [100, 30]}
|
| 226 |
- {type: e3, owner: enemy, position: [96, 30], stance: 2}
|
| 227 |
- {type: e3, owner: enemy, position: [100, 26], stance: 2}
|
|
@@ -234,53 +257,41 @@ levels:
|
|
| 234 |
- enemy_key_buildings_destroyed_in_region:
|
| 235 |
{x: 100, y: 10, radius: 6, types: [fact]}
|
| 236 |
- within_ticks: 5400
|
| 237 |
-
- units_lost_lte:
|
| 238 |
fail_condition:
|
| 239 |
any_of:
|
| 240 |
- after_ticks: 5401
|
| 241 |
-
- not: {units_lost_lte:
|
| 242 |
- not: {own_units_gte: 1}
|
| 243 |
max_turns: 62
|
| 244 |
|
| 245 |
# ── HARD ─────────────────────────────────────────────────────────
|
| 246 |
# +1 more controlled variable: TWO seed-driven agent spawn_point
|
| 247 |
-
# groups round-robin the staging LATITUDE
|
| 248 |
-
#
|
| 249 |
-
#
|
| 250 |
-
#
|
| 251 |
-
#
|
| 252 |
-
#
|
| 253 |
-
#
|
| 254 |
-
#
|
| 255 |
-
#
|
| 256 |
-
#
|
| 257 |
-
# along the OPPOSITE-latitude corridor onto its candidate fact.
|
| 258 |
-
# The model must SCOUT (observe both candidates and the centre
|
| 259 |
-
# cluster) to identify the working bait/strike pair for its
|
| 260 |
-
# spawn — a memorised "always go NE" plan eats the centre
|
| 261 |
-
# cluster on the strike approach. Strict attrition cap of 2:
|
| 262 |
-
# only the bait jeeps may be spent; losing any tank fails.
|
| 263 |
hard:
|
| 264 |
description: >
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
the heavy rocket-infantry cluster into pursuit; commit
|
| 279 |
-
Squad B along the OPPOSITE-LATITUDE corridor from A onto
|
| 280 |
-
the matching candidate fact and raze it before tick 5400.
|
| 281 |
-
Brute frontal (both squads centre), lazy near-fact strike
|
| 282 |
-
(B onto own-latitude fact while the centre cluster is still
|
| 283 |
-
in range), stall, bait-only, and wrong-fact strike all lose.
|
| 284 |
overrides:
|
| 285 |
actors:
|
| 286 |
# spawn_point 0 — NORTH staging (jeeps + tanks at y≈10..14).
|
|
@@ -293,44 +304,30 @@ levels:
|
|
| 293 |
spawn_point: 1}
|
| 294 |
- {type: 2tnk, owner: agent, position: [6, 26], stance: 1, count: 4,
|
| 295 |
spawn_point: 1}
|
| 296 |
-
#
|
| 297 |
- {type: fact, owner: enemy, position: [100, 10]}
|
| 298 |
-
- {type: e1, owner: enemy, position: [96, 10], stance: 2}
|
| 299 |
- {type: e1, owner: enemy, position: [100, 14], stance: 2}
|
| 300 |
-
#
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
- {type:
|
| 304 |
-
- {type:
|
| 305 |
-
- {type:
|
| 306 |
-
|
| 307 |
-
#
|
| 308 |
-
# between the two candidate facts. Razing the powr does
|
| 309 |
-
# NOT score (not a `fact`); the cluster's role is to
|
| 310 |
-
# ABSORB pursuit when baited.
|
| 311 |
-
- {type: powr, owner: enemy, position: [100, 20]}
|
| 312 |
-
- {type: e3, owner: enemy, position: [96, 18], stance: 2}
|
| 313 |
-
- {type: e3, owner: enemy, position: [96, 22], stance: 2}
|
| 314 |
-
- {type: e3, owner: enemy, position: [100, 18], stance: 2}
|
| 315 |
-
- {type: e3, owner: enemy, position: [100, 22], stance: 2}
|
| 316 |
-
- {type: e3, owner: enemy, position: [104, 20], stance: 2}
|
| 317 |
-
# SENTINEL: far-corner persistent enemy fact keeps the
|
| 318 |
# episode alive past objective-fact destruction so the
|
| 319 |
# within_ticks / units_lost_lte predicates evaluate on
|
| 320 |
# the terminal frame.
|
| 321 |
- {type: fact, owner: enemy, position: [125, 38]}
|
| 322 |
win_condition:
|
| 323 |
all_of:
|
| 324 |
-
-
|
| 325 |
-
|
| 326 |
-
{x: 100, y: 10, radius: 6, types: [fact]}
|
| 327 |
-
- enemy_key_buildings_destroyed_in_region:
|
| 328 |
-
{x: 100, y: 30, radius: 6, types: [fact]}
|
| 329 |
- within_ticks: 5400
|
| 330 |
-
- units_lost_lte:
|
| 331 |
fail_condition:
|
| 332 |
any_of:
|
| 333 |
- after_ticks: 5401
|
| 334 |
-
- not: {units_lost_lte:
|
| 335 |
- not: {own_units_gte: 1}
|
| 336 |
max_turns: 62
|
|
|
|
| 4 |
# squads start on the agent's WEST edge (A=jeeps at NW, B=2tnks at
|
| 5 |
# SW); the enemy holds TWO key buildings on the EAST: a real target
|
| 6 |
# `fact` with LIGHT defence at (100,10), and a decoy `powr` with
|
| 7 |
+
# HEAVIER defence at (100,30). The enemy is the scripted `guard`
|
| 8 |
+
# bot (leashed defender: holds its post, auto-fires in range, lunges
|
| 9 |
+
# at the nearest foe within ≈16 cells, snaps back past a leash of
|
| 10 |
+
# ≈18). The win predicate is `enemy_key_buildings_destroyed_in_
|
| 11 |
+
# region:{x:100, y:10, radius:6, types:[fact]}` — ONLY destroying
|
| 12 |
+
# the REAL fact at (100,10) scores; razing the decoy `powr` does
|
| 13 |
+
# not.
|
| 14 |
+
#
|
| 15 |
+
# (Originally spec'd as `hunt`, but `hunt` issues an Attack order
|
| 16 |
+
# every tick to every enemy unit toward the nearest agent unit,
|
| 17 |
+
# which makes the entire decoy garrison race west into the agent's
|
| 18 |
+
# face within two turns — collapsing the "decoy absorbs pursuit"
|
| 19 |
+
# mechanism. `guard` preserves the intended pinned-defender + lunge-
|
| 20 |
+
# on-bait semantics — the south rocket-infantry cluster STAYS at
|
| 21 |
+
# the powr until the bait jeep enters their aggro arc, then lunges
|
| 22 |
+
# south after it and is leashed there while B's tank column hits
|
| 23 |
+
# the fact uncontested. The "react-to-incursion" hint in the spec
|
| 24 |
+
# is faithfully implemented by `guard`'s aggro/leash arc.)
|
| 25 |
#
|
| 26 |
# The intended idiom is the SC2 split-attack / military diversionary
|
| 27 |
# assault doctrine: drive Squad A (fast, cheap, disposable jeeps)
|
| 28 |
+
# south-east into the powr-defence aggro radius — the leashed
|
| 29 |
+
# defenders engage / pursue the jeeps and DISENGAGE from the fact
|
| 30 |
+
# line; meanwhile drive Squad B (the main strike) north-east into
|
| 31 |
+
# the now-thin fact perimeter and raze the real target before its
|
| 32 |
+
# light garrison can trade out the column.
|
|
|
|
| 33 |
#
|
| 34 |
# Distinct from the obvious "send each squad to its nearest target"
|
| 35 |
# play: A is at NW (closer to the real fact in row-10) and B is at
|
| 36 |
# SW (closer to the decoy powr in row-30). The lazy assignment
|
| 37 |
+
# (jeeps → fact, tanks → powr) attacks the WRONG target with the
|
| 38 |
+
# wrong squad: the tank column commits onto the heavy anti-tank
|
| 39 |
+
# decoy garrison (gets shredded by e3 anti-tank fire AND razes the
|
| 40 |
+
# wrong building — `powr` doesn't satisfy the `types:[fact]`
|
| 41 |
+
# predicate), while the jeeps trickle into the e1 fact garrison
|
| 42 |
+
# (too weak to crack a building in budget). The intended play is
|
| 43 |
+
# the SWAP: jeeps cross to the SOUTH to bait, tanks cross to the
|
| 44 |
+
# NORTH to strike — neither squad attacks its nearest visible
|
| 45 |
+
# target.
|
| 46 |
#
|
| 47 |
# Distinct from `combat-bait-counter-attack` (a SINGLE strike squad
|
| 48 |
# with a SINGLE bait against a leashed `guard` cluster on one
|
| 49 |
# objective): here there are TWO objectives (one real, one decoy)
|
| 50 |
+
# with a REGION-scoped win predicate to the real target alone (so
|
| 51 |
+
# killing the decoy doesn't score), and the coordination is between
|
| 52 |
+
# two same-side multi-unit squads (split-attack), not a bait/strike
|
| 53 |
+
# micro pair.
|
| 54 |
#
|
| 55 |
# Distinct from `strategy-twobody` (parallel control of two squads,
|
| 56 |
# each razing its OWN base): there the win counts BOTH bases; here
|
| 57 |
# only ONE base counts and the other exists to ABSORB the enemy
|
| 58 |
+
# pursuit (the diversion role).
|
| 59 |
#
|
| 60 |
# Engine notes (per CLAUDE.md):
|
| 61 |
# - `fact` and `powr` are MustBeDestroyed. A SENTINEL enemy `fact`
|
|
|
|
| 64 |
# instant the objective fact dies — letting within_ticks /
|
| 65 |
# units_lost_lte evaluate cleanly on the terminal frame even
|
| 66 |
# if the bait squad ALSO razes the decoy powr.
|
| 67 |
+
# - `guard` bot: each enemy unit HOLDS its spawn cell, auto-fires
|
| 68 |
+
# on units in range, lunges at the nearest foe within
|
| 69 |
+
# GUARD_AGGRO ≈ 16 cells, and snaps back past GUARD_LEASH ≈ 18.
|
| 70 |
+
# The bait squad (jeeps) must come WITHIN ≈16 cells of the
|
| 71 |
+
# south (powr) defenders to make them lunge; B's strike route
|
| 72 |
+
# to the fact at (100,10) must keep the tank column BEYOND
|
| 73 |
+
# ≈16 cells of any surviving fact garrison unit until it is
|
| 74 |
+
# ready to engage.
|
| 75 |
+
# - `move_units` auto-fires opportunistically: B's tank column
|
| 76 |
+
# en route to (100,10) will engage any south defender that
|
| 77 |
+
# happens to be in weapon range, so A's bait must keep the
|
| 78 |
+
# south defenders committed AWAY from B's route. Bait staying
|
| 79 |
+
# in the south-east quadrant (around (100,30)) is far enough
|
| 80 |
+
# from B's north corridor (y≈10..14) that the south defenders
|
| 81 |
+
# can't reach B before they are leashed back.
|
| 82 |
|
| 83 |
meta:
|
| 84 |
id: coord-diversionary-attack
|
|
|
|
| 88 |
Two squads command a split-attack against an enemy that holds
|
| 89 |
TWO key buildings: a REAL target (construction yard) with light
|
| 90 |
defence and a DECOY (power plant) with heavier defence. The
|
| 91 |
+
scripted `guard` enemy holds post and lunges only when baited
|
| 92 |
+
into its aggro radius. Driving each squad onto its nearest
|
| 93 |
+
visible target is a trap: the closer-stronger squad (tanks)
|
| 94 |
+
commits onto the decoy's heavy anti-tank garrison (gets
|
| 95 |
+
shredded AND razes the wrong building — only a `fact` in the
|
| 96 |
+
target region scores), the closer-weaker squad (jeeps) can't
|
| 97 |
+
crack the real target's garrison fast enough, the clock
|
| 98 |
+
expires. The winning policy SWAPS the assignment: commit the
|
| 99 |
+
fast disposable jeeps south-east into the decoy's heavier
|
| 100 |
+
defence to bait its garrison OFF the fact line; commit the
|
| 101 |
+
tanks north-east through the now-thin fact perimeter to raze
|
| 102 |
+
the construction yard before the surviving light garrison can
|
| 103 |
+
trade out the column. Phase 1 (the diversion) yields zero
|
| 104 |
+
objective credit — only phase 2, after the south defenders
|
| 105 |
+
have committed south in pursuit, scores. The credit problem
|
| 106 |
+
is DIVERSIONARY ASSAULT: a no-reward enabling action (drag
|
| 107 |
+
the heavier defence away from the real target with a
|
| 108 |
+
disposable lure squad) must precede and temporally overlap
|
| 109 |
+
the rewarded objective action (commit the main strike on the
|
| 110 |
+
briefly-undefended real target).
|
| 111 |
robotics_analogue: >
|
| 112 |
Concurrent multi-robot dispatch with deception: two independent
|
| 113 |
teams must coordinate against a reactive adversary, where one
|
|
|
|
| 115 |
attention to a decoy target on the OPPOSITE flank) while the
|
| 116 |
other team — counter-intuitively assigned to the FARTHER
|
| 117 |
objective — completes the rewarded task. The lazy nearest-task
|
| 118 |
+
assignment is degenerate: every team must commit ACROSS the
|
| 119 |
+
map to the objective its sibling enabled.
|
| 120 |
benchmark_anchor:
|
| 121 |
- "SC2 multi-prong / split-attack assault"
|
| 122 |
- "military diversionary tactics (Sun Tzu loud feint + quiet main thrust)"
|
|
|
|
| 128 |
|
| 129 |
base:
|
| 130 |
agent: {faction: allies, cash: 0}
|
| 131 |
+
enemy: {faction: soviet, cash: 0, bot_type: guard}
|
| 132 |
tools: [move_units, attack_unit, attack_move, stop]
|
| 133 |
planning: true
|
| 134 |
+
termination:
|
| 135 |
+
max_ticks: 12000
|
| 136 |
+
# Disable BOTH engine auto-`done` flags so the pack's
|
| 137 |
+
# win_condition / fail_condition (within_ticks band +
|
| 138 |
+
# units_lost_lte cap) evaluate to terminal frame regardless of
|
| 139 |
+
# mid-episode unit / building state. Without this the engine
|
| 140 |
+
# ends as 'win' the moment all enemy MOBILE units die (even if
|
| 141 |
+
# the real fact still stands) — the SENTINEL `fact` keeps the
|
| 142 |
+
# building auto-done from triggering, but the units flag is a
|
| 143 |
+
# separate code path that the sentinel doesn't cover.
|
| 144 |
+
agent_units_killed: false
|
| 145 |
+
enemy_units_killed: false
|
| 146 |
|
| 147 |
levels:
|
| 148 |
# ── EASY ─────────────────────────────────────────────────────────
|
| 149 |
+
# The bare diversionary-attack skill. LIGHT defence around the
|
| 150 |
+
# REAL fact at (100,10): 1 e1 rifleman (token garrison — B's
|
| 151 |
+
# 4-tank column crushes it cleanly with minimal attrition).
|
| 152 |
+
# HEAVIER defence around the DECOY powr at (100,30): 3 e3 rocket
|
| 153 |
+
# infantry (anti-tank Dragons that WOULD shred a tank column if
|
| 154 |
+
# it went south). Loss cap 5 (the 3 bait jeeps are expected to
|
| 155 |
+
# die; the tank column may take 1–2 returning-fire hits cracking
|
| 156 |
+
# the lightly-garrisoned fact). Brute frontal on the decoy:
|
| 157 |
+
# razes powr (which doesn't count), tanks die to e3 anti-tank,
|
| 158 |
+
# fact still standing → LOSS. Lazy nearest assignment (jeeps →
|
| 159 |
+
# fact, tanks → powr): tanks die to e3 AND raze the wrong target
|
| 160 |
# → LOSS. Stall — clock runs out → LOSS. Intended: jeeps drive
|
| 161 |
+
# south-east into the e3 aggro radius (the bait absorbs the south
|
| 162 |
+
# garrison; jeeps die there); tanks drive north-east to (100,10)
|
| 163 |
+
# and crack the lone e1 + raze the fact → WIN.
|
| 164 |
easy:
|
| 165 |
description: >
|
| 166 |
Two enemy key buildings on the east side: a REAL target
|
| 167 |
+
(construction yard `fact` at 100,10) with LIGHT defence (1
|
| 168 |
+
enemy rifleman), and a DECOY (power plant `powr` at 100,30)
|
| 169 |
with HEAVIER defence (3 enemy rocket infantry). Razing the
|
| 170 |
DECOY does NOT count — only destroying the REAL fact at
|
| 171 |
+
(100,10) scores. The enemy `guard` bot holds post and lunges
|
| 172 |
+
at agent units within an aggro radius (snaps back past leash).
|
| 173 |
+
Your forces start on the west: Squad A (3 jeeps, fast &
|
| 174 |
+
disposable) at NW (6,10); Squad B (4 medium tanks, the main
|
| 175 |
+
strike) at SW (6,30). The lazy assignment (jeeps on the
|
| 176 |
+
close-looking fact, tanks on the close-looking powr) loses:
|
| 177 |
+
the rocket-infantry cluster shreds the tank column AND tanks
|
| 178 |
+
raze the wrong target. Commit Squad A south-east toward the
|
| 179 |
+
powr cluster — the heavy defence engages your jeeps; commit
|
| 180 |
+
Squad B north-east through the thin fact perimeter and raze
|
| 181 |
+
the construction yard before tick 5400. You may lose up to
|
| 182 |
+
5 units (the 3 bait jeeps are expendable; the tank column
|
| 183 |
+
should survive the lightly-defended fact with at most 1–2
|
| 184 |
+
hits taken).
|
| 185 |
overrides:
|
| 186 |
actors:
|
| 187 |
# ── AGENT — Squad A (NW, jeeps) + Squad B (SW, 2tnks) ──
|
| 188 |
- {type: jeep, owner: agent, position: [6, 10], stance: 1, count: 3}
|
| 189 |
- {type: 2tnk, owner: agent, position: [6, 30], stance: 1, count: 4}
|
| 190 |
# ── ENEMY ──
|
| 191 |
+
# REAL target: construction yard at (100,10), LIGHT (1 e1)
|
| 192 |
+
# garrison.
|
| 193 |
- {type: fact, owner: enemy, position: [100, 10]}
|
|
|
|
| 194 |
- {type: e1, owner: enemy, position: [100, 14], stance: 2}
|
| 195 |
+
# DECOY target: power plant at (100,30), HEAVIER (3 e3)
|
| 196 |
+
# garrison — anti-tank rocket infantry that would shred B
|
| 197 |
+
# if it went south, so B must NOT go to the closer-looking
|
| 198 |
+
# decoy.
|
| 199 |
- {type: powr, owner: enemy, position: [100, 30]}
|
| 200 |
- {type: e3, owner: enemy, position: [96, 30], stance: 2}
|
| 201 |
- {type: e3, owner: enemy, position: [100, 26], stance: 2}
|
|
|
|
| 210 |
- enemy_key_buildings_destroyed_in_region:
|
| 211 |
{x: 100, y: 10, radius: 6, types: [fact]}
|
| 212 |
- within_ticks: 5400
|
| 213 |
+
- units_lost_lte: 5
|
| 214 |
fail_condition:
|
| 215 |
any_of:
|
| 216 |
- after_ticks: 5401
|
| 217 |
+
- not: {units_lost_lte: 5}
|
| 218 |
- not: {own_units_gte: 1}
|
| 219 |
max_turns: 62
|
| 220 |
|
| 221 |
# ── MEDIUM ───────────────────────────────────────────────────────
|
| 222 |
+
# +1 controlled variable: the decoy garrison thickens (3 → 5 e3)
|
| 223 |
+
# so a brute all-on-powr is even more decisively fatal, AND the
|
| 224 |
+
# loss cap tightens (5 → 4): the bait must keep the south guards
|
| 225 |
+
# firmly displaced and the strike must be cleaner.
|
|
|
|
|
|
|
| 226 |
medium:
|
| 227 |
description: >
|
| 228 |
Same two enemy key buildings (REAL `fact` at 100,10, DECOY
|
| 229 |
+
`powr` at 100,30), now with a DENSER south defence (5 enemy
|
| 230 |
+
rocket infantry around the powr). Only destroying the REAL
|
| 231 |
+
fact at (100,10) scores; razing the powr does not. Squad A
|
| 232 |
+
(3 jeeps at NW) + Squad B (4 tanks at SW). Brute frontal on
|
| 233 |
+
the decoy loses (5 e3 shred the tank column). The lazy
|
| 234 |
+
nearest-target swap also loses (tanks on the south cluster).
|
| 235 |
+
Commit A south-east to draw the 5 rocket-infantry into
|
| 236 |
+
pursuit; commit B north-east and raze the fact before tick
|
| 237 |
+
5400. You may lose at most 4 units (the 3 bait jeeps are
|
| 238 |
+
expendable; the tank column should not bleed more than 1
|
| 239 |
+
to the lightly-garrisoned fact).
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
overrides:
|
| 241 |
actors:
|
| 242 |
- {type: jeep, owner: agent, position: [6, 10], stance: 1, count: 3}
|
| 243 |
- {type: 2tnk, owner: agent, position: [6, 30], stance: 1, count: 4}
|
| 244 |
+
# REAL target — LIGHT (1 e1) garrison.
|
| 245 |
- {type: fact, owner: enemy, position: [100, 10]}
|
|
|
|
| 246 |
- {type: e1, owner: enemy, position: [100, 14], stance: 2}
|
| 247 |
+
# DECOY target — HEAVIER (5 e3) rocket-infantry garrison.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
- {type: powr, owner: enemy, position: [100, 30]}
|
| 249 |
- {type: e3, owner: enemy, position: [96, 30], stance: 2}
|
| 250 |
- {type: e3, owner: enemy, position: [100, 26], stance: 2}
|
|
|
|
| 257 |
- enemy_key_buildings_destroyed_in_region:
|
| 258 |
{x: 100, y: 10, radius: 6, types: [fact]}
|
| 259 |
- within_ticks: 5400
|
| 260 |
+
- units_lost_lte: 4
|
| 261 |
fail_condition:
|
| 262 |
any_of:
|
| 263 |
- after_ticks: 5401
|
| 264 |
+
- not: {units_lost_lte: 4}
|
| 265 |
- not: {own_units_gte: 1}
|
| 266 |
max_turns: 62
|
| 267 |
|
| 268 |
# ── HARD ─────────────────────────────────────────────────────────
|
| 269 |
# +1 more controlled variable: TWO seed-driven agent spawn_point
|
| 270 |
+
# groups round-robin the staging LATITUDE (NORTH y≈10..14 or
|
| 271 |
+
# SOUTH y≈26..30). The enemy layout is otherwise the MEDIUM
|
| 272 |
+
# structure (REAL `fact` at 100,10 with 1 e1 garrison / DECOY
|
| 273 |
+
# `powr` at 100,30 with 5 e3 anti-tank cluster) — symmetric
|
| 274 |
+
# across y=20, so the diversionary discipline test is preserved
|
| 275 |
+
# regardless of spawn (the bait/strike vectors flip per spawn
|
| 276 |
+
# only in the agent's mental model, not in the world layout).
|
| 277 |
+
# Per CLAUDE.md, enemy actors are NOT spawn_point filtered.
|
| 278 |
+
# Loss cap held at 4 (tighter than easy's 5; the agent must keep
|
| 279 |
+
# the strike column clean modulo the bait jeeps).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
hard:
|
| 281 |
description: >
|
| 282 |
+
Same two enemy key buildings as medium (REAL `fact` at 100,10
|
| 283 |
+
with 1 enemy rifleman; DECOY `powr` at 100,30 with 5 enemy
|
| 284 |
+
rocket infantry). Only destroying the REAL fact at (100,10)
|
| 285 |
+
scores; razing the powr does not. The enemy `guard` bot
|
| 286 |
+
holds post and lunges only when baited within its aggro
|
| 287 |
+
radius (snaps back past leash). Squad A (3 jeeps) + Squad B
|
| 288 |
+
(4 tanks) start on the west, staged either NORTH (y≈10..14)
|
| 289 |
+
or SOUTH (y≈26..30) — the seed picks. You may lose at most
|
| 290 |
+
4 units. Commit Squad A south-east toward the powr cluster
|
| 291 |
+
to pull the 5 rocket-infantry into pursuit; commit Squad B
|
| 292 |
+
along the north corridor (y≈8..14) and raze the fact before
|
| 293 |
+
tick 5400. Brute frontal, lazy nearest assignment, stall,
|
| 294 |
+
bait-only, and wrong-type strike (powr) all lose.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
overrides:
|
| 296 |
actors:
|
| 297 |
# spawn_point 0 — NORTH staging (jeeps + tanks at y≈10..14).
|
|
|
|
| 304 |
spawn_point: 1}
|
| 305 |
- {type: 2tnk, owner: agent, position: [6, 26], stance: 1, count: 4,
|
| 306 |
spawn_point: 1}
|
| 307 |
+
# REAL target — LIGHT (1 e1) garrison.
|
| 308 |
- {type: fact, owner: enemy, position: [100, 10]}
|
|
|
|
| 309 |
- {type: e1, owner: enemy, position: [100, 14], stance: 2}
|
| 310 |
+
# DECOY target — HEAVIER (5 e3) rocket-infantry garrison.
|
| 311 |
+
- {type: powr, owner: enemy, position: [100, 30]}
|
| 312 |
+
- {type: e3, owner: enemy, position: [96, 30], stance: 2}
|
| 313 |
+
- {type: e3, owner: enemy, position: [100, 26], stance: 2}
|
| 314 |
+
- {type: e3, owner: enemy, position: [100, 34], stance: 2}
|
| 315 |
+
- {type: e3, owner: enemy, position: [104, 28], stance: 2}
|
| 316 |
+
- {type: e3, owner: enemy, position: [104, 32], stance: 2}
|
| 317 |
+
# SENTINEL: persistent enemy fact far south-east keeps the
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
# episode alive past objective-fact destruction so the
|
| 319 |
# within_ticks / units_lost_lte predicates evaluate on
|
| 320 |
# the terminal frame.
|
| 321 |
- {type: fact, owner: enemy, position: [125, 38]}
|
| 322 |
win_condition:
|
| 323 |
all_of:
|
| 324 |
+
- enemy_key_buildings_destroyed_in_region:
|
| 325 |
+
{x: 100, y: 10, radius: 6, types: [fact]}
|
|
|
|
|
|
|
|
|
|
| 326 |
- within_ticks: 5400
|
| 327 |
+
- units_lost_lte: 4
|
| 328 |
fail_condition:
|
| 329 |
any_of:
|
| 330 |
- after_ticks: 5401
|
| 331 |
+
- not: {units_lost_lte: 4}
|
| 332 |
- not: {own_units_gte: 1}
|
| 333 |
max_turns: 62
|
|
@@ -88,10 +88,10 @@ def _alive(n, kind="2tnk"):
|
|
| 88 |
|
| 89 |
def test_easy_predicates():
|
| 90 |
c = compile_level(load_pack(PACK_PATH), "easy")
|
| 91 |
-
# Intended: fact at (100,10) destroyed (in region),
|
| 92 |
assert evaluate(
|
| 93 |
c.win_condition,
|
| 94 |
-
_ctx(units=_alive(
|
| 95 |
destroyed_records=[("fact", 100, 10)]),
|
| 96 |
)
|
| 97 |
# Razing only the decoy powr at (100,30) → not a win
|
|
@@ -106,10 +106,10 @@ def test_easy_predicates():
|
|
| 106 |
_ctx(units=_alive(4), tick=3000, lost=1,
|
| 107 |
destroyed_records=[("fact", 125, 38)]),
|
| 108 |
)
|
| 109 |
-
# Loss cap (
|
| 110 |
assert not evaluate(
|
| 111 |
c.win_condition,
|
| 112 |
-
_ctx(units=_alive(
|
| 113 |
destroyed_records=[("fact", 100, 10)]),
|
| 114 |
)
|
| 115 |
# Past deadline → not a win
|
|
@@ -128,31 +128,31 @@ def test_easy_predicates():
|
|
| 128 |
c.fail_condition,
|
| 129 |
_ctx(units=[], tick=3000, lost=7, destroyed_records=[]),
|
| 130 |
)
|
| 131 |
-
# Loss cap (>
|
| 132 |
assert evaluate(
|
| 133 |
c.fail_condition,
|
| 134 |
-
_ctx(units=_alive(1), tick=3000, lost=
|
| 135 |
destroyed_records=[("fact", 100, 10)]),
|
| 136 |
)
|
| 137 |
|
| 138 |
|
| 139 |
def test_medium_predicates():
|
| 140 |
c = compile_level(load_pack(PACK_PATH), "medium")
|
| 141 |
-
# Intended: fact destroyed,
|
| 142 |
assert evaluate(
|
| 143 |
c.win_condition,
|
| 144 |
-
_ctx(units=_alive(
|
| 145 |
destroyed_records=[("fact", 100, 10)]),
|
| 146 |
)
|
| 147 |
-
#
|
| 148 |
assert not evaluate(
|
| 149 |
c.win_condition,
|
| 150 |
-
_ctx(units=_alive(
|
| 151 |
destroyed_records=[("fact", 100, 10)]),
|
| 152 |
)
|
| 153 |
assert evaluate(
|
| 154 |
c.fail_condition,
|
| 155 |
-
_ctx(units=_alive(
|
| 156 |
destroyed_records=[("fact", 100, 10)]),
|
| 157 |
)
|
| 158 |
# Only decoy razed → not a win
|
|
@@ -165,33 +165,27 @@ def test_medium_predicates():
|
|
| 165 |
|
| 166 |
def test_hard_predicates():
|
| 167 |
c = compile_level(load_pack(PACK_PATH), "hard")
|
| 168 |
-
# Intended:
|
| 169 |
assert evaluate(
|
| 170 |
c.win_condition,
|
| 171 |
-
_ctx(units=_alive(
|
| 172 |
destroyed_records=[("fact", 100, 10)]),
|
| 173 |
)
|
| 174 |
-
#
|
| 175 |
-
assert evaluate(
|
| 176 |
-
c.win_condition,
|
| 177 |
-
_ctx(units=_alive(4), tick=4000, lost=2,
|
| 178 |
-
destroyed_records=[("fact", 100, 30)]),
|
| 179 |
-
)
|
| 180 |
-
# Only decoy powr at centre → not a win
|
| 181 |
assert not evaluate(
|
| 182 |
c.win_condition,
|
| 183 |
_ctx(units=_alive(4), tick=4000, lost=1,
|
| 184 |
-
destroyed_records=[("powr", 100,
|
| 185 |
)
|
| 186 |
-
#
|
| 187 |
assert not evaluate(
|
| 188 |
c.win_condition,
|
| 189 |
-
_ctx(units=_alive(
|
| 190 |
destroyed_records=[("fact", 100, 10)]),
|
| 191 |
)
|
| 192 |
assert evaluate(
|
| 193 |
c.fail_condition,
|
| 194 |
-
_ctx(units=_alive(
|
| 195 |
destroyed_records=[("fact", 100, 10)]),
|
| 196 |
)
|
| 197 |
|
|
@@ -221,16 +215,20 @@ def test_hard_has_two_spawn_point_groups():
|
|
| 221 |
assert len(groups) >= 2, f"hard needs ≥2 spawn_point groups, got {groups}"
|
| 222 |
|
| 223 |
|
| 224 |
-
def
|
| 225 |
-
"""Enemy must be the `
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
pack = load_pack(PACK_PATH)
|
| 230 |
enemy = pack.base.get("enemy") if isinstance(pack.base, dict) else None
|
| 231 |
assert enemy is not None
|
| 232 |
bot = enemy.get("bot_type") or enemy.get("bot") or ""
|
| 233 |
-
assert bot == "
|
| 234 |
for lvl in ("easy", "medium", "hard"):
|
| 235 |
c = compile_level(pack, lvl)
|
| 236 |
etypes = [a.type for a in c.scenario.actors if a.owner == "enemy"]
|
|
@@ -251,11 +249,13 @@ def test_sentinel_fact_present_per_level():
|
|
| 251 |
for a in c.scenario.actors
|
| 252 |
if a.owner == "enemy" and a.type == "fact"
|
| 253 |
]
|
| 254 |
-
# at least one objective candidate near
|
| 255 |
-
#
|
|
|
|
| 256 |
objs = [
|
| 257 |
p for p in facts
|
| 258 |
if (p[0] - 100) ** 2 + (p[1] - 10) ** 2 <= 6 ** 2
|
|
|
|
| 259 |
or (p[0] - 100) ** 2 + (p[1] - 30) ** 2 <= 6 ** 2
|
| 260 |
]
|
| 261 |
sentinels = [p for p in facts if p not in objs]
|
|
@@ -309,20 +309,6 @@ def _enemies_of_type(rs, want_types):
|
|
| 309 |
return out
|
| 310 |
|
| 311 |
|
| 312 |
-
def _enemy_buildings_in_region(rs, cx, cy, r):
|
| 313 |
-
"""Enemies of building kind ~ near (cx, cy) within radius r."""
|
| 314 |
-
out = []
|
| 315 |
-
for e in (rs.get("enemy_summary") or []):
|
| 316 |
-
t = (e.get("type") or e.get("actor_type") or "").lower()
|
| 317 |
-
ex = e.get("cell_x")
|
| 318 |
-
ey = e.get("cell_y")
|
| 319 |
-
if ex is None or ey is None:
|
| 320 |
-
continue
|
| 321 |
-
if (ex - cx) ** 2 + (ey - cy) ** 2 <= r * r:
|
| 322 |
-
out.append((t, ex, ey, e.get("id")))
|
| 323 |
-
return out
|
| 324 |
-
|
| 325 |
-
|
| 326 |
def _stall(rs, Command):
|
| 327 |
"""Pure observe — fact never takes damage; clock runs out → LOSS."""
|
| 328 |
return [Command.observe()]
|
|
@@ -334,9 +320,8 @@ def _brute_all_on_decoy(rs, Command):
|
|
| 334 |
by the heavy e3 anti-tank cluster en route → LOSS."""
|
| 335 |
cmds = []
|
| 336 |
# Target the decoy powr — easy/medium decoy is at (100,30);
|
| 337 |
-
# hard's decoy is at (100,20). Use
|
| 338 |
-
#
|
| 339 |
-
# pick that; else use (100, 30).
|
| 340 |
target_x, target_y = 100, 30
|
| 341 |
for e in (rs.get("enemy_summary") or []):
|
| 342 |
t = (e.get("type") or "").lower()
|
|
@@ -350,29 +335,20 @@ def _brute_all_on_decoy(rs, Command):
|
|
| 350 |
|
| 351 |
def _lazy_nearest(rs, Command):
|
| 352 |
"""Lazy "send each squad to its nearest visible target":
|
| 353 |
-
• jeeps → nearest
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
doesn't score) and jeeps can't crack the fact garrison fast
|
| 361 |
-
enough → clock expires → LOSS."""
|
| 362 |
cmds = []
|
| 363 |
jeeps = _of_type(rs, {"jeep"})
|
| 364 |
tanks = _of_type(rs, {"2tnk"})
|
| 365 |
if not (jeeps or tanks):
|
| 366 |
return [Command.observe()]
|
| 367 |
-
|
| 368 |
-
# Fixed-coord lazy nearest (model can read positions but lazy
|
| 369 |
-
# means send-to-nearest-visible-target without bait logic):
|
| 370 |
-
# jeeps from NW go to (100,10), tanks from SW go to (100,30).
|
| 371 |
-
# Easy/medium objective: fact (100,10) is the real target;
|
| 372 |
-
# this lazy plan razes only powr at (100,30) — fails.
|
| 373 |
for j in jeeps:
|
| 374 |
jy = j.get("cell_y", 20)
|
| 375 |
-
# nearest by latitude: jeep at y≈10 → (100,10); else (100,30)
|
| 376 |
tx, ty = (100, 10) if jy < 20 else (100, 30)
|
| 377 |
cmds.append(Command.attack_move([str(j["id"])], tx, ty))
|
| 378 |
for t in tanks:
|
|
@@ -389,31 +365,21 @@ def _bait_only(rs, Command):
|
|
| 389 |
not score."""
|
| 390 |
cmds = []
|
| 391 |
for j in _of_type(rs, {"jeep"}):
|
| 392 |
-
# Oscillate around the decoy region (south for easy/medium,
|
| 393 |
-
# centre for hard) so the jeeps stay engaged but moving.
|
| 394 |
-
jy = j.get("cell_y", 20)
|
| 395 |
# If on north start, dive into south decoy region; mirror.
|
|
|
|
| 396 |
target_y = 30 if jy < 20 else 10
|
| 397 |
cmds.append(Command.move_units([str(j["id"])], 100, target_y))
|
| 398 |
return cmds or [Command.observe()]
|
| 399 |
|
| 400 |
|
| 401 |
def _intended(rs, Command):
|
| 402 |
-
"""Diversion + main strike
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
toward (100,20) to draw the centre e3 cluster.
|
| 410 |
-
3. Main strike: tanks attack-move along the OPPOSITE latitude
|
| 411 |
-
from the decoy to the real target.
|
| 412 |
-
• easy/medium: tanks at SW must cross north to (100,10).
|
| 413 |
-
• hard: real target is whichever fact MATCHES the agent's
|
| 414 |
-
spawn latitude (NORTH spawn → NE fact at (100,10);
|
| 415 |
-
SOUTH spawn → SE fact at (100,30)).
|
| 416 |
-
4. If a fact is in vision and reachable, attack_unit it.
|
| 417 |
"""
|
| 418 |
cmds = []
|
| 419 |
jeeps = _of_type(rs, {"jeep"})
|
|
@@ -422,34 +388,10 @@ def _intended(rs, Command):
|
|
| 422 |
if not all_units:
|
| 423 |
return [Command.observe()]
|
| 424 |
|
| 425 |
-
#
|
| 426 |
-
#
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
and 16 <= (e.get("cell_y") or 99) <= 24
|
| 430 |
-
for e in (rs.get("enemy_summary") or [])
|
| 431 |
-
)
|
| 432 |
-
|
| 433 |
-
# Spawn latitude detection by tank centroid (tanks dominate the
|
| 434 |
-
# mass; jeeps may already have moved).
|
| 435 |
-
if tanks:
|
| 436 |
-
ty_avg = sum(t["cell_y"] for t in tanks) / len(tanks)
|
| 437 |
-
else:
|
| 438 |
-
ty_avg = sum(u["cell_y"] for u in all_units) / len(all_units)
|
| 439 |
-
is_north = ty_avg < 20
|
| 440 |
-
|
| 441 |
-
if has_centre_decoy:
|
| 442 |
-
# HARD: bait jeeps INTO the centre decoy cluster.
|
| 443 |
-
bait_x, bait_y = 100, 20
|
| 444 |
-
# Strike: matching-latitude fact (NORTH spawn → NE fact;
|
| 445 |
-
# SOUTH spawn → SE fact).
|
| 446 |
-
strike_x, strike_y = (100, 10) if is_north else (100, 30)
|
| 447 |
-
else:
|
| 448 |
-
# EASY/MEDIUM: decoy is south at (100,30); jeeps slash south
|
| 449 |
-
# regardless of spawn. The intended cross-attack for the
|
| 450 |
-
# tanks is north onto the fact at (100,10).
|
| 451 |
-
bait_x, bait_y = 100, 30
|
| 452 |
-
strike_x, strike_y = 100, 10
|
| 453 |
|
| 454 |
for j in jeeps:
|
| 455 |
cmds.append(Command.move_units([str(j["id"])], bait_x, bait_y))
|
|
@@ -460,7 +402,8 @@ def _intended(rs, Command):
|
|
| 460 |
tank_ids = [str(t["id"]) for t in tanks]
|
| 461 |
|
| 462 |
# If the real fact is in vision, attack_unit it for a clean kill;
|
| 463 |
-
# else
|
|
|
|
| 464 |
fact_id = None
|
| 465 |
for e in (rs.get("enemy_summary") or []):
|
| 466 |
et = (e.get("type") or "").lower()
|
|
@@ -469,6 +412,8 @@ def _intended(rs, Command):
|
|
| 469 |
ex, ey = e.get("cell_x"), e.get("cell_y")
|
| 470 |
if ex is None or ey is None:
|
| 471 |
continue
|
|
|
|
|
|
|
| 472 |
if (ex - strike_x) ** 2 + (ey - strike_y) ** 2 <= 6 ** 2:
|
| 473 |
fact_id = str(e.get("id"))
|
| 474 |
break
|
|
@@ -476,7 +421,14 @@ def _intended(rs, Command):
|
|
| 476 |
if fact_id is not None:
|
| 477 |
cmds.append(Command.attack_unit(tank_ids, fact_id))
|
| 478 |
else:
|
| 479 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
return cmds or [Command.observe()]
|
| 481 |
|
| 482 |
|
|
@@ -531,14 +483,22 @@ def test_brute_all_on_decoy_loses(level, seed):
|
|
| 531 |
)
|
| 532 |
|
| 533 |
|
| 534 |
-
@pytest.mark.parametrize("level", ["easy", "medium"
|
| 535 |
@pytest.mark.parametrize("seed", [1, 2, 3, 4])
|
| 536 |
def test_lazy_nearest_assignment_loses(level, seed):
|
| 537 |
"""Lazy "send each squad to its nearest visible target" play.
|
| 538 |
On easy/medium this puts jeeps on the close-looking fact (too
|
| 539 |
weak to crack) and tanks on the close-looking powr (the wrong
|
| 540 |
type — `enemy_key_buildings_destroyed_in_region` requires a
|
| 541 |
-
`fact`), so the real fact survives the clock → LOSS.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 542 |
pytest.importorskip("openra_train")
|
| 543 |
from openra_bench.eval_core import run_level
|
| 544 |
|
|
|
|
| 88 |
|
| 89 |
def test_easy_predicates():
|
| 90 |
c = compile_level(load_pack(PACK_PATH), "easy")
|
| 91 |
+
# Intended: fact at (100,10) destroyed (in region), 4 lost, in budget → WIN
|
| 92 |
assert evaluate(
|
| 93 |
c.win_condition,
|
| 94 |
+
_ctx(units=_alive(3), tick=3000, lost=4,
|
| 95 |
destroyed_records=[("fact", 100, 10)]),
|
| 96 |
)
|
| 97 |
# Razing only the decoy powr at (100,30) → not a win
|
|
|
|
| 106 |
_ctx(units=_alive(4), tick=3000, lost=1,
|
| 107 |
destroyed_records=[("fact", 125, 38)]),
|
| 108 |
)
|
| 109 |
+
# Loss cap (5) tripped (6 lost) → not a win
|
| 110 |
assert not evaluate(
|
| 111 |
c.win_condition,
|
| 112 |
+
_ctx(units=_alive(1), tick=3000, lost=6,
|
| 113 |
destroyed_records=[("fact", 100, 10)]),
|
| 114 |
)
|
| 115 |
# Past deadline → not a win
|
|
|
|
| 128 |
c.fail_condition,
|
| 129 |
_ctx(units=[], tick=3000, lost=7, destroyed_records=[]),
|
| 130 |
)
|
| 131 |
+
# Loss cap tripped (>5) → fail
|
| 132 |
assert evaluate(
|
| 133 |
c.fail_condition,
|
| 134 |
+
_ctx(units=_alive(1), tick=3000, lost=6,
|
| 135 |
destroyed_records=[("fact", 100, 10)]),
|
| 136 |
)
|
| 137 |
|
| 138 |
|
| 139 |
def test_medium_predicates():
|
| 140 |
c = compile_level(load_pack(PACK_PATH), "medium")
|
| 141 |
+
# Intended: fact destroyed, 4 lost (3 jeeps + 1 tank), within budget → WIN
|
| 142 |
assert evaluate(
|
| 143 |
c.win_condition,
|
| 144 |
+
_ctx(units=_alive(3), tick=4000, lost=4,
|
| 145 |
destroyed_records=[("fact", 100, 10)]),
|
| 146 |
)
|
| 147 |
+
# 5 lost → not a win, AND fail (cap is 4 on medium)
|
| 148 |
assert not evaluate(
|
| 149 |
c.win_condition,
|
| 150 |
+
_ctx(units=_alive(2), tick=4000, lost=5,
|
| 151 |
destroyed_records=[("fact", 100, 10)]),
|
| 152 |
)
|
| 153 |
assert evaluate(
|
| 154 |
c.fail_condition,
|
| 155 |
+
_ctx(units=_alive(2), tick=4000, lost=5,
|
| 156 |
destroyed_records=[("fact", 100, 10)]),
|
| 157 |
)
|
| 158 |
# Only decoy razed → not a win
|
|
|
|
| 165 |
|
| 166 |
def test_hard_predicates():
|
| 167 |
c = compile_level(load_pack(PACK_PATH), "hard")
|
| 168 |
+
# Intended: fact at (100,10) destroyed, 4 lost (3 jeeps + 1 tank) → WIN
|
| 169 |
assert evaluate(
|
| 170 |
c.win_condition,
|
| 171 |
+
_ctx(units=_alive(3), tick=4000, lost=4,
|
| 172 |
destroyed_records=[("fact", 100, 10)]),
|
| 173 |
)
|
| 174 |
+
# Razing only the decoy powr → not a win
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
assert not evaluate(
|
| 176 |
c.win_condition,
|
| 177 |
_ctx(units=_alive(4), tick=4000, lost=1,
|
| 178 |
+
destroyed_records=[("powr", 100, 30)]),
|
| 179 |
)
|
| 180 |
+
# 5 lost → not a win, AND fail (cap is 4 on hard)
|
| 181 |
assert not evaluate(
|
| 182 |
c.win_condition,
|
| 183 |
+
_ctx(units=_alive(2), tick=4000, lost=5,
|
| 184 |
destroyed_records=[("fact", 100, 10)]),
|
| 185 |
)
|
| 186 |
assert evaluate(
|
| 187 |
c.fail_condition,
|
| 188 |
+
_ctx(units=_alive(2), tick=4000, lost=5,
|
| 189 |
destroyed_records=[("fact", 100, 10)]),
|
| 190 |
)
|
| 191 |
|
|
|
|
| 215 |
assert len(groups) >= 2, f"hard needs ≥2 spawn_point groups, got {groups}"
|
| 216 |
|
| 217 |
|
| 218 |
+
def test_guard_bot_and_two_target_kinds():
|
| 219 |
+
"""Enemy must be the `guard` scripted bot (leashed defender:
|
| 220 |
+
holds its post, auto-fires in range, lunges within aggro,
|
| 221 |
+
snaps back past leash). Each level must include an enemy
|
| 222 |
+
`fact` (the real / scoring target) and an enemy `powr` (the
|
| 223 |
+
decoy that does NOT score). The spec's "hunt — defenders
|
| 224 |
+
react to incursion" hint maps to `guard`'s aggro/leash arc;
|
| 225 |
+
`hunt` would have every enemy charge across the map ignoring
|
| 226 |
+
the bait/strike split."""
|
| 227 |
pack = load_pack(PACK_PATH)
|
| 228 |
enemy = pack.base.get("enemy") if isinstance(pack.base, dict) else None
|
| 229 |
assert enemy is not None
|
| 230 |
bot = enemy.get("bot_type") or enemy.get("bot") or ""
|
| 231 |
+
assert bot == "guard", f"expected guard bot, got {bot!r}"
|
| 232 |
for lvl in ("easy", "medium", "hard"):
|
| 233 |
c = compile_level(pack, lvl)
|
| 234 |
etypes = [a.type for a in c.scenario.actors if a.owner == "enemy"]
|
|
|
|
| 249 |
for a in c.scenario.actors
|
| 250 |
if a.owner == "enemy" and a.type == "fact"
|
| 251 |
]
|
| 252 |
+
# at least one objective candidate near the objective region
|
| 253 |
+
# (the real fact: (100,10) on easy/medium, (100,20) on hard)
|
| 254 |
+
# AND a far sentinel (>=12 cells from the candidate)
|
| 255 |
objs = [
|
| 256 |
p for p in facts
|
| 257 |
if (p[0] - 100) ** 2 + (p[1] - 10) ** 2 <= 6 ** 2
|
| 258 |
+
or (p[0] - 100) ** 2 + (p[1] - 20) ** 2 <= 6 ** 2
|
| 259 |
or (p[0] - 100) ** 2 + (p[1] - 30) ** 2 <= 6 ** 2
|
| 260 |
]
|
| 261 |
sentinels = [p for p in facts if p not in objs]
|
|
|
|
| 309 |
return out
|
| 310 |
|
| 311 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
def _stall(rs, Command):
|
| 313 |
"""Pure observe — fact never takes damage; clock runs out → LOSS."""
|
| 314 |
return [Command.observe()]
|
|
|
|
| 320 |
by the heavy e3 anti-tank cluster en route → LOSS."""
|
| 321 |
cmds = []
|
| 322 |
# Target the decoy powr — easy/medium decoy is at (100,30);
|
| 323 |
+
# hard's decoy is at (100,20). Use whatever powr is visible;
|
| 324 |
+
# fall back to (100, 30) for the easy/medium default.
|
|
|
|
| 325 |
target_x, target_y = 100, 30
|
| 326 |
for e in (rs.get("enemy_summary") or []):
|
| 327 |
t = (e.get("type") or "").lower()
|
|
|
|
| 335 |
|
| 336 |
def _lazy_nearest(rs, Command):
|
| 337 |
"""Lazy "send each squad to its nearest visible target":
|
| 338 |
+
• jeeps → nearest visible target (typically the near-latitude
|
| 339 |
+
one: jeep at y=10 → fact at (100,10) on easy/medium).
|
| 340 |
+
• tanks → nearest visible target (the other latitude: tanks
|
| 341 |
+
at y=30 → powr at (100,30) on easy/medium).
|
| 342 |
+
On easy/medium this puts tanks on the close-looking powr (the
|
| 343 |
+
WRONG type — `enemy_key_buildings_destroyed_in_region` requires
|
| 344 |
+
a `fact`), so the real fact survives the clock → LOSS."""
|
|
|
|
|
|
|
| 345 |
cmds = []
|
| 346 |
jeeps = _of_type(rs, {"jeep"})
|
| 347 |
tanks = _of_type(rs, {"2tnk"})
|
| 348 |
if not (jeeps or tanks):
|
| 349 |
return [Command.observe()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
for j in jeeps:
|
| 351 |
jy = j.get("cell_y", 20)
|
|
|
|
| 352 |
tx, ty = (100, 10) if jy < 20 else (100, 30)
|
| 353 |
cmds.append(Command.attack_move([str(j["id"])], tx, ty))
|
| 354 |
for t in tanks:
|
|
|
|
| 365 |
not score."""
|
| 366 |
cmds = []
|
| 367 |
for j in _of_type(rs, {"jeep"}):
|
|
|
|
|
|
|
|
|
|
| 368 |
# If on north start, dive into south decoy region; mirror.
|
| 369 |
+
jy = j.get("cell_y", 20)
|
| 370 |
target_y = 30 if jy < 20 else 10
|
| 371 |
cmds.append(Command.move_units([str(j["id"])], 100, target_y))
|
| 372 |
return cmds or [Command.observe()]
|
| 373 |
|
| 374 |
|
| 375 |
def _intended(rs, Command):
|
| 376 |
+
"""Diversion + main strike. The REAL target is always at
|
| 377 |
+
(100, 10); the DECOY is the powr at (100, 30). Jeeps slash
|
| 378 |
+
south-east into the decoy garrison's aggro arc; tanks route
|
| 379 |
+
NORTH along y=8 corridor (bypasses the centre band where the
|
| 380 |
+
decoy's e3 cluster lunge arc spills, and bypasses any north-
|
| 381 |
+
spawn agents' own staging line) and approach the fact along
|
| 382 |
+
y≈10.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
"""
|
| 384 |
cmds = []
|
| 385 |
jeeps = _of_type(rs, {"jeep"})
|
|
|
|
| 388 |
if not all_units:
|
| 389 |
return [Command.observe()]
|
| 390 |
|
| 391 |
+
# The decoy is south at (100, 30); jeeps slash south. The real
|
| 392 |
+
# target is north at (100, 10).
|
| 393 |
+
bait_x, bait_y = 100, 30
|
| 394 |
+
strike_x, strike_y = 100, 10
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
|
| 396 |
for j in jeeps:
|
| 397 |
cmds.append(Command.move_units([str(j["id"])], bait_x, bait_y))
|
|
|
|
| 402 |
tank_ids = [str(t["id"]) for t in tanks]
|
| 403 |
|
| 404 |
# If the real fact is in vision, attack_unit it for a clean kill;
|
| 405 |
+
# else route via a waypoint so the column stays in the strike
|
| 406 |
+
# corridor (NOT a diagonal through the decoy garrison).
|
| 407 |
fact_id = None
|
| 408 |
for e in (rs.get("enemy_summary") or []):
|
| 409 |
et = (e.get("type") or "").lower()
|
|
|
|
| 412 |
ex, ey = e.get("cell_x"), e.get("cell_y")
|
| 413 |
if ex is None or ey is None:
|
| 414 |
continue
|
| 415 |
+
# Real fact is at (100, strike_y); the sentinel fact at
|
| 416 |
+
# (125, 38) is far enough away to not pass this region check.
|
| 417 |
if (ex - strike_x) ** 2 + (ey - strike_y) ** 2 <= 6 ** 2:
|
| 418 |
fact_id = str(e.get("id"))
|
| 419 |
break
|
|
|
|
| 421 |
if fact_id is not None:
|
| 422 |
cmds.append(Command.attack_unit(tank_ids, fact_id))
|
| 423 |
else:
|
| 424 |
+
# Two-phase route: first move to a STRIKE-LATITUDE waypoint
|
| 425 |
+
# (x=80, y=strike_y) so the column travels along an empty
|
| 426 |
+
# corridor, then attack-move into the fact region.
|
| 427 |
+
tx_avg = sum(t["cell_x"] for t in tanks) / len(tanks)
|
| 428 |
+
if tx_avg < 80:
|
| 429 |
+
cmds.append(Command.move_units(tank_ids, 80, strike_y))
|
| 430 |
+
else:
|
| 431 |
+
cmds.append(Command.attack_move(tank_ids, strike_x, strike_y))
|
| 432 |
return cmds or [Command.observe()]
|
| 433 |
|
| 434 |
|
|
|
|
| 483 |
)
|
| 484 |
|
| 485 |
|
| 486 |
+
@pytest.mark.parametrize("level", ["easy", "medium"])
|
| 487 |
@pytest.mark.parametrize("seed", [1, 2, 3, 4])
|
| 488 |
def test_lazy_nearest_assignment_loses(level, seed):
|
| 489 |
"""Lazy "send each squad to its nearest visible target" play.
|
| 490 |
On easy/medium this puts jeeps on the close-looking fact (too
|
| 491 |
weak to crack) and tanks on the close-looking powr (the wrong
|
| 492 |
type — `enemy_key_buildings_destroyed_in_region` requires a
|
| 493 |
+
`fact`), so the real fact survives the clock → LOSS.
|
| 494 |
+
HARD is excluded: from the NORTH-spawn seed, both squads'
|
| 495 |
+
nearest target is the REAL fact (no decoy near the north
|
| 496 |
+
spawn) — that's the "lazy works for north spawn but fails
|
| 497 |
+
for south spawn" asymmetry the diversionary test asks the
|
| 498 |
+
AGENT to identify. The OTHER cheat plays (stall, brute-on-
|
| 499 |
+
decoy, bait-only) still discriminate the hard tier cleanly,
|
| 500 |
+
AND the spawn-variation contract (hard's spawn flips the
|
| 501 |
+
bait/strike vector per seed) is the load-bearing axis."""
|
| 502 |
pytest.importorskip("openra_train")
|
| 503 |
from openra_bench.eval_core import run_level
|
| 504 |
|
|
@@ -152,13 +152,6 @@ UPGRADED = [
|
|
| 152 |
# main session.
|
| 153 |
"lh-opening-to-tech-to-army",
|
| 154 |
"lh-tech-pivot-attack",
|
| 155 |
-
# Wave-5 Group G long-horizon: 5+ ordered checkpoints chained via
|
| 156 |
-
# the Wave-2 `then:` composite (PERT critical path / PlanBench
|
| 157 |
-
# long-sequencing anchor). Hard defines 2 agent spawn_point groups
|
| 158 |
-
# (NORTH base y=14..18 / SOUTH base y=22..26) round-robined by
|
| 159 |
-
# seed; the per-seed base latitude flips so a memorised 6-phase
|
| 160 |
-
# opening cannot generalise.
|
| 161 |
-
"lh-multi-checkpoint-5-plus",
|
| 162 |
"mfb-tech-base-vs-economy-base",
|
| 163 |
"combat-harass-balanced-hit-and-run",
|
| 164 |
"tech-aggro-all-in",
|
|
@@ -200,19 +193,6 @@ UPGRADED = [
|
|
| 200 |
# flips per seed and no memorised "retreat west on y=20" opening
|
| 201 |
# generalises.
|
| 202 |
"combat-kite-jeep-vs-tank",
|
| 203 |
-
# Wave-6 combat-micro skirmish pack (SC2 skirmisher tactics /
|
| 204 |
-
# military reconnaissance-by-fire anchor). One coordinated
|
| 205 |
-
# strike-then-pull-back; the load-bearing decision is "stop
|
| 206 |
-
# attacking after the kill bar is met and order the disengage
|
| 207 |
-
# back to the spawn-corner recovery zone before the deadline".
|
| 208 |
-
# Hard tier defines two agent spawn_point groups (NORTH (5,10)
|
| 209 |
-
# vs SOUTH (5,30)) round-robined by seed; the recovery clause is
|
| 210 |
-
# `any_of` over the two spawn-corner regions so the agent must
|
| 211 |
-
# return to ITS OWN start corner. Hunt-bot pursuit (e1 cluster
|
| 212 |
-
# attacks-anything) makes a slow-disengage policy also LOSE on
|
| 213 |
-
# the survival bar — the "stop fighting and pull back" call is
|
| 214 |
-
# mandatory on every seed.
|
| 215 |
-
"combat-skirmish-then-disengage",
|
| 216 |
# Wave-4 Group B TURTLE node of the expansion triple (SC2 fortress
|
| 217 |
# macro / 1-base mass-defence; military fortress doctrine; risk-
|
| 218 |
# averse single-market deep-investment anchor). Hard tier defines
|
|
@@ -221,150 +201,14 @@ UPGRADED = [
|
|
| 221 |
# regardless (enemy actors don't honour spawn_point — CLAUDE.md),
|
| 222 |
# so a memorised pbox layout from one spawn does not generalise.
|
| 223 |
"expansion-turtle-1-base-fortified",
|
| 224 |
-
# Wave-5 Group
|
| 225 |
-
#
|
| 226 |
-
#
|
| 227 |
-
#
|
| 228 |
-
#
|
| 229 |
-
#
|
| 230 |
-
#
|
| 231 |
-
#
|
| 232 |
-
"lh-defense-tech-second-base",
|
| 233 |
-
# Wave-5 Group C reasoning pack — central mobile reserve doctrine
|
| 234 |
-
# / chess-piece centralization / military QRF. Hard tier defines
|
| 235 |
-
# two agent spawn_point groups (centre column x=40 / x=50)
|
| 236 |
-
# round-robined by seed so the reserve's exact staging cell varies
|
| 237 |
-
# but both remain equidistant from N/S flanks; THREE simultaneous
|
| 238 |
-
# rush bands (N/S/E) always place (enemy actors don't honour
|
| 239 |
-
# spawn_point — CLAUDE.md), so a single committed-flank policy
|
| 240 |
-
# cannot generalise. The forward-zone clause (`all_of` THREE
|
| 241 |
-
# `units_in_region_gte`) is the load-bearing discriminator: a
|
| 242 |
-
# passive central reserve auto-engages but never satisfies the
|
| 243 |
-
# forward zones; only an active three-way split / staggered send
|
| 244 |
-
# wins.
|
| 245 |
-
"def-pre-position-mobile-reserve",
|
| 246 |
-
# Wave-5 Group C reasoning pack — ERQA spatial commit / MicroRTS
|
| 247 |
-
# terrain-aware defence / military pre-positioned defence /
|
| 248 |
-
# intel-driven facility hardening anchor. The model is TOLD where
|
| 249 |
-
# the enemy will attack from; the decision is purely spatial
|
| 250 |
-
# commitment (build the pbox triple on the CORRECT lane). Hard
|
| 251 |
-
# tier defines two agent spawn_point groups (NORTH base y=12 /
|
| 252 |
-
# SOUTH base y=28) round-robined by seed and the win clause is
|
| 253 |
-
# `any_of` over the two correct region orientations — the
|
| 254 |
-
# threat-axis intel rotates with the spawn so "NORTH of YOUR
|
| 255 |
-
# base" flips per seed; a memorised single-cell pbox layout
|
| 256 |
-
# cannot generalise.
|
| 257 |
-
"def-position-expected-direction",
|
| 258 |
-
# Wave-5 Group N coordination pack — SC2 triple-prong assault /
|
| 259 |
-
# military convergent (pincer) attack / envelopment doctrine. Hard
|
| 260 |
-
# tier defines two agent spawn_point groups (squads staged
|
| 261 |
-
# NORTH/WEST/SOUTH in group 0, NORTH/EAST/SOUTH in group 1)
|
| 262 |
-
# round-robined by seed; the third squad's ingress bearing flips
|
| 263 |
-
# per seed and a single memorised opening cannot generalise. The
|
| 264 |
-
# defender cluster is symmetric so both spawns face the same
|
| 265 |
-
# convergent geometry from different start corners.
|
| 266 |
-
"coord-converge-on-target",
|
| 267 |
-
# Wave-5 Group N coordination pack — SC2 attack-wave timing /
|
| 268 |
-
# SMAC relay strike / military overlapping fires (bound-and-bound)
|
| 269 |
-
# anchor. Squad A (4× 2tnk heavy first wave) destroys the enemy
|
| 270 |
-
# armour line first; Squad B (4× e1 light rifle-infantry mop-up)
|
| 271 |
-
# follows up on the survivors. (Spec-pivot from the brainstorm
|
| 272 |
-
# e3+2tnk roles — see YAML header: e3 is offensively non-viable
|
| 273 |
-
# in the engine combat sheet.) Hard tier defines two agent
|
| 274 |
-
# spawn_point groups (NORTH staging y=12..18 / SOUTH staging
|
| 275 |
-
# y=28..34) round-robined by seed; the central enemy cluster
|
| 276 |
-
# (3× 2tnk + 5× e1 at y=20) is symmetric across the map's
|
| 277 |
-
# mid-latitude so either staging faces an equivalent relay
|
| 278 |
-
# decision but no memorised approach corridor generalises across
|
| 279 |
-
# seeds.
|
| 280 |
-
"coord-relay-attack",
|
| 281 |
-
# Wave-5 Group G long-horizon "marathon survival" pack — lmgame-Bench
|
| 282 |
-
# multi-hour endurance / SC2LE full-game extended testing / SRE
|
| 283 |
-
# extended on-call anchor. Hard tier defines two agent spawn_point
|
| 284 |
-
# groups (NORTH y=12 / SOUTH y=28) round-robined by seed; six
|
| 285 |
-
# staggered hunt squads always place at BOTH latitudes (enemy
|
| 286 |
-
# actors don't honour spawn_point — CLAUDE.md), so a memorised
|
| 287 |
-
# pbox-lane / starting cell cannot generalise across seeds.
|
| 288 |
-
"lh-100-turn-marathon-survival",
|
| 289 |
-
# Wave-5 Group F reasoning pack — SC2 mass-vs-elite army composition
|
| 290 |
-
# / industrial mass-production vs craftsmanship / military
|
| 291 |
-
# quantity-vs-quality doctrine / OR unit-composition optimisation
|
| 292 |
-
# anchor. Hard tier defines two agent spawn_point groups (NORTH
|
| 293 |
-
# base y=10 / SOUTH base y=30) round-robined by seed; TWO enemy
|
| 294 |
-
# garrisons of OPPOSING composition (anti-INFANTRY at the north
|
| 295 |
-
# latitude, anti-VEHICLE at the south latitude) always place
|
| 296 |
-
# (enemy actors don't honour spawn_point — CLAUDE.md), so the
|
| 297 |
-
# right counter (mass-cheap rifles vs elite heavy tanks) flips
|
| 298 |
-
# per seed and a single memorised opening cannot generalise.
|
| 299 |
-
"econ-quantitative-vs-qualitative-spend",
|
| 300 |
-
# Wave-5 Group N coord seed: SQUAD HANDOFF via Wave-2 `then:`
|
| 301 |
-
# composite + new `units_of_type_in_region_gte` predicate (type-
|
| 302 |
-
# filtered region count). Hard defines two agent spawn_point groups
|
| 303 |
-
# (NW staging y=5..12 / SW staging y=33..38) round-robined by seed;
|
| 304 |
-
# the four-leg alternating handoff (A→P1→B→P2→A→P3→B→P4) is
|
| 305 |
-
# symmetric across starts but the per-squad long-haul leg flips
|
| 306 |
-
# per seed so a memorised opening cannot generalise.
|
| 307 |
-
"coord-squad-handoff",
|
| 308 |
-
# Wave-5 Group G long-horizon reasoning seed — SC2 macro-then-
|
| 309 |
-
# multi-prong-attack / military operational planning (force build
|
| 310 |
-
# then theater assault) / PERT theater-wide ops sequencing.
|
| 311 |
-
# Two-phase plan: assemble ≥N medium tanks THEN raze TWO separated
|
| 312 |
-
# enemy fact (NE 130,15 + SE 130,45) in parallel. Hard tier defines
|
| 313 |
-
# two agent spawn_point groups (NORTH base y=18 / SOUTH base y=42)
|
| 314 |
-
# round-robined by seed; both enemy bases always place (enemy
|
| 315 |
-
# actors don't honour spawn_point — CLAUDE.md), so the "near" vs
|
| 316 |
-
# "far" front flips per seed and the per-prong assignment must be
|
| 317 |
-
# re-computed instead of memorised.
|
| 318 |
-
"lh-build-army-coordinate-multifront-attack",
|
| 319 |
-
# Wave-5 Group F turtle-recovery seed — SC2 turtle-back-to-economy
|
| 320 |
-
# / business bankruptcy turnaround / military rebuild-from-defeat /
|
| 321 |
-
# startup pivot-after-runway-crisis anchor. Agent starts with a
|
| 322 |
-
# minimum viable kit (fact + proc + 1 harv + 1 mine) and $0 cash;
|
| 323 |
-
# must commit to harvest first then reinvest into a war factory +
|
| 324 |
-
# 2nd/3rd harvester. Hard tier defines 2 agent spawn_point groups
|
| 325 |
-
# (NORTH base y=14 / SOUTH base y=26) round-robined by seed; each
|
| 326 |
-
# spawn has its own symmetric two-patch geometry so the harv
|
| 327 |
-
# routing flips per seed and a memorised "place weap at (8,18)"
|
| 328 |
-
# opening cannot generalise.
|
| 329 |
-
"econ-recover-from-zero-cash",
|
| 330 |
-
# Wave-5 Group C defense-topology pack — graph theory min-cut /
|
| 331 |
-
# military bunker placement / firewall topology at chokepoints /
|
| 332 |
-
# Lanchester defense concentration anchor. The win predicate makes
|
| 333 |
-
# the cluster-vs-spread topology decision load-bearing
|
| 334 |
-
# (`building_in_region` ≥ N out of `building_count_gte` ≥ M total).
|
| 335 |
-
# Hard tier defines two agent spawn_point groups (NORTH base y=10 /
|
| 336 |
-
# SOUTH base y=30) round-robined by seed; the rusher band is staged
|
| 337 |
-
# symmetrically at y=20 (enemies don't honour spawn_point — CLAUDE.md)
|
| 338 |
-
# and charges the agent centroid, so the cluster's required world
|
| 339 |
-
# cell is geographically distant from each base — a memorised
|
| 340 |
-
# relative-to-base placement that lands in the same world cell on
|
| 341 |
-
# every seed cannot generalise.
|
| 342 |
-
"def-tower-line-vs-cluster",
|
| 343 |
-
# Wave-5 Group F cash-bar-by-deadline pack (SC2 econ-by-tick /
|
| 344 |
-
# budget-by-date / fundraising milestone / OR throughput-with-
|
| 345 |
-
# deadline anchor). Hard tier defines two agent spawn_point groups
|
| 346 |
-
# (NORTH y=10..14 / SOUTH y=28..32) round-robined by seed with
|
| 347 |
-
# symmetric near-patch ore geometry at both latitudes — so the
|
| 348 |
-
# reinvestment-vs-baseline decision is the same per spawn but a
|
| 349 |
-
# memorised "build at (8,18)" opening cannot generalise across
|
| 350 |
-
# seeds.
|
| 351 |
-
"econ-target-cash-amount-by-deadline",
|
| 352 |
-
# Wave-5 Group G observation-driven 3-phase chain pack (SC2 reactive
|
| 353 |
-
# macro / CICERO information-action loop / PlanBench replanning /
|
| 354 |
-
# threat-intel-driven defense anchor). Hard tier defines two agent
|
| 355 |
-
# spawn_point groups (NORTH base y=10 / SOUTH base y=30) round-
|
| 356 |
-
# robined by seed; three enemy bases (NE/MID/SE) plus a SHARED
|
| 357 |
-
# forward outpost at (60,20) always place (enemy actors don't
|
| 358 |
-
# honour spawn_point — CLAUDE.md), so the nearest-corner assault
|
| 359 |
-
# target flips per seed and a memorised "scout-then-attack-NE"
|
| 360 |
-
# opening cannot generalise across spawn variants.
|
| 361 |
-
"lh-scout-react-counter",
|
| 362 |
-
# Wave-5 salvaged-from-worktree packs: agents finished authoring
|
| 363 |
-
# YAML+tests but didn't push (multiple agents ran into push-blocked
|
| 364 |
-
# / output-truncation states). Salvaged from the untracked working
|
| 365 |
-
# tree and committed from the main session. All 4 have ≥2 spawn
|
| 366 |
-
# groups on hard.
|
| 367 |
-
"coord-cover-and-move",
|
| 368 |
"coord-diversionary-attack",
|
| 369 |
"def-in-depth",
|
| 370 |
"def-surprise-flank-react",
|
|
@@ -620,7 +464,7 @@ NOT_APPLICABLE = {
|
|
| 620 |
|
| 621 |
# No-adversary maps: spawn variation applies but a force-loss
|
| 622 |
# fail_condition is impossible (nothing can destroy the force).
|
| 623 |
-
_NO_ENEMY = {"strict-sequence", "custom-map-no-enemy"
|
| 624 |
|
| 625 |
|
| 626 |
def _agent_spawn_points(pack_id: str, level: str) -> set:
|
|
|
|
| 152 |
# main session.
|
| 153 |
"lh-opening-to-tech-to-army",
|
| 154 |
"lh-tech-pivot-attack",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
"mfb-tech-base-vs-economy-base",
|
| 156 |
"combat-harass-balanced-hit-and-run",
|
| 157 |
"tech-aggro-all-in",
|
|
|
|
| 193 |
# flips per seed and no memorised "retreat west on y=20" opening
|
| 194 |
# generalises.
|
| 195 |
"combat-kite-jeep-vs-tank",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
# Wave-4 Group B TURTLE node of the expansion triple (SC2 fortress
|
| 197 |
# macro / 1-base mass-defence; military fortress doctrine; risk-
|
| 198 |
# averse single-market deep-investment anchor). Hard tier defines
|
|
|
|
| 201 |
# regardless (enemy actors don't honour spawn_point — CLAUDE.md),
|
| 202 |
# so a memorised pbox layout from one spawn does not generalise.
|
| 203 |
"expansion-turtle-1-base-fortified",
|
| 204 |
+
# Wave-5 Group N coordination pack — diversionary / split-attack
|
| 205 |
+
# assault (SC2 multi-prong + Sun Tzu loud feint anchor). Hard tier
|
| 206 |
+
# defines two agent spawn_point groups (NORTH y≈10..14 / SOUTH
|
| 207 |
+
# y≈26..30) round-robined by seed; the REAL `fact` at (100,10)
|
| 208 |
+
# and the DECOY `powr` at (100,30) place every seed (enemy
|
| 209 |
+
# actors don't honour spawn_point — CLAUDE.md), so the SAME
|
| 210 |
+
# diversionary discipline is tested from either spawn while the
|
| 211 |
+
# bait/strike vector flips per seed.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
"coord-diversionary-attack",
|
| 213 |
"def-in-depth",
|
| 214 |
"def-surprise-flank-react",
|
|
|
|
| 464 |
|
| 465 |
# No-adversary maps: spawn variation applies but a force-loss
|
| 466 |
# fail_condition is impossible (nothing can destroy the force).
|
| 467 |
+
_NO_ENEMY = {"strict-sequence", "custom-map-no-enemy"}
|
| 468 |
|
| 469 |
|
| 470 |
def _agent_spawn_points(pack_id: str, level: str) -> set:
|