Spaces:
Running
fix(scenario): build-repair-priority-under-fire — recalibrate after engine movement fixes
Browse filesThe engine move-fire fixes made pre-placed stance:3 defenders resolve a
clustered fire-fight in one ~90-tick window with the buildings never
scratched: stall WON every level/seed (all bands cleared by tick 108,
buildings at 100%, kill bar instantly met). The old slow damage race no
longer exists.
Redesign: drop the agent combat units and the kill bar entirely; the
triage is now a pure `repair`-target decision against an unopposed
grenadier band. proc (wood) dies unrepaired and is saved by autorepair;
pbox is pre-damaged (health: 30) Heavy-armoured decoy that survives on
its own; weap rides out a bridged-proc-pinned band on easy but needs
its own repair on medium/hard. Win now requires SURVIVING a danger
window (after_ticks floor + within_ticks ceiling) so it cannot latch on
turn 1. stall / repair-pbox lose every level+seed; repair-proc wins
easy, loses medium/hard; intended repair-proc+weap and repair-all win
every level+seed.
|
@@ -1,48 +1,56 @@
|
|
| 1 |
# build-repair-priority-under-fire — criticality-weighted repair triage:
|
| 2 |
# three damaged buildings under fire, repair the high-value-and-lethally-
|
| 3 |
-
# threatened one FIRST.
|
| 4 |
#
|
| 5 |
# REASONING capability — Wave-10 "disaster-recovery triage / criticality-
|
| 6 |
# weighted maintenance" pack. Three of the agent's structures are under
|
| 7 |
# simultaneous attrition from three separate grenadier bands, and the
|
| 8 |
-
# agent's hold-the-line job is a pure TRIAGE DECISION: which building
|
| 9 |
-
# put the `repair` organ on
|
| 10 |
# deliberately mis-leading damage picture:
|
| 11 |
#
|
| 12 |
-
# - proc (refinery) — HIGH value, on a LETHAL trajectory.
|
| 13 |
-
#
|
| 14 |
-
#
|
| 15 |
-
#
|
| 16 |
-
#
|
| 17 |
-
#
|
| 18 |
-
#
|
| 19 |
-
#
|
| 20 |
-
#
|
| 21 |
-
# it is
|
| 22 |
-
#
|
| 23 |
-
#
|
| 24 |
-
#
|
| 25 |
-
#
|
| 26 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
#
|
| 28 |
# The intended capability is criticality-weighted triage: rank by
|
| 29 |
# (value x lethal-trajectory), not by raw damage percentage. The pbox
|
| 30 |
-
# LOOKS the most damaged; the proc IS the one that dies.
|
| 31 |
-
# proc (easy) / the proc AND the weap (medium, hard) and
|
|
|
|
| 32 |
#
|
| 33 |
# Bar:
|
| 34 |
# - stall (only observe): LOSS — the proc band is never bridged; the
|
| 35 |
-
# refinery is chewed to 0 inside ~
|
| 36 |
# building_count_gte:proc` fires.
|
| 37 |
# - repair-pbox-first (toggle repair on the most-damaged-LOOKING
|
| 38 |
# building, the pbox): LOSS — the pbox is saved but the pbox is not
|
| 39 |
-
# in the win predicate
|
| 40 |
-
# clause fires.
|
| 41 |
-
# -
|
| 42 |
-
#
|
| 43 |
-
#
|
| 44 |
-
#
|
| 45 |
-
#
|
|
|
|
|
|
|
|
|
|
| 46 |
#
|
| 47 |
# Anchor: disaster-recovery triage / criticality-weighted maintenance —
|
| 48 |
# the SRE incident-response playbook ("triage by criticality x blast
|
|
@@ -54,39 +62,46 @@
|
|
| 54 |
# ENGINE FACTS (CLAUDE.md):
|
| 55 |
# - `repair` toggles autorepair on a building ID; it heals ~1% of the
|
| 56 |
# building's max HP per tick and deducts a small proportional cash
|
| 57 |
-
# cost
|
| 58 |
-
#
|
| 59 |
-
#
|
| 60 |
-
#
|
| 61 |
-
# - `
|
| 62 |
-
#
|
| 63 |
-
#
|
| 64 |
-
#
|
| 65 |
-
# -
|
| 66 |
-
#
|
| 67 |
-
#
|
| 68 |
-
#
|
| 69 |
-
#
|
| 70 |
-
#
|
| 71 |
-
#
|
| 72 |
-
#
|
| 73 |
-
#
|
| 74 |
-
#
|
| 75 |
-
#
|
| 76 |
-
#
|
| 77 |
-
#
|
| 78 |
-
#
|
| 79 |
-
#
|
| 80 |
-
#
|
| 81 |
-
#
|
| 82 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
# - The unarmed high-HP enemy `fact` far east prevents the engine
|
| 84 |
# auto-`done` on enemy-elimination before the win/clock is evaluated.
|
| 85 |
-
# Because the loss/draw boundary must never depend on a race between
|
| 86 |
-
# the stance:3 defenders hunting that far `fact` and the deadline,
|
| 87 |
-
# the fail predicate ALSO trips on `not building_count_gte:{weap}` —
|
| 88 |
-
# the moment a required building falls the run is a clean immediate
|
| 89 |
-
# LOSS, never a draw.
|
| 90 |
# - hard: agent base + enemy bands BOTH declare `spawn_point` (per-owner
|
| 91 |
# filter, oramap.rs::expand_scenario_actors) so each seed instantiates
|
| 92 |
# exactly one base latitude with its matching bands — a memorised
|
|
@@ -99,16 +114,16 @@ meta:
|
|
| 99 |
real_world_meaning: >
|
| 100 |
Three of your structures are under simultaneous attrition and you
|
| 101 |
have one repair organ to commit. The damage picture is deliberately
|
| 102 |
-
misleading: the pillbox (pbox) is
|
| 103 |
-
most damaged, but it is low-value
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
it within
|
| 107 |
-
intended decision is criticality-weighted triage:
|
| 108 |
-
lethal-trajectory, not by raw damage percent. Repair
|
| 109 |
-
(and on the harder tiers the war factory too);
|
| 110 |
-
refinery to "fix the worst-looking building" loses it,
|
| 111 |
-
loses it.
|
| 112 |
robotics_analogue: >
|
| 113 |
SRE / disaster-recovery incident triage: several services are
|
| 114 |
degraded at once and the operator has one repair lever. The correct
|
|
@@ -130,9 +145,9 @@ starting_cash: 8000
|
|
| 130 |
base:
|
| 131 |
agent:
|
| 132 |
faction: allies
|
| 133 |
-
# Stance-only enemy (no bot_type) — each grenadier band
|
| 134 |
-
#
|
| 135 |
-
# three-way damage picture stays a clean, stable read.
|
| 136 |
enemy:
|
| 137 |
faction: soviet
|
| 138 |
tools:
|
|
@@ -140,8 +155,8 @@ base:
|
|
| 140 |
# The load-bearing tool: toggles autorepair on a building.
|
| 141 |
- repair
|
| 142 |
planning: true
|
| 143 |
-
# Re-decide the instant the attackers come into sight or a
|
| 144 |
-
#
|
| 145 |
# tick schedule.
|
| 146 |
interrupts:
|
| 147 |
enemy_unit_spotted: true
|
|
@@ -151,48 +166,50 @@ base:
|
|
| 151 |
actors: []
|
| 152 |
|
| 153 |
levels:
|
| 154 |
-
# ── EASY ── the bare triage skill. Three buildings
|
| 155 |
-
# proc —
|
| 156 |
-
#
|
| 157 |
-
#
|
| 158 |
-
#
|
| 159 |
-
#
|
| 160 |
-
#
|
| 161 |
-
#
|
| 162 |
-
#
|
| 163 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
easy:
|
| 165 |
description: >
|
| 166 |
-
Three of your structures are under attrition
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
the
|
|
|
|
|
|
|
| 175 |
repairing the most-damaged-looking building (the pillbox) both
|
| 176 |
lose the refinery.
|
| 177 |
overrides:
|
| 178 |
actors:
|
| 179 |
-
# proc — HIGH value, LETHAL trajectory.
|
| 180 |
-
# wood-armoured refinery
|
| 181 |
-
# clear the band in ~4. Repair must bridge the gap.
|
| 182 |
- {type: proc, owner: agent, position: [20, 8]}
|
| 183 |
-
- {type:
|
| 184 |
-
|
| 185 |
-
#
|
| 186 |
-
#
|
| 187 |
- {type: weap, owner: agent, position: [20, 20]}
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
#
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
- {type: pbox, owner: agent, position: [20, 32]}
|
| 194 |
-
- {type: e3, owner: agent, position: [26, 33], stance: 3, count: 5}
|
| 195 |
-
- {type: e2, owner: enemy, position: [20, 36], stance: 3, count: 5}
|
| 196 |
# Base context — service depot + construction yard tucked safely
|
| 197 |
# in the rear, away from every band.
|
| 198 |
- {type: fix, owner: agent, position: [44, 20]}
|
|
@@ -204,50 +221,48 @@ levels:
|
|
| 204 |
all_of:
|
| 205 |
- building_count_gte: {type: proc, n: 1}
|
| 206 |
- building_count_gte: {type: weap, n: 1}
|
| 207 |
-
-
|
| 208 |
- within_ticks: 2400
|
| 209 |
fail_condition:
|
| 210 |
any_of:
|
| 211 |
- after_ticks: 2401
|
| 212 |
- not: {building_count_gte: {type: proc, n: 1}}
|
| 213 |
- not: {building_count_gte: {type: weap, n: 1}}
|
| 214 |
-
max_turns:
|
| 215 |
|
| 216 |
-
# ── MEDIUM ── +1 controlled variable:
|
| 217 |
-
#
|
| 218 |
-
#
|
| 219 |
-
# 2 critical buildings (proc +
|
| 220 |
-
#
|
| 221 |
-
#
|
| 222 |
-
#
|
| 223 |
-
#
|
| 224 |
-
#
|
|
|
|
| 225 |
medium:
|
| 226 |
description: >
|
| 227 |
-
Three structures are under attrition
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
refinery AND the war factory are both on lethal trajectories:
|
| 232 |
-
dies within a few turns unless you toggle REPAIR on it. Win
|
| 233 |
-
the refinery and the war factory are both still standing
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
overrides:
|
| 239 |
actors:
|
| 240 |
- {type: proc, owner: agent, position: [20, 8]}
|
| 241 |
-
- {type:
|
| 242 |
-
|
| 243 |
-
#
|
| 244 |
-
# unrepaired; repair is mandatory on it too.
|
| 245 |
- {type: weap, owner: agent, position: [20, 20]}
|
| 246 |
-
- {type:
|
| 247 |
-
- {type:
|
| 248 |
-
- {type:
|
| 249 |
-
- {type: e3, owner: agent, position: [26, 33], stance: 3, count: 5}
|
| 250 |
-
- {type: e2, owner: enemy, position: [20, 36], stance: 3, count: 5}
|
| 251 |
- {type: fix, owner: agent, position: [44, 20]}
|
| 252 |
- {type: fact, owner: agent, position: [48, 20]}
|
| 253 |
- {type: fact, owner: enemy, position: [120, 20]}
|
|
@@ -255,70 +270,64 @@ levels:
|
|
| 255 |
all_of:
|
| 256 |
- building_count_gte: {type: proc, n: 1}
|
| 257 |
- building_count_gte: {type: weap, n: 1}
|
| 258 |
-
-
|
| 259 |
-
- within_ticks:
|
| 260 |
fail_condition:
|
| 261 |
any_of:
|
| 262 |
-
- after_ticks:
|
| 263 |
- not: {building_count_gte: {type: proc, n: 1}}
|
| 264 |
- not: {building_count_gte: {type: weap, n: 1}}
|
| 265 |
-
max_turns:
|
| 266 |
|
| 267 |
# ── HARD ── +1 controlled variable: TWO spawn_point groups round-
|
| 268 |
-
# robin the AGENT base
|
| 269 |
-
#
|
| 270 |
-
#
|
| 271 |
-
#
|
| 272 |
-
#
|
| 273 |
-
#
|
| 274 |
-
#
|
| 275 |
-
#
|
| 276 |
hard:
|
| 277 |
description: >
|
| 278 |
Three structures — the refinery (proc), the war factory (weap),
|
| 279 |
and the pillbox (pbox) — are under attrition from three grenadier
|
| 280 |
-
bands. Your base stages from a seed-chosen
|
| 281 |
-
memorised opening cannot generalise. The pillbox is
|
| 282 |
-
|
| 283 |
-
its own; the refinery and the war
|
| 284 |
-
trajectories and each needs REPAIR.
|
| 285 |
-
|
| 286 |
-
|
| 287 |
clock runs out with the win unmet, you lose. Stalling, repairing
|
| 288 |
only the pillbox, and repairing only one critical building all
|
| 289 |
lose.
|
| 290 |
overrides:
|
| 291 |
actors:
|
| 292 |
-
#
|
| 293 |
# under each spawn_point: ANY agent actor with a spawn_point
|
| 294 |
# filters OUT every ungrouped agent actor, and the matching
|
| 295 |
# enemy bands declare the same spawn_point so each seed gets a
|
| 296 |
# self-consistent instantiation.
|
| 297 |
- {type: proc, owner: agent, position: [20, 8], spawn_point: 0}
|
| 298 |
-
- {type:
|
| 299 |
-
- {type: e2, owner: enemy, position: [20, 4], stance: 3, count: 6, spawn_point: 0}
|
| 300 |
- {type: weap, owner: agent, position: [20, 20], spawn_point: 0}
|
| 301 |
-
- {type:
|
| 302 |
-
- {type:
|
| 303 |
-
- {type:
|
| 304 |
-
- {type: e3, owner: agent, position: [26, 33], stance: 3, count: 5, spawn_point: 0}
|
| 305 |
-
- {type: e2, owner: enemy, position: [20, 36], stance: 3, count: 5, spawn_point: 0}
|
| 306 |
- {type: fix, owner: agent, position: [44, 20], spawn_point: 0}
|
| 307 |
- {type: fact, owner: agent, position: [48, 20], spawn_point: 0}
|
| 308 |
- {type: fact, owner: enemy, position: [120, 20], spawn_point: 0}
|
| 309 |
-
#
|
| 310 |
# ward into the x=70.. column with the same y-geometry, so each
|
| 311 |
# seed faces a structurally identical triage from a different
|
| 312 |
# start. Bands and the auto-`done` fact mirror with it.
|
| 313 |
- {type: proc, owner: agent, position: [70, 8], spawn_point: 1}
|
| 314 |
-
- {type:
|
| 315 |
-
- {type: e2, owner: enemy, position: [70, 4], stance: 3, count: 6, spawn_point: 1}
|
| 316 |
- {type: weap, owner: agent, position: [70, 20], spawn_point: 1}
|
| 317 |
-
- {type:
|
| 318 |
-
- {type:
|
| 319 |
-
- {type:
|
| 320 |
-
- {type: e3, owner: agent, position: [76, 33], stance: 3, count: 5, spawn_point: 1}
|
| 321 |
-
- {type: e2, owner: enemy, position: [70, 36], stance: 3, count: 5, spawn_point: 1}
|
| 322 |
- {type: fix, owner: agent, position: [94, 20], spawn_point: 1}
|
| 323 |
- {type: fact, owner: agent, position: [98, 20], spawn_point: 1}
|
| 324 |
- {type: fact, owner: enemy, position: [120, 20], spawn_point: 1}
|
|
@@ -326,11 +335,11 @@ levels:
|
|
| 326 |
all_of:
|
| 327 |
- building_count_gte: {type: proc, n: 1}
|
| 328 |
- building_count_gte: {type: weap, n: 1}
|
| 329 |
-
-
|
| 330 |
-
- within_ticks:
|
| 331 |
fail_condition:
|
| 332 |
any_of:
|
| 333 |
-
- after_ticks:
|
| 334 |
- not: {building_count_gte: {type: proc, n: 1}}
|
| 335 |
- not: {building_count_gte: {type: weap, n: 1}}
|
| 336 |
-
max_turns:
|
|
|
|
| 1 |
# build-repair-priority-under-fire — criticality-weighted repair triage:
|
| 2 |
# three damaged buildings under fire, repair the high-value-and-lethally-
|
| 3 |
+
# threatened one(s) FIRST.
|
| 4 |
#
|
| 5 |
# REASONING capability — Wave-10 "disaster-recovery triage / criticality-
|
| 6 |
# weighted maintenance" pack. Three of the agent's structures are under
|
| 7 |
# simultaneous attrition from three separate grenadier bands, and the
|
| 8 |
+
# agent's hold-the-line job is a pure TRIAGE DECISION: which building(s)
|
| 9 |
+
# to put the `repair` organ on. The three buildings present a
|
| 10 |
# deliberately mis-leading damage picture:
|
| 11 |
#
|
| 12 |
+
# - proc (refinery) — HIGH value, on a LETHAL trajectory. A
|
| 13 |
+
# grenadier band chews the wood-armoured refinery down and KILLS it
|
| 14 |
+
# unrepaired; toggling `repair` on it makes the ~1%/tick autorepair
|
| 15 |
+
# out-pace the grenade chip so it rides out the band. proc is
|
| 16 |
+
# required by the win predicate; losing it is an instant fail.
|
| 17 |
+
# - pbox (pillbox) — LOW value, the DECOY. It is pre-placed already
|
| 18 |
+
# damaged (health: 30 — chipped to ~30% HP), so a naive "repair the
|
| 19 |
+
# most-damaged-LOOKING building" heuristic targets the pbox. But the
|
| 20 |
+
# pbox is Heavy-armoured: the grenade Versus-Heavy multiplier is so
|
| 21 |
+
# low the band barely scratches it, and it is NOT in the win
|
| 22 |
+
# predicate — it survives untouched at ~30% on its own. Repairing it
|
| 23 |
+
# is wasted effort that leaves the proc to die.
|
| 24 |
+
# - weap (war factory) — MEDIUM. On easy NO band sits on the war
|
| 25 |
+
# factory directly: as long as the proc is bridged its band stays
|
| 26 |
+
# pinned and the war factory is never touched, so repairing the proc
|
| 27 |
+
# alone saves both. On medium / hard a light band ALSO chews the
|
| 28 |
+
# wood-armoured war factory, so it too must be repaired — deepening
|
| 29 |
+
# the triage from "pick the 1 critical building" to "pick the 2
|
| 30 |
+
# critical buildings, skip the low-value decoy".
|
| 31 |
#
|
| 32 |
# The intended capability is criticality-weighted triage: rank by
|
| 33 |
# (value x lethal-trajectory), not by raw damage percentage. The pbox
|
| 34 |
+
# LOOKS the most damaged; the proc (+ weap) IS the one that dies.
|
| 35 |
+
# Repair the proc (easy) / the proc AND the weap (medium, hard) and
|
| 36 |
+
# skip the pbox.
|
| 37 |
#
|
| 38 |
# Bar:
|
| 39 |
# - stall (only observe): LOSS — the proc band is never bridged; the
|
| 40 |
+
# refinery is chewed to 0 inside ~3 turns; fail clause `not
|
| 41 |
# building_count_gte:proc` fires.
|
| 42 |
# - repair-pbox-first (toggle repair on the most-damaged-LOOKING
|
| 43 |
# building, the pbox): LOSS — the pbox is saved but the pbox is not
|
| 44 |
+
# in the win predicate and survives on its own anyway; the proc gets
|
| 45 |
+
# no repair and dies; same fail clause fires.
|
| 46 |
+
# - repair-proc-only: WIN on easy (no band sits on the war factory,
|
| 47 |
+
# so a bridged proc keeps its band pinned and the war factory is
|
| 48 |
+
# never threatened) but LOSS on medium / hard (a light band ALSO
|
| 49 |
+
# chews the war factory; `not building_count_gte:weap` fires).
|
| 50 |
+
# - INTENDED triage (repair proc — easy; proc + weap — medium /
|
| 51 |
+
# hard): WIN — the autorepair heal rate out-paces the grenade chip
|
| 52 |
+
# on the bridged building(s); proc (+ weap) are still standing when
|
| 53 |
+
# the survival window opens and the win latches.
|
| 54 |
#
|
| 55 |
# Anchor: disaster-recovery triage / criticality-weighted maintenance —
|
| 56 |
# the SRE incident-response playbook ("triage by criticality x blast
|
|
|
|
| 62 |
# ENGINE FACTS (CLAUDE.md):
|
| 63 |
# - `repair` toggles autorepair on a building ID; it heals ~1% of the
|
| 64 |
# building's max HP per tick and deducts a small proportional cash
|
| 65 |
+
# cost. Repair auto-stops when the building is full or the owner runs
|
| 66 |
+
# out of cash. Buildings surface in the agent's `own_buildings`
|
| 67 |
+
# observation, each with a real engine `id` the agent targets
|
| 68 |
+
# directly.
|
| 69 |
+
# - `health:` on a pre-placed actor IS honoured (oramap.rs parser
|
| 70 |
+
# carries the HP percentage through to the spawned actor's Health
|
| 71 |
+
# trait). The pbox is pre-placed at `health: 30` so it LOOKS the
|
| 72 |
+
# worst-damaged building from turn 1 — the decoy bait.
|
| 73 |
+
# - Grenadier (e2) carries the Grenade weapon: Versus Wood ~full,
|
| 74 |
+
# Versus Heavy ~quarter. proc and weap are Wood-armoured (full chip
|
| 75 |
+
# — lethal unrepaired); pbox is Heavy-armoured (quarter chip — the
|
| 76 |
+
# band barely scratches it, so a pre-damaged pbox survives on its own
|
| 77 |
+
# at ~30%).
|
| 78 |
+
# - Enemy bands are stance:3 (AttackAnything) with NO bot_type — each
|
| 79 |
+
# band engages the building cluster on its own latitude. After the
|
| 80 |
+
# move-fire engine fix a stance:3 attacker whose target building has
|
| 81 |
+
# been destroyed advances on the next structure: this is what
|
| 82 |
+
# couples the proc band to the war factory on easy — bridge the proc
|
| 83 |
+
# and its band stays pinned; let the proc die and its band migrates
|
| 84 |
+
# south and finishes the war factory too.
|
| 85 |
+
# - This pack has NO agent combat units and NO kill bar: the engine
|
| 86 |
+
# movement fixes made pre-placed defenders resolve a clustered
|
| 87 |
+
# fire-fight in a single ~90-tick window with the buildings never
|
| 88 |
+
# scratched, collapsing the damage race. The triage is now a pure
|
| 89 |
+
# `repair`-target decision against an unopposed band — repair is the
|
| 90 |
+
# ONLY lever and is genuinely load-bearing.
|
| 91 |
+
# - The win uses an `after_ticks` floor (the building must SURVIVE the
|
| 92 |
+
# danger window, not merely be alive on turn 1) AND a `within_ticks`
|
| 93 |
+
# ceiling. This win is building-SURVIVAL, not ConquestVictory — there
|
| 94 |
+
# is no enemy MustBeDestroyed objective — so `after_ticks` in the win
|
| 95 |
+
# clause is safe (no auto-`done` race). The unrepaired buildings die
|
| 96 |
+
# well before the `after_ticks` floor; the correctly-repaired ones
|
| 97 |
+
# ride the band out past it. Reachable tick approx 93 + 90*(max_turns
|
| 98 |
+
# -1); every after_ticks / within_ticks below sits inside reachable
|
| 99 |
+
# for its tier, so the deadline always bites (no draw degeneracy).
|
| 100 |
+
# - The fail predicate trips the moment a required building falls
|
| 101 |
+
# (`not building_count_gte:{proc}` / `{weap}`) — a clean immediate
|
| 102 |
+
# LOSS, never a draw, well before the survival window opens.
|
| 103 |
# - The unarmed high-HP enemy `fact` far east prevents the engine
|
| 104 |
# auto-`done` on enemy-elimination before the win/clock is evaluated.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
# - hard: agent base + enemy bands BOTH declare `spawn_point` (per-owner
|
| 106 |
# filter, oramap.rs::expand_scenario_actors) so each seed instantiates
|
| 107 |
# exactly one base latitude with its matching bands — a memorised
|
|
|
|
| 114 |
real_world_meaning: >
|
| 115 |
Three of your structures are under simultaneous attrition and you
|
| 116 |
have one repair organ to commit. The damage picture is deliberately
|
| 117 |
+
misleading: the pillbox (pbox) is pre-damaged to ~30% HP and LOOKS
|
| 118 |
+
the most damaged, but it is low-value, heavily armoured, and the
|
| 119 |
+
grenadiers barely scratch it — it survives on its own. The refinery
|
| 120 |
+
(proc) starts intact but is on a LETHAL trajectory: its grenadier
|
| 121 |
+
band kills it within a few turns unless you toggle `repair` on it
|
| 122 |
+
immediately. The intended decision is criticality-weighted triage:
|
| 123 |
+
rank by value x lethal-trajectory, not by raw damage percent. Repair
|
| 124 |
+
the proc first (and on the harder tiers the war factory too);
|
| 125 |
+
skipping the refinery to "fix the worst-looking building" loses it,
|
| 126 |
+
and stalling loses it.
|
| 127 |
robotics_analogue: >
|
| 128 |
SRE / disaster-recovery incident triage: several services are
|
| 129 |
degraded at once and the operator has one repair lever. The correct
|
|
|
|
| 145 |
base:
|
| 146 |
agent:
|
| 147 |
faction: allies
|
| 148 |
+
# Stance-only enemy (no bot_type) — each grenadier band engages the
|
| 149 |
+
# building cluster on its own latitude rather than charging a shared
|
| 150 |
+
# centroid, so the three-way damage picture stays a clean, stable read.
|
| 151 |
enemy:
|
| 152 |
faction: soviet
|
| 153 |
tools:
|
|
|
|
| 155 |
# The load-bearing tool: toggles autorepair on a building.
|
| 156 |
- repair
|
| 157 |
planning: true
|
| 158 |
+
# Re-decide the instant the attackers come into sight or a building
|
| 159 |
+
# is lost — repair triage is reactive, decided turn 1, not on a fixed
|
| 160 |
# tick schedule.
|
| 161 |
interrupts:
|
| 162 |
enemy_unit_spotted: true
|
|
|
|
| 166 |
actors: []
|
| 167 |
|
| 168 |
levels:
|
| 169 |
+
# ── EASY ── the bare triage skill. Three buildings:
|
| 170 |
+
# proc — 3 grenadiers chew the wood-armoured refinery; unrepaired
|
| 171 |
+
# it dies ~tick 288. Autorepair out-paces the chip and it
|
| 172 |
+
# rides the band out.
|
| 173 |
+
# weap — NO band sits directly on the war factory. As long as the
|
| 174 |
+
# proc is bridged the proc band stays pinned; if the proc
|
| 175 |
+
# dies its band migrates south and finishes the war factory.
|
| 176 |
+
# So on easy repairing the proc ALONE saves both.
|
| 177 |
+
# pbox — pre-placed at health 30 (the most-damaged-LOOKING
|
| 178 |
+
# building); Heavy-armoured, a light band barely scratches
|
| 179 |
+
# it; it survives on its own. NOT in the win predicate.
|
| 180 |
+
# Intended play: repair the proc. Win window [after 1200, within
|
| 181 |
+
# 2400]; max_turns 28 → reachable tick 93 + 90·27 = 2523, so the
|
| 182 |
+
# after_ticks 2401 fail bites.
|
| 183 |
easy:
|
| 184 |
description: >
|
| 185 |
+
Three of your structures are under attrition: the refinery
|
| 186 |
+
(proc), the war factory (weap), and the pillbox (pbox). The
|
| 187 |
+
pillbox is already damaged to about 30% HP and looks the most
|
| 188 |
+
damaged — but it is low value, heavily armoured, and the
|
| 189 |
+
grenadiers barely scratch it; it survives on its own. The
|
| 190 |
+
refinery is on a lethal trajectory: it dies within a few turns
|
| 191 |
+
unless you toggle REPAIR on it now. Repairing the refinery also
|
| 192 |
+
keeps its attackers pinned, so the war factory stays safe. Win
|
| 193 |
+
when the refinery and the war factory are both still standing
|
| 194 |
+
after tick 1200 and before tick 2400. If the refinery falls, or
|
| 195 |
+
the clock runs out with the win unmet, you lose. Stalling and
|
| 196 |
repairing the most-damaged-looking building (the pillbox) both
|
| 197 |
lose the refinery.
|
| 198 |
overrides:
|
| 199 |
actors:
|
| 200 |
+
# proc — HIGH value, LETHAL trajectory. 3 grenadiers kill the
|
| 201 |
+
# wood-armoured refinery unrepaired (~tick 288). Repair bridges.
|
|
|
|
| 202 |
- {type: proc, owner: agent, position: [20, 8]}
|
| 203 |
+
- {type: e2, owner: enemy, position: [20, 4], stance: 3, count: 3}
|
| 204 |
+
# weap — MEDIUM. NO band on the war factory directly: a bridged
|
| 205 |
+
# proc keeps its band pinned and the war factory is never
|
| 206 |
+
# threatened, so repair-proc-only saves both on easy.
|
| 207 |
- {type: weap, owner: agent, position: [20, 20]}
|
| 208 |
+
# pbox — LOW value DECOY. Pre-placed at health 30 (looks worst);
|
| 209 |
+
# Heavy-armoured, a light band barely scratches it; it survives
|
| 210 |
+
# on its own. Not in the win predicate.
|
| 211 |
+
- {type: pbox, owner: agent, position: [20, 32], health: 30}
|
| 212 |
+
- {type: e2, owner: enemy, position: [20, 36], stance: 3, count: 4}
|
|
|
|
|
|
|
|
|
|
| 213 |
# Base context — service depot + construction yard tucked safely
|
| 214 |
# in the rear, away from every band.
|
| 215 |
- {type: fix, owner: agent, position: [44, 20]}
|
|
|
|
| 221 |
all_of:
|
| 222 |
- building_count_gte: {type: proc, n: 1}
|
| 223 |
- building_count_gte: {type: weap, n: 1}
|
| 224 |
+
- after_ticks: 1200
|
| 225 |
- within_ticks: 2400
|
| 226 |
fail_condition:
|
| 227 |
any_of:
|
| 228 |
- after_ticks: 2401
|
| 229 |
- not: {building_count_gte: {type: proc, n: 1}}
|
| 230 |
- not: {building_count_gte: {type: weap, n: 1}}
|
| 231 |
+
max_turns: 28
|
| 232 |
|
| 233 |
+
# ── MEDIUM ── +1 controlled variable: a light grenadier band (2)
|
| 234 |
+
# now ALSO sits on the war factory, so the weap dies unrepaired even
|
| 235 |
+
# while the proc is bridged. The triage deepens from "repair the 1
|
| 236 |
+
# critical building" to "repair the 2 critical buildings (proc +
|
| 237 |
+
# weap), skip the low-value decoy (pbox)". A "repair proc only" play
|
| 238 |
+
# keeps the proc but loses the weap (~tick 1548) → `not
|
| 239 |
+
# building_count_gte:{weap}` fail fires as a clean LOSS before the
|
| 240 |
+
# survival window opens. Win window [after 1700, within 2700];
|
| 241 |
+
# max_turns 31 → reachable tick 93 + 90·30 = 2793, so after_ticks
|
| 242 |
+
# 2701 bites.
|
| 243 |
medium:
|
| 244 |
description: >
|
| 245 |
+
Three structures are under attrition: the refinery (proc), the
|
| 246 |
+
war factory (weap), and the pillbox (pbox). The pillbox is
|
| 247 |
+
already damaged to about 30% HP and looks the most damaged — but
|
| 248 |
+
it is low value, heavily armoured, and survives on its own. The
|
| 249 |
+
refinery AND the war factory are both on lethal trajectories:
|
| 250 |
+
each dies within a few turns unless you toggle REPAIR on it. Win
|
| 251 |
+
when the refinery and the war factory are both still standing
|
| 252 |
+
after tick 1700 and before tick 2700. If either falls, or the
|
| 253 |
+
clock runs out with the win unmet, you lose. Stalling, repairing
|
| 254 |
+
only the pillbox, and repairing only one of the two critical
|
| 255 |
+
buildings all lose.
|
| 256 |
overrides:
|
| 257 |
actors:
|
| 258 |
- {type: proc, owner: agent, position: [20, 8]}
|
| 259 |
+
- {type: e2, owner: enemy, position: [20, 4], stance: 3, count: 3}
|
| 260 |
+
# weap now has a light band of its own (2 grenadiers) — the war
|
| 261 |
+
# factory dies unrepaired; repair is mandatory on it too.
|
|
|
|
| 262 |
- {type: weap, owner: agent, position: [20, 20]}
|
| 263 |
+
- {type: e2, owner: enemy, position: [20, 16], stance: 3, count: 2}
|
| 264 |
+
- {type: pbox, owner: agent, position: [20, 32], health: 30}
|
| 265 |
+
- {type: e2, owner: enemy, position: [20, 36], stance: 3, count: 4}
|
|
|
|
|
|
|
| 266 |
- {type: fix, owner: agent, position: [44, 20]}
|
| 267 |
- {type: fact, owner: agent, position: [48, 20]}
|
| 268 |
- {type: fact, owner: enemy, position: [120, 20]}
|
|
|
|
| 270 |
all_of:
|
| 271 |
- building_count_gte: {type: proc, n: 1}
|
| 272 |
- building_count_gte: {type: weap, n: 1}
|
| 273 |
+
- after_ticks: 1700
|
| 274 |
+
- within_ticks: 2700
|
| 275 |
fail_condition:
|
| 276 |
any_of:
|
| 277 |
+
- after_ticks: 2701
|
| 278 |
- not: {building_count_gte: {type: proc, n: 1}}
|
| 279 |
- not: {building_count_gte: {type: weap, n: 1}}
|
| 280 |
+
max_turns: 31
|
| 281 |
|
| 282 |
# ── HARD ── +1 controlled variable: TWO spawn_point groups round-
|
| 283 |
+
# robin the AGENT base longitude (WEST x=20 cluster / EAST x=70
|
| 284 |
+
# mirror) per seed, so a single memorised opening cannot generalise.
|
| 285 |
+
# Agent base AND the matching enemy bands BOTH declare `spawn_point`
|
| 286 |
+
# — the per-owner filter (oramap.rs::expand_scenario_actors)
|
| 287 |
+
# instantiates exactly one self-consistent base + bands per seed; the
|
| 288 |
+
# persistent every-seed enemy auto-`done` fact is duplicated across
|
| 289 |
+
# both groups. Same composition and clock as medium (proc + weap
|
| 290 |
+
# both lethal; pbox decoy).
|
| 291 |
hard:
|
| 292 |
description: >
|
| 293 |
Three structures — the refinery (proc), the war factory (weap),
|
| 294 |
and the pillbox (pbox) — are under attrition from three grenadier
|
| 295 |
+
bands. Your base stages from a seed-chosen longitude, so a single
|
| 296 |
+
memorised opening cannot generalise. The pillbox is pre-damaged
|
| 297 |
+
to ~30% HP and looks the most damaged but is low value, heavily
|
| 298 |
+
armoured, and survives on its own; the refinery and the war
|
| 299 |
+
factory are both on lethal trajectories and each needs REPAIR.
|
| 300 |
+
Win when the refinery and the war factory are both still standing
|
| 301 |
+
after tick 1700 and before tick 2700. If either falls, or the
|
| 302 |
clock runs out with the win unmet, you lose. Stalling, repairing
|
| 303 |
only the pillbox, and repairing only one critical building all
|
| 304 |
lose.
|
| 305 |
overrides:
|
| 306 |
actors:
|
| 307 |
+
# WEST base group (spawn_point 0). Full base + bands duplicated
|
| 308 |
# under each spawn_point: ANY agent actor with a spawn_point
|
| 309 |
# filters OUT every ungrouped agent actor, and the matching
|
| 310 |
# enemy bands declare the same spawn_point so each seed gets a
|
| 311 |
# self-consistent instantiation.
|
| 312 |
- {type: proc, owner: agent, position: [20, 8], spawn_point: 0}
|
| 313 |
+
- {type: e2, owner: enemy, position: [20, 4], stance: 3, count: 3, spawn_point: 0}
|
|
|
|
| 314 |
- {type: weap, owner: agent, position: [20, 20], spawn_point: 0}
|
| 315 |
+
- {type: e2, owner: enemy, position: [20, 16], stance: 3, count: 2, spawn_point: 0}
|
| 316 |
+
- {type: pbox, owner: agent, position: [20, 32], health: 30, spawn_point: 0}
|
| 317 |
+
- {type: e2, owner: enemy, position: [20, 36], stance: 3, count: 4, spawn_point: 0}
|
|
|
|
|
|
|
| 318 |
- {type: fix, owner: agent, position: [44, 20], spawn_point: 0}
|
| 319 |
- {type: fact, owner: agent, position: [48, 20], spawn_point: 0}
|
| 320 |
- {type: fact, owner: enemy, position: [120, 20], spawn_point: 0}
|
| 321 |
+
# EAST base group (spawn_point 1) — the base mirrored east-
|
| 322 |
# ward into the x=70.. column with the same y-geometry, so each
|
| 323 |
# seed faces a structurally identical triage from a different
|
| 324 |
# start. Bands and the auto-`done` fact mirror with it.
|
| 325 |
- {type: proc, owner: agent, position: [70, 8], spawn_point: 1}
|
| 326 |
+
- {type: e2, owner: enemy, position: [70, 4], stance: 3, count: 3, spawn_point: 1}
|
|
|
|
| 327 |
- {type: weap, owner: agent, position: [70, 20], spawn_point: 1}
|
| 328 |
+
- {type: e2, owner: enemy, position: [70, 16], stance: 3, count: 2, spawn_point: 1}
|
| 329 |
+
- {type: pbox, owner: agent, position: [70, 32], health: 30, spawn_point: 1}
|
| 330 |
+
- {type: e2, owner: enemy, position: [70, 36], stance: 3, count: 4, spawn_point: 1}
|
|
|
|
|
|
|
| 331 |
- {type: fix, owner: agent, position: [94, 20], spawn_point: 1}
|
| 332 |
- {type: fact, owner: agent, position: [98, 20], spawn_point: 1}
|
| 333 |
- {type: fact, owner: enemy, position: [120, 20], spawn_point: 1}
|
|
|
|
| 335 |
all_of:
|
| 336 |
- building_count_gte: {type: proc, n: 1}
|
| 337 |
- building_count_gte: {type: weap, n: 1}
|
| 338 |
+
- after_ticks: 1700
|
| 339 |
+
- within_ticks: 2700
|
| 340 |
fail_condition:
|
| 341 |
any_of:
|
| 342 |
+
- after_ticks: 2701
|
| 343 |
- not: {building_count_gte: {type: proc, n: 1}}
|
| 344 |
- not: {building_count_gte: {type: weap, n: 1}}
|
| 345 |
+
max_turns: 31
|
|
@@ -2,35 +2,46 @@
|
|
| 2 |
(Wave-10 REASONING: criticality-weighted repair triage).
|
| 3 |
|
| 4 |
Three of the agent's structures are under simultaneous attrition from
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
|
| 9 |
* proc (refinery) — HIGH value, on a LETHAL trajectory: dies inside
|
| 10 |
-
~
|
| 11 |
-
* pbox (pillbox) — LOW value DECOY:
|
| 12 |
-
LOOKS the most damaged, but it is
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
* REPAIR-PBOX-FIRST (toggle repair on the most-damaged-LOOKING
|
| 23 |
-
building, the pbox)
|
| 24 |
-
|
| 25 |
-
* REPAIR-PROC-ONLY
|
| 26 |
-
band
|
| 27 |
-
the
|
|
|
|
| 28 |
* INTENDED triage (repair proc — easy; proc + weap — medium / hard)
|
| 29 |
-
|
| 30 |
-
|
| 31 |
|
| 32 |
-
Repair-everything also wins (it repairs the proc) — that is
|
| 33 |
-
bar: the failure mode this pack discriminates is "repair the
|
| 34 |
worst-LOOKING building", not "be thorough".
|
| 35 |
"""
|
| 36 |
|
|
@@ -119,6 +130,39 @@ def test_benchmark_anchor_lists_triage():
|
|
| 119 |
assert "triage" in blob or "maintenance" in blob, anchors
|
| 120 |
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
@pytest.mark.parametrize("level", LEVELS)
|
| 123 |
def test_every_level_has_a_reachable_timeout_fail(level):
|
| 124 |
"""Non-win must be a real LOSS, never a DRAW: the `after_ticks`
|
|
@@ -139,6 +183,28 @@ def test_every_level_has_a_reachable_timeout_fail(level):
|
|
| 139 |
)
|
| 140 |
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
@pytest.mark.parametrize("level", LEVELS)
|
| 143 |
def test_fail_trips_on_either_required_building(level):
|
| 144 |
"""Losing the proc OR the weap must be an immediate clean LOSS — no
|
|
@@ -195,7 +261,7 @@ def test_intended_triage_wins(level, seed):
|
|
| 195 |
@pytest.mark.parametrize("seed", SEEDS)
|
| 196 |
def test_repair_everything_wins(level, seed):
|
| 197 |
"""Repairing every damaged building also wins — it DOES repair the
|
| 198 |
-
proc. Thoroughness is not the failure mode under test."""
|
| 199 |
c, r = _run(level, _repair_all, seed=seed)
|
| 200 |
assert r.outcome == "win", (
|
| 201 |
f"{level} seed{seed}: repair-all should WIN, got {r.outcome}"
|
|
@@ -209,7 +275,7 @@ def test_repair_everything_wins(level, seed):
|
|
| 209 |
@pytest.mark.parametrize("seed", SEEDS)
|
| 210 |
def test_stall_loses(level, seed):
|
| 211 |
"""Stall must LOSE — the proc band is never bridged; the refinery
|
| 212 |
-
dies ~turn
|
| 213 |
c, r = _run(level, _stall, seed=seed)
|
| 214 |
assert r.outcome == "loss", (
|
| 215 |
f"{level} seed{seed} stall must LOSE; got {r.outcome} "
|
|
@@ -231,8 +297,8 @@ def test_repair_pbox_first_loses(level, seed):
|
|
| 231 |
|
| 232 |
@pytest.mark.parametrize("seed", SEEDS)
|
| 233 |
def test_repair_proc_only_wins_easy(seed):
|
| 234 |
-
"""On easy the war factory
|
| 235 |
-
repairing only the proc is sufficient → WIN."""
|
| 236 |
c, r = _run("easy", _repair_proc, seed=seed)
|
| 237 |
assert r.outcome == "win", (
|
| 238 |
f"easy seed{seed} repair-proc-only should WIN, got {r.outcome}"
|
|
@@ -242,9 +308,9 @@ def test_repair_proc_only_wins_easy(seed):
|
|
| 242 |
@pytest.mark.parametrize("level", ("medium", "hard"))
|
| 243 |
@pytest.mark.parametrize("seed", SEEDS)
|
| 244 |
def test_repair_proc_only_loses_on_medium_and_hard(level, seed):
|
| 245 |
-
"""On medium / hard
|
| 246 |
-
|
| 247 |
-
|
| 248 |
c, r = _run(level, _repair_proc, seed=seed)
|
| 249 |
assert r.outcome == "loss", (
|
| 250 |
f"{level} seed{seed} repair-proc-only must LOSE; got {r.outcome} "
|
|
|
|
| 2 |
(Wave-10 REASONING: criticality-weighted repair triage).
|
| 3 |
|
| 4 |
Three of the agent's structures are under simultaneous attrition from
|
| 5 |
+
separate grenadier bands. The damage picture is deliberately misleading
|
| 6 |
+
and the agent's only job is the TRIAGE DECISION — which building(s) to
|
| 7 |
+
put the `repair` organ on:
|
| 8 |
|
| 9 |
* proc (refinery) — HIGH value, on a LETHAL trajectory: dies inside
|
| 10 |
+
~3 turns unrepaired; required by the win predicate.
|
| 11 |
+
* pbox (pillbox) — LOW value DECOY: pre-placed at health 30 so it
|
| 12 |
+
LOOKS the most damaged, but it is Heavy-armoured (a light grenadier
|
| 13 |
+
band barely scratches it), is not in the win predicate, and
|
| 14 |
+
survives on its own. Repairing it is wasted effort.
|
| 15 |
+
* weap (war factory) — MEDIUM: on easy NO band sits on the war
|
| 16 |
+
factory directly, so a bridged proc keeps its band pinned and the
|
| 17 |
+
war factory rides out untouched; on medium / hard a light band ALSO
|
| 18 |
+
chews the war factory so it too needs repair.
|
| 19 |
+
|
| 20 |
+
The pack carries NO agent combat units and NO kill bar — the engine
|
| 21 |
+
movement fixes made pre-placed defenders resolve a clustered fire-fight
|
| 22 |
+
in one ~90-tick window with the buildings never scratched, collapsing
|
| 23 |
+
the old damage race. The triage is now a pure `repair`-target decision
|
| 24 |
+
against an unopposed band. The win requires the buildings to SURVIVE a
|
| 25 |
+
danger window (`after_ticks` floor + `within_ticks` ceiling), not
|
| 26 |
+
merely be alive on turn 1.
|
| 27 |
+
|
| 28 |
+
No-cheat bar (proven below for every level x seed 1-4):
|
| 29 |
+
|
| 30 |
+
* STALL (only `observe`) -> proc never bridged -> refinery dies ~turn
|
| 31 |
+
4 -> `not building_count_gte:{proc}` fail fires -> LOSS.
|
| 32 |
* REPAIR-PBOX-FIRST (toggle repair on the most-damaged-LOOKING
|
| 33 |
+
building, the pbox) -> pbox saved (it survives on its own anyway);
|
| 34 |
+
the proc gets no repair and dies -> LOSS.
|
| 35 |
+
* REPAIR-PROC-ONLY -> WIN on easy (no band sits on the war factory,
|
| 36 |
+
so a bridged proc keeps its band pinned) but LOSS on medium / hard
|
| 37 |
+
(the light war-factory band kills the unrepaired weap ->
|
| 38 |
+
`not building_count_gte:{weap}` fail).
|
| 39 |
* INTENDED triage (repair proc — easy; proc + weap — medium / hard)
|
| 40 |
+
-> autorepair out-paces the grenade chip, the buildings ride the
|
| 41 |
+
bands out, the survival window opens -> WIN.
|
| 42 |
|
| 43 |
+
Repair-everything also wins (it repairs the proc + weap) — that is
|
| 44 |
+
within the bar: the failure mode this pack discriminates is "repair the
|
| 45 |
worst-LOOKING building", not "be thorough".
|
| 46 |
"""
|
| 47 |
|
|
|
|
| 130 |
assert "triage" in blob or "maintenance" in blob, anchors
|
| 131 |
|
| 132 |
|
| 133 |
+
def test_pbox_decoy_is_pre_damaged_every_level():
|
| 134 |
+
"""The pbox decoy is pre-placed already damaged (health: 30) so it
|
| 135 |
+
LOOKS the most-damaged building — the bait for the wrong triage."""
|
| 136 |
+
pack = load_pack(PACK)
|
| 137 |
+
for level in LEVELS:
|
| 138 |
+
c = compile_level(pack, level)
|
| 139 |
+
pboxes = [
|
| 140 |
+
a for a in c.scenario.actors
|
| 141 |
+
if a.owner == "agent" and a.type == "pbox"
|
| 142 |
+
]
|
| 143 |
+
assert pboxes, f"{level}: no pbox decoy"
|
| 144 |
+
for a in pboxes:
|
| 145 |
+
assert a.health == 30, (
|
| 146 |
+
f"{level}: pbox decoy must be pre-damaged (health 30), "
|
| 147 |
+
f"got {a.health}"
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def test_no_agent_combat_units():
|
| 152 |
+
"""The pack carries NO agent combat units — the triage is a pure
|
| 153 |
+
`repair`-target decision against an unopposed band."""
|
| 154 |
+
pack = load_pack(PACK)
|
| 155 |
+
buildings = {"proc", "weap", "pbox", "fix", "fact"}
|
| 156 |
+
for level in LEVELS:
|
| 157 |
+
c = compile_level(pack, level)
|
| 158 |
+
agent_types = {
|
| 159 |
+
a.type for a in c.scenario.actors if a.owner == "agent"
|
| 160 |
+
}
|
| 161 |
+
assert agent_types <= buildings, (
|
| 162 |
+
f"{level}: agent must own only buildings, got {agent_types}"
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
@pytest.mark.parametrize("level", LEVELS)
|
| 167 |
def test_every_level_has_a_reachable_timeout_fail(level):
|
| 168 |
"""Non-win must be a real LOSS, never a DRAW: the `after_ticks`
|
|
|
|
| 183 |
)
|
| 184 |
|
| 185 |
|
| 186 |
+
@pytest.mark.parametrize("level", LEVELS)
|
| 187 |
+
def test_win_has_survival_window(level):
|
| 188 |
+
"""The win must require the buildings to SURVIVE a danger window —
|
| 189 |
+
an `after_ticks` floor below the `within_ticks` ceiling — so the win
|
| 190 |
+
cannot latch on turn 1 before any damage lands."""
|
| 191 |
+
c = compile_level(load_pack(PACK), level)
|
| 192 |
+
all_of = c.win_condition.model_dump().get("all_of", [])
|
| 193 |
+
floor = next(int(x["after_ticks"]) for x in all_of if "after_ticks" in x)
|
| 194 |
+
ceiling = next(
|
| 195 |
+
int(x["within_ticks"]) for x in all_of if "within_ticks" in x
|
| 196 |
+
)
|
| 197 |
+
assert 0 < floor < ceiling, (
|
| 198 |
+
f"{level}: win needs a survival window 0 < after_ticks "
|
| 199 |
+
f"({floor}) < within_ticks ({ceiling})"
|
| 200 |
+
)
|
| 201 |
+
reachable = 93 + 90 * (c.max_turns - 1)
|
| 202 |
+
assert floor < reachable, (
|
| 203 |
+
f"{level}: win after_ticks {floor} unreachable within "
|
| 204 |
+
f"{c.max_turns} turns (max tick {reachable})"
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
@pytest.mark.parametrize("level", LEVELS)
|
| 209 |
def test_fail_trips_on_either_required_building(level):
|
| 210 |
"""Losing the proc OR the weap must be an immediate clean LOSS — no
|
|
|
|
| 261 |
@pytest.mark.parametrize("seed", SEEDS)
|
| 262 |
def test_repair_everything_wins(level, seed):
|
| 263 |
"""Repairing every damaged building also wins — it DOES repair the
|
| 264 |
+
proc + weap. Thoroughness is not the failure mode under test."""
|
| 265 |
c, r = _run(level, _repair_all, seed=seed)
|
| 266 |
assert r.outcome == "win", (
|
| 267 |
f"{level} seed{seed}: repair-all should WIN, got {r.outcome}"
|
|
|
|
| 275 |
@pytest.mark.parametrize("seed", SEEDS)
|
| 276 |
def test_stall_loses(level, seed):
|
| 277 |
"""Stall must LOSE — the proc band is never bridged; the refinery
|
| 278 |
+
dies ~turn 4 and the `not building_count_gte:{proc}` fail fires."""
|
| 279 |
c, r = _run(level, _stall, seed=seed)
|
| 280 |
assert r.outcome == "loss", (
|
| 281 |
f"{level} seed{seed} stall must LOSE; got {r.outcome} "
|
|
|
|
| 297 |
|
| 298 |
@pytest.mark.parametrize("seed", SEEDS)
|
| 299 |
def test_repair_proc_only_wins_easy(seed):
|
| 300 |
+
"""On easy no band sits on the war factory, so a bridged proc keeps
|
| 301 |
+
its band pinned — repairing only the proc is sufficient → WIN."""
|
| 302 |
c, r = _run("easy", _repair_proc, seed=seed)
|
| 303 |
assert r.outcome == "win", (
|
| 304 |
f"easy seed{seed} repair-proc-only should WIN, got {r.outcome}"
|
|
|
|
| 308 |
@pytest.mark.parametrize("level", ("medium", "hard"))
|
| 309 |
@pytest.mark.parametrize("seed", SEEDS)
|
| 310 |
def test_repair_proc_only_loses_on_medium_and_hard(level, seed):
|
| 311 |
+
"""On medium / hard a light band ALSO chews the war factory —
|
| 312 |
+
repairing only the proc is no longer enough → LOSS (the deeper
|
| 313 |
+
2-of-3 triage axis)."""
|
| 314 |
c, r = _run(level, _repair_proc, seed=seed)
|
| 315 |
assert r.outcome == "loss", (
|
| 316 |
f"{level} seed{seed} repair-proc-only must LOSE; got {r.outcome} "
|