Spaces:
Running
fix(scenario): build-defensive-skirt-corners — pbox now load-bearing after engine pbox-weapon fix
Browse filesThe engine pbox-weapon fix makes a BUILT pbox an active direct-fire
anti-infantry tower. This pack's intended SKIRT policy previously LOST
(the four corner rushes razed the fact before any pbox could be built;
inert pboxes did nothing and the pre-placed e1 ring died instantly).
Recalibrated to the def-walls-vs-towers idiom so the pbox is genuinely
load-bearing:
- the four rush bands now arrive as a `scheduled_events: spawn_actors`
wave at tick 1800 — AFTER the skirt has time to build all 4 pboxes
serially — with one band sitting ON each corner region so a pbox
planted there engages its band immediately;
- removed the pre-placed e1 combat ring (only one far-corner
non-combatant e1 per spawn group remains for units_summary);
- the four corner-region discs moved to the diagonal-corner positions
where the bands spawn (fact +/-(18,11)); the kill bar already
present is now satisfied by pbox kills.
The four corner region clauses still discriminate: a concentrate at one
corner fails three regions and lets three waves through. Intended SKIRT
wins every level + seed; stall / concentrate / pure-army lose.
|
@@ -14,12 +14,25 @@
|
|
| 14 |
#
|
| 15 |
# The win predicate makes the four-corner topology load-bearing — total
|
| 16 |
# pbox count alone is NOT enough; ONE pbox must sit inside a radius-4
|
| 17 |
-
# disc in EACH of the four corner regions (NE/NW/SE/SW of the fact)
|
|
|
|
| 18 |
# A concentrate-at-one-corner policy meets the count (4 pbox) but
|
| 19 |
# satisfies only ONE of the four region clauses and fails the other
|
| 20 |
# three — AND the three uncovered corner waves reach the fact and raze
|
| 21 |
# it. Concentrate LOSES; the intended four-corner skirt WINS.
|
| 22 |
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
# Real-world anchors (binding meta.benchmark_anchor):
|
| 24 |
# • MicroRTS pillbox placement — the canonical RTS sub-benchmark for
|
| 25 |
# where a finite set of static defences should be planted; the
|
|
@@ -35,10 +48,11 @@
|
|
| 35 |
# converge on the fact and raze it — the fact-alive fail clause
|
| 36 |
# fires → LOSS (`after_ticks` is the deterministic backstop, no
|
| 37 |
# draw degeneracy).
|
| 38 |
-
# • concentrate (all 4 pbox massed at ONE corner
|
| 39 |
-
#
|
| 40 |
-
#
|
| 41 |
-
# corner waves walk into the fact and raze it →
|
|
|
|
| 42 |
# • pure-army (only e1, never a pbox): FAILS the count clause AND all
|
| 43 |
# four region clauses; the rifle wall alone cannot hold four
|
| 44 |
# concurrent rushes → LOSS.
|
|
@@ -48,27 +62,33 @@
|
|
| 48 |
# fact-alive clauses all satisfied → WIN.
|
| 49 |
#
|
| 50 |
# Why the spec works (engine combat sheet, per CLAUDE.md):
|
| 51 |
-
# • pbox = pillbox (anti-infantry base defence; cost 600cr;
|
| 52 |
-
#
|
| 53 |
-
#
|
| 54 |
-
# and build('e1') in
|
|
|
|
| 55 |
# • `building_count_gte:{pbox,4}` + FOUR `building_in_region` clauses
|
| 56 |
# (one radius-4 disc per corner) make the four-corner SKIRT load-
|
| 57 |
# bearing: a concentration satisfies the count and exactly one
|
| 58 |
# region clause and fails the other three.
|
|
|
|
|
|
|
|
|
|
| 59 |
# • `building_count_gte:{fact,1}` (PRESENT-TENSE) is the fact-alive
|
| 60 |
# check — `has_building:fact` is a one-shot ever-seen set that
|
| 61 |
# stays true after the fact is razed (documented CLAUDE.md footgun).
|
| 62 |
-
# •
|
| 63 |
-
# (
|
| 64 |
-
#
|
|
|
|
|
|
|
| 65 |
# • A persistent unarmed enemy `fact` keeps the episode alive past
|
| 66 |
# full rusher elimination so the win/fail evaluator sees the
|
| 67 |
# terminal frame (no DRAW collapse on enemy-wipe auto-done).
|
| 68 |
-
# • The `rusher` bot charges the agent centroid (the fact
|
| 69 |
-
#
|
| 70 |
-
#
|
| 71 |
-
# every
|
| 72 |
# • Cash is intentionally tight (exactly 4×600cr = 2400cr). A model
|
| 73 |
# that wastes cash on extra units cannot complete the pbox count.
|
| 74 |
|
|
@@ -87,7 +107,8 @@ meta:
|
|
| 87 |
the three uncovered corner waves stride untouched into the fact and
|
| 88 |
raze it. The win predicate makes the topology load-bearing: total
|
| 89 |
pbox count is not enough — one pbox must sit inside a radius-4 disc
|
| 90 |
-
in EVERY corner region,
|
|
|
|
| 91 |
robotics_analogue: >
|
| 92 |
Distributed defense / quadrant coverage: when one central asset is
|
| 93 |
threatened from all bearings, finite defensive capacity must be
|
|
@@ -129,23 +150,28 @@ base:
|
|
| 129 |
levels:
|
| 130 |
# ── EASY ── bare FOUR-CORNER-SKIRT skill: budget covers exactly 4
|
| 131 |
# pbox (2400cr). Win requires the count (4 pbox) AND one pbox inside
|
| 132 |
-
# the radius-4 disc of EACH corner region (NE (
|
| 133 |
-
# SE (
|
|
|
|
|
|
|
| 134 |
# concentrate-at-one-corner layout meets the count but satisfies
|
| 135 |
-
# exactly one region clause
|
| 136 |
-
#
|
| 137 |
-
#
|
|
|
|
|
|
|
| 138 |
easy:
|
| 139 |
description: >
|
| 140 |
-
Four rusher bands of rifle infantry charge your construction
|
| 141 |
-
(fact, at map centre (64,20)) CONCURRENTLY from the four
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
reach the fact. Stall, pure-army,
|
| 148 |
-
fact must survive; ≥
|
|
|
|
| 149 |
starting_cash: 2400
|
| 150 |
overrides:
|
| 151 |
actors:
|
|
@@ -155,35 +181,37 @@ levels:
|
|
| 155 |
- {type: fact, owner: agent, position: [64, 20]}
|
| 156 |
- {type: tent, owner: agent, position: [60, 16]}
|
| 157 |
- {type: powr, owner: agent, position: [60, 24]}
|
| 158 |
-
#
|
| 159 |
-
#
|
| 160 |
-
#
|
| 161 |
-
#
|
| 162 |
-
|
| 163 |
-
- {type: e1, owner: agent, position: [
|
| 164 |
-
- {type: e1, owner: agent, position: [62, 19], stance: 2}
|
| 165 |
-
- {type: e1, owner: agent, position: [62, 21], stance: 2}
|
| 166 |
-
# Four rusher bands, one per diagonal map corner. Each charges
|
| 167 |
-
# the agent centroid (the central fact), so each wave approaches
|
| 168 |
-
# along its own diagonal axis. 3× e1 per corner is enough to
|
| 169 |
-
# raze the fact through an uncovered corner but a single pbox
|
| 170 |
-
# planted in that corner shreds it.
|
| 171 |
-
- {type: e1, owner: enemy, position: [114, 6], stance: 3, count: 3}
|
| 172 |
-
- {type: e1, owner: enemy, position: [14, 6], stance: 3, count: 3}
|
| 173 |
-
- {type: e1, owner: enemy, position: [114, 34], stance: 3, count: 3}
|
| 174 |
-
- {type: e1, owner: enemy, position: [14, 34], stance: 3, count: 3}
|
| 175 |
# Unarmed high-HP marker (anti-DRAW): keeps the episode alive
|
| 176 |
# past full rusher elimination so the win/fail check fires.
|
| 177 |
- {type: fact, owner: enemy, position: [64, 4]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
win_condition:
|
| 179 |
all_of:
|
| 180 |
- {building_count_gte: {type: pbox, n: 4}}
|
| 181 |
-
- {building_in_region: {type: pbox, x:
|
| 182 |
-
- {building_in_region: {type: pbox, x:
|
| 183 |
-
- {building_in_region: {type: pbox, x:
|
| 184 |
-
- {building_in_region: {type: pbox, x:
|
| 185 |
- {building_count_gte: {type: fact, n: 1}}
|
| 186 |
-
- {units_killed_gte:
|
| 187 |
- {within_ticks: 5400}
|
| 188 |
fail_condition:
|
| 189 |
any_of:
|
|
@@ -191,63 +219,53 @@ levels:
|
|
| 191 |
- {not: {building_count_gte: {type: fact, n: 1}}}
|
| 192 |
max_turns: 60
|
| 193 |
|
| 194 |
-
# ── MEDIUM ── +1 axis:
|
| 195 |
-
#
|
| 196 |
-
#
|
| 197 |
-
#
|
| 198 |
-
# the
|
| 199 |
-
# merely survive on the clock. A concentrate layout, with three
|
| 200 |
-
# corners uncovered, both fails three region clauses AND lets those
|
| 201 |
-
# waves walk into the fact. The four-corner region bar is unchanged.
|
| 202 |
-
# max_turns 60 ⇒ reachable tick 5403; deadline 5400.
|
| 203 |
medium:
|
| 204 |
description: >
|
| 205 |
-
Four rusher bands —
|
| 206 |
-
your construction yard (fact, at map centre (64,20))
|
| 207 |
CONCURRENTLY from the four diagonal corners. Build 4 pillboxes
|
| 208 |
(budget exactly 2400cr = 4 pbox at 600 each) AND place ONE inside
|
| 209 |
-
the radius-4 disc of EACH corner region: NE (
|
| 210 |
-
SE (
|
| 211 |
corner satisfies only one region clause and lets the three
|
| 212 |
uncovered waves walk into the fact. Stall, pure-army, and
|
| 213 |
-
concentrate all LOSE. The fact must survive; ≥
|
| 214 |
-
die before tick 5400
|
| 215 |
-
actively clear the rush, not merely outlast it.
|
| 216 |
starting_cash: 2400
|
| 217 |
overrides:
|
| 218 |
actors:
|
| 219 |
- {type: fact, owner: agent, position: [64, 20]}
|
| 220 |
- {type: tent, owner: agent, position: [60, 16]}
|
| 221 |
- {type: powr, owner: agent, position: [60, 24]}
|
| 222 |
-
#
|
| 223 |
-
#
|
| 224 |
-
- {type: e1, owner: agent, position: [
|
| 225 |
-
- {type: e1, owner: agent, position: [66, 21], stance: 2}
|
| 226 |
-
- {type: e1, owner: agent, position: [62, 19], stance: 2}
|
| 227 |
-
- {type: e1, owner: agent, position: [62, 21], stance: 2}
|
| 228 |
-
# Four rusher bands, 3× e1 per corner (12 total) — the same
|
| 229 |
-
# count as easy so the central pre-placed defenders hold the
|
| 230 |
-
# fact long enough for the four-corner skirt to come online.
|
| 231 |
-
# The medium +1 axis is the raised kill bar (≥8 vs easy's ≥4):
|
| 232 |
-
# the skirt must actually engage and clear most of the rush,
|
| 233 |
-
# not merely survive it. A concentrate layout, with three
|
| 234 |
-
# corners uncovered, both fails three region clauses AND lets
|
| 235 |
-
# those waves through to the fact.
|
| 236 |
-
- {type: e1, owner: enemy, position: [114, 6], stance: 3, count: 3}
|
| 237 |
-
- {type: e1, owner: enemy, position: [14, 6], stance: 3, count: 3}
|
| 238 |
-
- {type: e1, owner: enemy, position: [114, 34], stance: 3, count: 3}
|
| 239 |
-
- {type: e1, owner: enemy, position: [14, 34], stance: 3, count: 3}
|
| 240 |
# Anti-DRAW marker.
|
| 241 |
- {type: fact, owner: enemy, position: [64, 4]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
win_condition:
|
| 243 |
all_of:
|
| 244 |
- {building_count_gte: {type: pbox, n: 4}}
|
| 245 |
-
- {building_in_region: {type: pbox, x:
|
| 246 |
-
- {building_in_region: {type: pbox, x:
|
| 247 |
-
- {building_in_region: {type: pbox, x:
|
| 248 |
-
- {building_in_region: {type: pbox, x:
|
| 249 |
- {building_count_gte: {type: fact, n: 1}}
|
| 250 |
-
- {units_killed_gte:
|
| 251 |
- {within_ticks: 5400}
|
| 252 |
fail_condition:
|
| 253 |
any_of:
|
|
@@ -259,96 +277,94 @@ levels:
|
|
| 259 |
# fact (and therefore the four corner regions of the skirt) FLIPS
|
| 260 |
# between a WEST base (fact at x=50) and an EAST base (fact at x=78)
|
| 261 |
# per seed — a 14-cell swing each way from the easy/medium centre at
|
| 262 |
-
# x=64. The flip is along the EAST-WEST axis
|
| 263 |
-
#
|
| 264 |
-
# corner
|
| 265 |
-
#
|
| 266 |
-
#
|
| 267 |
-
#
|
| 268 |
-
#
|
| 269 |
-
#
|
| 270 |
-
#
|
| 271 |
-
#
|
| 272 |
-
#
|
| 273 |
-
#
|
| 274 |
-
#
|
| 275 |
-
#
|
| 276 |
-
# the agent centroid, so all four bands converge on whichever fact
|
| 277 |
-
# is present this seed. Kill bar 8. max_turns 60 ⇒ reachable tick
|
| 278 |
-
# 5403; deadline 5400.
|
| 279 |
hard:
|
| 280 |
description: >
|
| 281 |
The agent construction yard (fact) flips between a WEST base
|
| 282 |
(fact at (50,20)) and an EAST base (fact at (78,20)) by seed; the
|
| 283 |
four corner regions of the skirt must follow. Build 4 pillboxes
|
| 284 |
(budget 2400cr = 4 pbox at 600 each) AND place ONE inside the
|
| 285 |
-
radius-4 disc of EACH corner of the CURRENT fact
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
|
|
|
|
|
|
|
|
|
| 291 |
starting_cash: 2400
|
| 292 |
overrides:
|
| 293 |
actors:
|
| 294 |
# spawn_point 0 — WEST base. Fact at (50,20); corner regions
|
| 295 |
-
# at NE (
|
| 296 |
-
#
|
| 297 |
-
#
|
| 298 |
-
#
|
| 299 |
-
# built in (the win predicate only scores the FINAL skirt, so
|
| 300 |
-
# build order must not decide the outcome).
|
| 301 |
- {type: fact, owner: agent, position: [50, 20], spawn_point: 0}
|
| 302 |
- {type: tent, owner: agent, position: [46, 16], spawn_point: 0}
|
| 303 |
- {type: powr, owner: agent, position: [46, 24], spawn_point: 0}
|
| 304 |
-
- {type: e1, owner: agent, position: [
|
| 305 |
-
- {type: e1, owner: agent, position: [52, 21], stance: 2, spawn_point: 0}
|
| 306 |
-
- {type: e1, owner: agent, position: [48, 19], stance: 2, spawn_point: 0}
|
| 307 |
-
- {type: e1, owner: agent, position: [48, 21], stance: 2, spawn_point: 0}
|
| 308 |
-
- {type: e1, owner: agent, position: [50, 18], stance: 2, spawn_point: 0}
|
| 309 |
-
- {type: e1, owner: agent, position: [50, 22], stance: 2, spawn_point: 0}
|
| 310 |
# spawn_point 1 — EAST base. Fact at (78,20); corner regions
|
| 311 |
-
# at NE (
|
| 312 |
-
# pre-placed e1 ring the fact (mirror of the WEST base).
|
| 313 |
- {type: fact, owner: agent, position: [78, 20], spawn_point: 1}
|
| 314 |
- {type: tent, owner: agent, position: [74, 16], spawn_point: 1}
|
| 315 |
- {type: powr, owner: agent, position: [74, 24], spawn_point: 1}
|
| 316 |
-
- {type: e1, owner: agent, position: [
|
| 317 |
-
- {type: e1, owner: agent, position: [80, 21], stance: 2, spawn_point: 1}
|
| 318 |
-
- {type: e1, owner: agent, position: [76, 19], stance: 2, spawn_point: 1}
|
| 319 |
-
- {type: e1, owner: agent, position: [76, 21], stance: 2, spawn_point: 1}
|
| 320 |
-
- {type: e1, owner: agent, position: [78, 18], stance: 2, spawn_point: 1}
|
| 321 |
-
- {type: e1, owner: agent, position: [78, 22], stance: 2, spawn_point: 1}
|
| 322 |
-
# Four rusher bands FIXED at the four map corners (3× e1 each =
|
| 323 |
-
# 12 total, same load as easy/medium). Enemies don't honour
|
| 324 |
-
# spawn_point so all four always place; the rusher charges the
|
| 325 |
-
# agent centroid so every band converges on whichever fact the
|
| 326 |
-
# seed selected — the seed flips ONLY the agent base / skirt
|
| 327 |
-
# geometry, not the threat count.
|
| 328 |
-
- {type: e1, owner: enemy, position: [114, 6], stance: 3, count: 3}
|
| 329 |
-
- {type: e1, owner: enemy, position: [14, 6], stance: 3, count: 3}
|
| 330 |
-
- {type: e1, owner: enemy, position: [114, 34], stance: 3, count: 3}
|
| 331 |
-
- {type: e1, owner: enemy, position: [14, 34], stance: 3, count: 3}
|
| 332 |
# Anti-DRAW marker on the symmetry axis.
|
| 333 |
- {type: fact, owner: enemy, position: [64, 4]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
win_condition:
|
| 335 |
all_of:
|
| 336 |
- {building_count_gte: {type: pbox, n: 4}}
|
| 337 |
- any_of:
|
| 338 |
-
# WEST base (fact x=50):
|
| 339 |
- all_of:
|
| 340 |
-
- {building_in_region: {type: pbox, x:
|
| 341 |
-
- {building_in_region: {type: pbox, x:
|
| 342 |
-
- {building_in_region: {type: pbox, x:
|
| 343 |
-
- {building_in_region: {type: pbox, x:
|
| 344 |
-
# EAST base (fact x=78):
|
| 345 |
- all_of:
|
| 346 |
-
- {building_in_region: {type: pbox, x:
|
| 347 |
-
- {building_in_region: {type: pbox, x:
|
| 348 |
-
- {building_in_region: {type: pbox, x:
|
| 349 |
-
- {building_in_region: {type: pbox, x:
|
| 350 |
- {building_count_gte: {type: fact, n: 1}}
|
| 351 |
-
- {units_killed_gte:
|
| 352 |
- {within_ticks: 5400}
|
| 353 |
fail_condition:
|
| 354 |
any_of:
|
|
|
|
| 14 |
#
|
| 15 |
# The win predicate makes the four-corner topology load-bearing — total
|
| 16 |
# pbox count alone is NOT enough; ONE pbox must sit inside a radius-4
|
| 17 |
+
# disc in EACH of the four corner regions (NE/NW/SE/SW of the fact),
|
| 18 |
+
# AND the pillboxes must actually KILL the rush (`units_killed_gte`).
|
| 19 |
# A concentrate-at-one-corner policy meets the count (4 pbox) but
|
| 20 |
# satisfies only ONE of the four region clauses and fails the other
|
| 21 |
# three — AND the three uncovered corner waves reach the fact and raze
|
| 22 |
# it. Concentrate LOSES; the intended four-corner skirt WINS.
|
| 23 |
#
|
| 24 |
+
# pbox is the load-bearing weapon. After the engine pbox-weapon fix
|
| 25 |
+
# (`fix(engine): pbox gets a direct-fire Armament`) a BUILT pbox is an
|
| 26 |
+
# active direct-fire anti-infantry tower (M60mg MG: a burst shreds an
|
| 27 |
+
# e1). The four rush bands arrive as a `scheduled_events: spawn_actors`
|
| 28 |
+
# wave at a fixed tick — AFTER the agent has had time to build all 4
|
| 29 |
+
# pillboxes serially. Each band spawns AT one of the four corner
|
| 30 |
+
# regions, so a pbox planted in that region engages its band the moment
|
| 31 |
+
# the wave arrives. There are NO pre-placed agent defenders, so the
|
| 32 |
+
# pbox SKIRT is the sole source of kill output: a concentrate /
|
| 33 |
+
# pure-army layout leaves three corners uncovered — those bands walk to
|
| 34 |
+
# the fact unopposed and raze it.
|
| 35 |
+
#
|
| 36 |
# Real-world anchors (binding meta.benchmark_anchor):
|
| 37 |
# • MicroRTS pillbox placement — the canonical RTS sub-benchmark for
|
| 38 |
# where a finite set of static defences should be planted; the
|
|
|
|
| 48 |
# converge on the fact and raze it — the fact-alive fail clause
|
| 49 |
# fires → LOSS (`after_ticks` is the deterministic backstop, no
|
| 50 |
# draw degeneracy).
|
| 51 |
+
# • concentrate (all 4 pbox massed at ONE corner): meets the count
|
| 52 |
+
# clause and satisfies the one region clause for that corner but
|
| 53 |
+
# FAILS the other three region clauses (0 pbox in each) AND the
|
| 54 |
+
# three uncovered corner waves walk into the fact and raze it →
|
| 55 |
+
# LOSS.
|
| 56 |
# • pure-army (only e1, never a pbox): FAILS the count clause AND all
|
| 57 |
# four region clauses; the rifle wall alone cannot hold four
|
| 58 |
# concurrent rushes → LOSS.
|
|
|
|
| 62 |
# fact-alive clauses all satisfied → WIN.
|
| 63 |
#
|
| 64 |
# Why the spec works (engine combat sheet, per CLAUDE.md):
|
| 65 |
+
# • pbox = pillbox (anti-infantry base defence; cost 600cr; direct-
|
| 66 |
+
# fire M60mg anti-infantry MG after the engine pbox-weapon fix;
|
| 67 |
+
# 1×1 footprint). Defence and infantry are SEPARATE production
|
| 68 |
+
# queues, so a model may queue build('pbox') and build('e1') in
|
| 69 |
+
# parallel.
|
| 70 |
# • `building_count_gte:{pbox,4}` + FOUR `building_in_region` clauses
|
| 71 |
# (one radius-4 disc per corner) make the four-corner SKIRT load-
|
| 72 |
# bearing: a concentration satisfies the count and exactly one
|
| 73 |
# region clause and fails the other three.
|
| 74 |
+
# • `units_killed_gte:K` makes the pbox weapon load-bearing: with no
|
| 75 |
+
# pre-placed agent defenders the pbox SKIRT is the sole kill
|
| 76 |
+
# source; a stall / pure-army layout kills 0.
|
| 77 |
# • `building_count_gte:{fact,1}` (PRESENT-TENSE) is the fact-alive
|
| 78 |
# check — `has_building:fact` is a one-shot ever-seen set that
|
| 79 |
# stays true after the fact is razed (documented CLAUDE.md footgun).
|
| 80 |
+
# • The rush arrives as a `scheduled_events: spawn_actors` wave at a
|
| 81 |
+
# fixed tick (1800), AFTER the agent has had time to build 4 pbox
|
| 82 |
+
# serially. This makes the build/rush race fair: the intended skirt
|
| 83 |
+
# comfortably completes before the wave, while a staller /
|
| 84 |
+
# concentrate / pure-army spend still cannot satisfy the predicate.
|
| 85 |
# • A persistent unarmed enemy `fact` keeps the episode alive past
|
| 86 |
# full rusher elimination so the win/fail evaluator sees the
|
| 87 |
# terminal frame (no DRAW collapse on enemy-wipe auto-done).
|
| 88 |
+
# • The `rusher` bot charges the agent centroid (the fact) — that is
|
| 89 |
+
# WHY a corner wave whose corner has no pbox walks straight into
|
| 90 |
+
# the fact, and why a skirt with a pbox in every corner shreds
|
| 91 |
+
# every wave at its own approach.
|
| 92 |
# • Cash is intentionally tight (exactly 4×600cr = 2400cr). A model
|
| 93 |
# that wastes cash on extra units cannot complete the pbox count.
|
| 94 |
|
|
|
|
| 107 |
the three uncovered corner waves stride untouched into the fact and
|
| 108 |
raze it. The win predicate makes the topology load-bearing: total
|
| 109 |
pbox count is not enough — one pbox must sit inside a radius-4 disc
|
| 110 |
+
in EVERY corner region, the pillboxes must KILL the rush, AND the
|
| 111 |
+
fact must survive.
|
| 112 |
robotics_analogue: >
|
| 113 |
Distributed defense / quadrant coverage: when one central asset is
|
| 114 |
threatened from all bearings, finite defensive capacity must be
|
|
|
|
| 150 |
levels:
|
| 151 |
# ── EASY ── bare FOUR-CORNER-SKIRT skill: budget covers exactly 4
|
| 152 |
# pbox (2400cr). Win requires the count (4 pbox) AND one pbox inside
|
| 153 |
+
# the radius-4 disc of EACH corner region (NE (82,9), NW (46,9),
|
| 154 |
+
# SE (82,31), SW (46,31) — the four diagonal corners around the fact
|
| 155 |
+
# at (64,20)) AND a kill quota (≥9). Each rush band spawns AT one
|
| 156 |
+
# corner region so a pbox planted there engages it immediately. A
|
| 157 |
# concentrate-at-one-corner layout meets the count but satisfies
|
| 158 |
+
# exactly one region clause AND lets three corner waves through. The
|
| 159 |
+
# rush arrives at tick 1800 — after the skirt has had time to
|
| 160 |
+
# assemble. Stall loses (clock OR fact razed); pure-army loses
|
| 161 |
+
# (count). max_turns 60 ⇒ reachable tick 93+90·59 = 5403;
|
| 162 |
+
# deadline 5400.
|
| 163 |
easy:
|
| 164 |
description: >
|
| 165 |
+
Four rusher bands of rifle infantry will charge your construction
|
| 166 |
+
yard (fact, at map centre (64,20)) CONCURRENTLY from the four
|
| 167 |
+
diagonal corners. Build 4 pillboxes (pbox — 600cr each, budget
|
| 168 |
+
exactly 2400) AND place ONE of them inside the radius-4 disc of
|
| 169 |
+
EACH corner region: NE (82,9), NW (46,9), SE (82,31), SW (46,31).
|
| 170 |
+
Each band spawns at one corner — a pbox planted there shreds it.
|
| 171 |
+
Massing all four pillboxes on a single corner holds that corner
|
| 172 |
+
but lets the other three waves reach the fact. Stall, pure-army,
|
| 173 |
+
and concentrate all LOSE. The fact must survive; ≥9 enemy units
|
| 174 |
+
must die before tick 5400.
|
| 175 |
starting_cash: 2400
|
| 176 |
overrides:
|
| 177 |
actors:
|
|
|
|
| 181 |
- {type: fact, owner: agent, position: [64, 20]}
|
| 182 |
- {type: tent, owner: agent, position: [60, 16]}
|
| 183 |
- {type: powr, owner: agent, position: [60, 24]}
|
| 184 |
+
# ONE non-combatant agent e1 parked in the far NW map corner,
|
| 185 |
+
# nowhere near the fact or any corner disc. It exists only so
|
| 186 |
+
# units_summary is non-empty (hard-tier env-reset check); it
|
| 187 |
+
# never reaches combat and contributes ZERO kills — the pbox
|
| 188 |
+
# SKIRT is the sole source of kill output.
|
| 189 |
+
- {type: e1, owner: agent, position: [4, 4], stance: 2}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
# Unarmed high-HP marker (anti-DRAW): keeps the episode alive
|
| 191 |
# past full rusher elimination so the win/fail check fires.
|
| 192 |
- {type: fact, owner: enemy, position: [64, 4]}
|
| 193 |
+
# Scheduled rush wave — injected at tick 1800, one band AT each
|
| 194 |
+
# of the four corner regions. The rusher bot charges the agent
|
| 195 |
+
# centroid (the central fact). By tick 1800 all 4 skirt pillboxes
|
| 196 |
+
# are built; a pbox planted in a corner engages that corner's
|
| 197 |
+
# band the moment the wave spawns.
|
| 198 |
+
scheduled_events:
|
| 199 |
+
- tick: 1800
|
| 200 |
+
type: spawn_actors
|
| 201 |
+
actors:
|
| 202 |
+
- {type: e1, owner: enemy, position: [82, 9], stance: 3, count: 3}
|
| 203 |
+
- {type: e1, owner: enemy, position: [46, 9], stance: 3, count: 3}
|
| 204 |
+
- {type: e1, owner: enemy, position: [82, 31], stance: 3, count: 3}
|
| 205 |
+
- {type: e1, owner: enemy, position: [46, 31], stance: 3, count: 3}
|
| 206 |
win_condition:
|
| 207 |
all_of:
|
| 208 |
- {building_count_gte: {type: pbox, n: 4}}
|
| 209 |
+
- {building_in_region: {type: pbox, x: 82, y: 9, radius: 4, count: 1}}
|
| 210 |
+
- {building_in_region: {type: pbox, x: 46, y: 9, radius: 4, count: 1}}
|
| 211 |
+
- {building_in_region: {type: pbox, x: 82, y: 31, radius: 4, count: 1}}
|
| 212 |
+
- {building_in_region: {type: pbox, x: 46, y: 31, radius: 4, count: 1}}
|
| 213 |
- {building_count_gte: {type: fact, n: 1}}
|
| 214 |
+
- {units_killed_gte: 9}
|
| 215 |
- {within_ticks: 5400}
|
| 216 |
fail_condition:
|
| 217 |
any_of:
|
|
|
|
| 219 |
- {not: {building_count_gte: {type: fact, n: 1}}}
|
| 220 |
max_turns: 60
|
| 221 |
|
| 222 |
+
# ── MEDIUM ── +1 axis: heavier rush (4× e1 per corner = 16 total)
|
| 223 |
+
# and a higher kill bar (≥13). The four-corner region bar is
|
| 224 |
+
# unchanged. A concentrate layout, with three corners uncovered,
|
| 225 |
+
# both fails three region clauses AND lets those heavier waves walk
|
| 226 |
+
# into the fact. max_turns 60 ⇒ reachable tick 5403; deadline 5400.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
medium:
|
| 228 |
description: >
|
| 229 |
+
Four rusher bands — 4 rifle infantry each (16 total) — will
|
| 230 |
+
charge your construction yard (fact, at map centre (64,20))
|
| 231 |
CONCURRENTLY from the four diagonal corners. Build 4 pillboxes
|
| 232 |
(budget exactly 2400cr = 4 pbox at 600 each) AND place ONE inside
|
| 233 |
+
the radius-4 disc of EACH corner region: NE (82,9), NW (46,9),
|
| 234 |
+
SE (82,31), SW (46,31). Massing all four pillboxes on a single
|
| 235 |
corner satisfies only one region clause and lets the three
|
| 236 |
uncovered waves walk into the fact. Stall, pure-army, and
|
| 237 |
+
concentrate all LOSE. The fact must survive; ≥13 enemy units
|
| 238 |
+
must die before tick 5400.
|
|
|
|
| 239 |
starting_cash: 2400
|
| 240 |
overrides:
|
| 241 |
actors:
|
| 242 |
- {type: fact, owner: agent, position: [64, 20]}
|
| 243 |
- {type: tent, owner: agent, position: [60, 16]}
|
| 244 |
- {type: powr, owner: agent, position: [60, 24]}
|
| 245 |
+
# Non-combatant NW-corner e1 (see easy) — non-empty
|
| 246 |
+
# units_summary, zero kill contribution.
|
| 247 |
+
- {type: e1, owner: agent, position: [4, 4], stance: 2}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
# Anti-DRAW marker.
|
| 249 |
- {type: fact, owner: enemy, position: [64, 4]}
|
| 250 |
+
# Heavier rush wave — 4× e1 per corner (16 total), injected at
|
| 251 |
+
# tick 1800 AT each of the four corner regions.
|
| 252 |
+
scheduled_events:
|
| 253 |
+
- tick: 1800
|
| 254 |
+
type: spawn_actors
|
| 255 |
+
actors:
|
| 256 |
+
- {type: e1, owner: enemy, position: [82, 9], stance: 3, count: 4}
|
| 257 |
+
- {type: e1, owner: enemy, position: [46, 9], stance: 3, count: 4}
|
| 258 |
+
- {type: e1, owner: enemy, position: [82, 31], stance: 3, count: 4}
|
| 259 |
+
- {type: e1, owner: enemy, position: [46, 31], stance: 3, count: 4}
|
| 260 |
win_condition:
|
| 261 |
all_of:
|
| 262 |
- {building_count_gte: {type: pbox, n: 4}}
|
| 263 |
+
- {building_in_region: {type: pbox, x: 82, y: 9, radius: 4, count: 1}}
|
| 264 |
+
- {building_in_region: {type: pbox, x: 46, y: 9, radius: 4, count: 1}}
|
| 265 |
+
- {building_in_region: {type: pbox, x: 82, y: 31, radius: 4, count: 1}}
|
| 266 |
+
- {building_in_region: {type: pbox, x: 46, y: 31, radius: 4, count: 1}}
|
| 267 |
- {building_count_gte: {type: fact, n: 1}}
|
| 268 |
+
- {units_killed_gte: 13}
|
| 269 |
- {within_ticks: 5400}
|
| 270 |
fail_condition:
|
| 271 |
any_of:
|
|
|
|
| 277 |
# fact (and therefore the four corner regions of the skirt) FLIPS
|
| 278 |
# between a WEST base (fact at x=50) and an EAST base (fact at x=78)
|
| 279 |
# per seed — a 14-cell swing each way from the easy/medium centre at
|
| 280 |
+
# x=64. The flip is along the EAST-WEST axis so the fact stays at
|
| 281 |
+
# y=20 and remains equidistant from all four diagonal corner rushes.
|
| 282 |
+
# The four corner regions track the fact: WEST fact (50,20) → NE
|
| 283 |
+
# (68,9) NW (32,9) SE (68,31) SW (32,31); EAST fact (78,20) → NE
|
| 284 |
+
# (96,9) NW (60,9) SE (96,31) SW (60,31). The scheduled wave spawns
|
| 285 |
+
# ALL EIGHT bands (four per candidate fact) — the spawn_actors list
|
| 286 |
+
# is not spawn_point-filtered — so whichever fact the seed selects,
|
| 287 |
+
# its four corner regions each have a band sitting on them for the
|
| 288 |
+
# skirt to engage, while the four off-fact bands charge across the
|
| 289 |
+
# map toward the active fact. A single memorised "skirt (64,20)"
|
| 290 |
+
# plan does NOT work: the easy/medium discs (x∈{46,82}) land ≥10
|
| 291 |
+
# cells outside every hard disc, failing the region bar. The model
|
| 292 |
+
# must READ the fact's longitude from observation. Kill bar 13.
|
| 293 |
+
# max_turns 60 ⇒ reachable tick 5403; deadline 5400.
|
|
|
|
|
|
|
|
|
|
| 294 |
hard:
|
| 295 |
description: >
|
| 296 |
The agent construction yard (fact) flips between a WEST base
|
| 297 |
(fact at (50,20)) and an EAST base (fact at (78,20)) by seed; the
|
| 298 |
four corner regions of the skirt must follow. Build 4 pillboxes
|
| 299 |
(budget 2400cr = 4 pbox at 600 each) AND place ONE inside the
|
| 300 |
+
radius-4 disc of EACH corner of the CURRENT fact. For a WEST fact
|
| 301 |
+
(50,20) the corners are NE (68,9) NW (32,9) SE (68,31) SW
|
| 302 |
+
(32,31); for an EAST fact (78,20) they are NE (96,9) NW (60,9)
|
| 303 |
+
SE (96,31) SW (60,31) — read the fact's longitude from the
|
| 304 |
+
observation. Massing all four pillboxes on one corner, or
|
| 305 |
+
skirting the wrong (x=64) centre, satisfies at most one region
|
| 306 |
+
clause and lets the uncovered waves raze the fact. Stall,
|
| 307 |
+
pure-army, and concentrate all LOSE. The fact must survive;
|
| 308 |
+
≥13 enemy units must die before tick 5400.
|
| 309 |
starting_cash: 2400
|
| 310 |
overrides:
|
| 311 |
actors:
|
| 312 |
# spawn_point 0 — WEST base. Fact at (50,20); corner regions
|
| 313 |
+
# at NE (68,9), NW (32,9), SE (68,31), SW (32,31). A single
|
| 314 |
+
# non-combatant e1 parks in the far SW map corner (zero kill
|
| 315 |
+
# contribution; non-empty units_summary for the env-reset
|
| 316 |
+
# check).
|
|
|
|
|
|
|
| 317 |
- {type: fact, owner: agent, position: [50, 20], spawn_point: 0}
|
| 318 |
- {type: tent, owner: agent, position: [46, 16], spawn_point: 0}
|
| 319 |
- {type: powr, owner: agent, position: [46, 24], spawn_point: 0}
|
| 320 |
+
- {type: e1, owner: agent, position: [4, 36], stance: 2, spawn_point: 0}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
# spawn_point 1 — EAST base. Fact at (78,20); corner regions
|
| 322 |
+
# at NE (96,9), NW (60,9), SE (96,31), SW (60,31).
|
|
|
|
| 323 |
- {type: fact, owner: agent, position: [78, 20], spawn_point: 1}
|
| 324 |
- {type: tent, owner: agent, position: [74, 16], spawn_point: 1}
|
| 325 |
- {type: powr, owner: agent, position: [74, 24], spawn_point: 1}
|
| 326 |
+
- {type: e1, owner: agent, position: [4, 36], stance: 2, spawn_point: 1}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
# Anti-DRAW marker on the symmetry axis.
|
| 328 |
- {type: fact, owner: enemy, position: [64, 4]}
|
| 329 |
+
# Scheduled rush wave — EIGHT bands (3× e1 each), four sitting on
|
| 330 |
+
# the WEST candidate's corner regions and four on the EAST
|
| 331 |
+
# candidate's, injected at tick 1800. The spawn_actors list is
|
| 332 |
+
# not spawn_point-filtered so all eight always inject; the rusher
|
| 333 |
+
# charges the agent centroid so the four bands on the active
|
| 334 |
+
# fact's corners are engaged by the skirt at their discs while
|
| 335 |
+
# the four off-fact bands charge across to the active fact.
|
| 336 |
+
scheduled_events:
|
| 337 |
+
- tick: 1800
|
| 338 |
+
type: spawn_actors
|
| 339 |
+
actors:
|
| 340 |
+
# WEST candidate corners.
|
| 341 |
+
- {type: e1, owner: enemy, position: [68, 9], stance: 3, count: 3}
|
| 342 |
+
- {type: e1, owner: enemy, position: [32, 9], stance: 3, count: 3}
|
| 343 |
+
- {type: e1, owner: enemy, position: [68, 31], stance: 3, count: 3}
|
| 344 |
+
- {type: e1, owner: enemy, position: [32, 31], stance: 3, count: 3}
|
| 345 |
+
# EAST candidate corners.
|
| 346 |
+
- {type: e1, owner: enemy, position: [96, 9], stance: 3, count: 3}
|
| 347 |
+
- {type: e1, owner: enemy, position: [60, 9], stance: 3, count: 3}
|
| 348 |
+
- {type: e1, owner: enemy, position: [96, 31], stance: 3, count: 3}
|
| 349 |
+
- {type: e1, owner: enemy, position: [60, 31], stance: 3, count: 3}
|
| 350 |
win_condition:
|
| 351 |
all_of:
|
| 352 |
- {building_count_gte: {type: pbox, n: 4}}
|
| 353 |
- any_of:
|
| 354 |
+
# WEST base (fact x=50): four corner regions.
|
| 355 |
- all_of:
|
| 356 |
+
- {building_in_region: {type: pbox, x: 68, y: 9, radius: 4, count: 1}}
|
| 357 |
+
- {building_in_region: {type: pbox, x: 32, y: 9, radius: 4, count: 1}}
|
| 358 |
+
- {building_in_region: {type: pbox, x: 68, y: 31, radius: 4, count: 1}}
|
| 359 |
+
- {building_in_region: {type: pbox, x: 32, y: 31, radius: 4, count: 1}}
|
| 360 |
+
# EAST base (fact x=78): four corner regions.
|
| 361 |
- all_of:
|
| 362 |
+
- {building_in_region: {type: pbox, x: 96, y: 9, radius: 4, count: 1}}
|
| 363 |
+
- {building_in_region: {type: pbox, x: 60, y: 9, radius: 4, count: 1}}
|
| 364 |
+
- {building_in_region: {type: pbox, x: 96, y: 31, radius: 4, count: 1}}
|
| 365 |
+
- {building_in_region: {type: pbox, x: 60, y: 31, radius: 4, count: 1}}
|
| 366 |
- {building_count_gte: {type: fact, n: 1}}
|
| 367 |
+
- {units_killed_gte: 13}
|
| 368 |
- {within_ticks: 5400}
|
| 369 |
fail_condition:
|
| 370 |
any_of:
|
|
@@ -22,11 +22,18 @@ pbox count alone is not enough:
|
|
| 22 |
ever-seen set that stays true after the fact is razed — CLAUDE.md
|
| 23 |
footgun);
|
| 24 |
* `units_killed_gte:K` ⇒ the skirt has to actually engage the rush;
|
|
|
|
|
|
|
|
|
|
| 25 |
* `within_ticks` paired with `after_ticks` ⇒ a non-finisher is a real
|
| 26 |
reachable timeout LOSS (no interrupts ⇒ each step is exactly 90
|
| 27 |
ticks, so max_turns is a hard tick budget the `after_ticks` deadline
|
| 28 |
reliably bites in).
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
The scripted-policy validations prove deterministically that:
|
| 31 |
|
| 32 |
* the intended adaptive SKIRT policy (one pbox in EACH of the four
|
|
@@ -91,10 +98,17 @@ def _fact_cell(rs):
|
|
| 91 |
return facts[0].get("cell_x"), facts[0].get("cell_y")
|
| 92 |
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
def make_adaptive_skirt():
|
| 95 |
"""Intended SKIRT topology: read the fact's cell from the observation
|
| 96 |
on turn 1, then place one pbox in EACH of the four corner regions
|
| 97 |
-
around it (offsets +/-
|
| 98 |
pack rewards: the skirt must follow the fact, which on hard flips
|
| 99 |
between x=50 and x=78 by seed."""
|
| 100 |
state = {"cells": None}
|
|
@@ -105,12 +119,7 @@ def make_adaptive_skirt():
|
|
| 105 |
if fc is None:
|
| 106 |
return [C.observe()]
|
| 107 |
fx, fy = fc
|
| 108 |
-
state["cells"] = [
|
| 109 |
-
(fx + 12, fy - 10), # NE
|
| 110 |
-
(fx - 12, fy - 10), # NW
|
| 111 |
-
(fx + 12, fy + 10), # SE
|
| 112 |
-
(fx - 12, fy + 10), # SW
|
| 113 |
-
]
|
| 114 |
return _build_and_place(rs, C, state["cells"])
|
| 115 |
|
| 116 |
return policy
|
|
@@ -129,25 +138,23 @@ def make_concentrate():
|
|
| 129 |
if fc is None:
|
| 130 |
return [C.observe()]
|
| 131 |
fx, fy = fc
|
| 132 |
-
cx, cy = fx +
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
(cx - 1, cy + 1), (cx + 1, cy + 1),
|
| 136 |
-
]
|
| 137 |
return _build_and_place(rs, C, state["cells"])
|
| 138 |
|
| 139 |
return policy
|
| 140 |
|
| 141 |
|
| 142 |
def make_wrong_centre_skirt():
|
| 143 |
-
"""A skirt centred on the OLD (64,20) location —
|
| 144 |
-
|
| 145 |
-
(
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
cells = [(
|
| 151 |
|
| 152 |
def policy(rs, C):
|
| 153 |
return _build_and_place(rs, C, cells)
|
|
@@ -231,6 +238,56 @@ def test_win_requires_four_corner_region_clauses():
|
|
| 231 |
assert len(regions) == 4, layout
|
| 232 |
|
| 233 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
@pytest.mark.parametrize("level", LEVELS)
|
| 235 |
def test_every_level_has_a_reachable_timeout_fail(level):
|
| 236 |
"""Non-win must be a real LOSS: the `after_ticks` fail clause must
|
|
|
|
| 22 |
ever-seen set that stays true after the fact is razed — CLAUDE.md
|
| 23 |
footgun);
|
| 24 |
* `units_killed_gte:K` ⇒ the skirt has to actually engage the rush;
|
| 25 |
+
the pbox is the load-bearing weapon (engine pbox-weapon fix), and
|
| 26 |
+
with no pre-placed agent defenders the pbox skirt is the SOLE kill
|
| 27 |
+
source — a stall / pure-army layout kills 0;
|
| 28 |
* `within_ticks` paired with `after_ticks` ⇒ a non-finisher is a real
|
| 29 |
reachable timeout LOSS (no interrupts ⇒ each step is exactly 90
|
| 30 |
ticks, so max_turns is a hard tick budget the `after_ticks` deadline
|
| 31 |
reliably bites in).
|
| 32 |
|
| 33 |
+
The rush arrives as a `scheduled_events: spawn_actors` wave at tick
|
| 34 |
+
1800 — AFTER the skirt has had time to assemble — with one band sitting
|
| 35 |
+
ON each corner region so a pbox planted there engages it immediately.
|
| 36 |
+
|
| 37 |
The scripted-policy validations prove deterministically that:
|
| 38 |
|
| 39 |
* the intended adaptive SKIRT policy (one pbox in EACH of the four
|
|
|
|
| 98 |
return facts[0].get("cell_x"), facts[0].get("cell_y")
|
| 99 |
|
| 100 |
|
| 101 |
+
# Corner-region offsets from the fact (NE, NW, SE, SW). The four
|
| 102 |
+
# corner discs sit at fact + (+/-18, +/-11). The scheduled rush bands
|
| 103 |
+
# spawn ON these discs, so a pbox planted in a disc engages its band
|
| 104 |
+
# the moment the wave arrives.
|
| 105 |
+
SKIRT_OFFSETS = [(18, -11), (-18, -11), (18, 11), (-18, 11)]
|
| 106 |
+
|
| 107 |
+
|
| 108 |
def make_adaptive_skirt():
|
| 109 |
"""Intended SKIRT topology: read the fact's cell from the observation
|
| 110 |
on turn 1, then place one pbox in EACH of the four corner regions
|
| 111 |
+
around it (offsets +/-18 in x, +/-11 in y). This is the policy the
|
| 112 |
pack rewards: the skirt must follow the fact, which on hard flips
|
| 113 |
between x=50 and x=78 by seed."""
|
| 114 |
state = {"cells": None}
|
|
|
|
| 119 |
if fc is None:
|
| 120 |
return [C.observe()]
|
| 121 |
fx, fy = fc
|
| 122 |
+
state["cells"] = [(fx + dx, fy + dy) for dx, dy in SKIRT_OFFSETS]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
return _build_and_place(rs, C, state["cells"])
|
| 124 |
|
| 125 |
return policy
|
|
|
|
| 138 |
if fc is None:
|
| 139 |
return [C.observe()]
|
| 140 |
fx, fy = fc
|
| 141 |
+
cx, cy = fx + 18, fy - 11 # NE corner
|
| 142 |
+
# Four pboxes in a 1-cell-spaced row at the NE corner.
|
| 143 |
+
state["cells"] = [(cx - i, cy) for i in range(4)]
|
|
|
|
|
|
|
| 144 |
return _build_and_place(rs, C, state["cells"])
|
| 145 |
|
| 146 |
return policy
|
| 147 |
|
| 148 |
|
| 149 |
def make_wrong_centre_skirt():
|
| 150 |
+
"""A skirt centred on the OLD (64,20) location — the easy/medium
|
| 151 |
+
fact cell — but applied on HARD where the fact is at (50,20) or
|
| 152 |
+
(78,20) per seed. A skirt around (64,20) lands every pbox >=10 cells
|
| 153 |
+
outside the active corner discs (radius 4), failing the region
|
| 154 |
+
clauses. Demonstrates the spawn-driven discrimination: a memorised
|
| 155 |
+
cell list that worked at lower tiers does NOT generalise to the hard
|
| 156 |
+
fact-flip."""
|
| 157 |
+
cells = [(64 + dx, 20 + dy) for dx, dy in SKIRT_OFFSETS]
|
| 158 |
|
| 159 |
def policy(rs, C):
|
| 160 |
return _build_and_place(rs, C, cells)
|
|
|
|
| 238 |
assert len(regions) == 4, layout
|
| 239 |
|
| 240 |
|
| 241 |
+
def test_win_requires_a_kill_quota():
|
| 242 |
+
"""The pbox skirt must actively KILL the rush: every level's win
|
| 243 |
+
clause carries a `units_killed_gte` quota. With no pre-placed agent
|
| 244 |
+
defenders the pbox skirt is the sole kill source, so this clause
|
| 245 |
+
makes the pbox weapon load-bearing."""
|
| 246 |
+
pack = load_pack(PACK)
|
| 247 |
+
for lvl in LEVELS:
|
| 248 |
+
c = compile_level(pack, lvl)
|
| 249 |
+
wc = c.win_condition.model_dump(exclude_none=True)
|
| 250 |
+
kill = [
|
| 251 |
+
cl for cl in wc.get("all_of", []) or []
|
| 252 |
+
if isinstance(cl, dict) and "units_killed_gte" in cl
|
| 253 |
+
]
|
| 254 |
+
assert kill, f"{lvl}: missing units_killed_gte kill quota"
|
| 255 |
+
assert int(kill[0]["units_killed_gte"]) >= 8, (lvl, kill)
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def test_rush_arrives_as_a_scheduled_event():
|
| 259 |
+
"""The four-corner rush is injected via `scheduled_events:
|
| 260 |
+
spawn_actors` AFTER the skirt has time to assemble — there is no
|
| 261 |
+
t=0 enemy band racing the build."""
|
| 262 |
+
pack = load_pack(PACK)
|
| 263 |
+
for lvl in LEVELS:
|
| 264 |
+
raw = pack.levels[lvl]
|
| 265 |
+
ov = getattr(raw, "overrides", None) or {}
|
| 266 |
+
if hasattr(ov, "model_dump"):
|
| 267 |
+
ov = ov.model_dump(exclude_none=True)
|
| 268 |
+
evts = ov.get("scheduled_events") or []
|
| 269 |
+
assert evts, f"{lvl}: expected a scheduled rush wave"
|
| 270 |
+
assert any(e.get("type") == "spawn_actors" for e in evts), (lvl, evts)
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def test_no_pre_placed_agent_combat_screen():
|
| 274 |
+
"""The pbox skirt must be the sole kill source — there is no
|
| 275 |
+
pre-placed agent combat screen ringing the fact. Only ONE
|
| 276 |
+
non-combatant agent e1 per active spawn group is parked in a far
|
| 277 |
+
map corner (so units_summary is non-empty for the env-reset check);
|
| 278 |
+
it never fights."""
|
| 279 |
+
for lvl in LEVELS:
|
| 280 |
+
c = compile_level(load_pack(PACK), lvl)
|
| 281 |
+
agent_units = [
|
| 282 |
+
a for a in c.scenario.actors
|
| 283 |
+
if a.owner == "agent" and a.type == "e1"
|
| 284 |
+
]
|
| 285 |
+
assert len(agent_units) <= 2, (lvl, [a.position for a in agent_units])
|
| 286 |
+
for a in agent_units:
|
| 287 |
+
x, y = a.position
|
| 288 |
+
assert x <= 6 and (y <= 6 or y >= 34), (lvl, a.position)
|
| 289 |
+
|
| 290 |
+
|
| 291 |
@pytest.mark.parametrize("level", LEVELS)
|
| 292 |
def test_every_level_has_a_reachable_timeout_fail(level):
|
| 293 |
"""Non-win must be a real LOSS: the `after_ticks` fail clause must
|