Spaces:
Running
def-walls-vs-towers: tier-differentiated walls vs towers doctrine
Browse filesRewrites the pack so the correct defensive posture genuinely differs
per tier โ a single 'always-towers' or 'always-walls' play fails at
least one tier, and the pack now exercises real per-tier doctrine
selection rather than the prior structure where all three tiers
solved with the same 4-turret cluster.
Map: rush-hour-arena (unchanged base map).
Defenders: brik (200cr inert walls) vs pbox (600cr M60mg pillbox).
Per-tier doctrine:
easy โ 2400cr; frontal 8xe1 rush; 4 pbox cluster wraps the fact
and shreds the wave. Pure-active is the correct doctrine.
medium โ 2400cr; overwhelming horde 16xe1 + 4xe3 on one lane;
1 pbox + 8 brik chokepoint funnels the horde to a single
cell where the pbox burst kills serially. Predicate
requires BOTH pbox AND brik (>=6) AND >=10 kills.
hard โ 3000cr; two simultaneous waves with seed-driven fact
flip (NORTH y=14 / SOUTH y=26); 2 pbox + 8 brik wall
belt across the convergence lane. Predicate requires
pbox (>=2), brik (>=6), >=10 kills.
Validation (scripted, no model):
intended doctrine WINS every tier x every seed (1..4);
pure-pbox WINS easy but LOSES medium/hard (the tier-differentiation
proof โ memorising easy fails the higher tiers);
pure-walls LOSES every tier x seed with 0 kills (mechanism check);
stall LOSES every tier x seed;
pure-army LOSES every tier x seed.
Engine: relies on the pbox-fires fix (M60mg burst on
garrison-only defenses with no explicit Armament, see
tests/test_pbox_fires.py).
Adds 'brik' to game_knowledge glossary (required by
test_every_active_pack_code_has_a_glossary_entry).
Updates the hard-tier doc comment in tests/test_hard_tier.py to
reflect the new tier-differentiated doctrine.
|
Binary file (881 Bytes). View file
|
|
|
|
@@ -55,6 +55,7 @@ ACTOR_GLOSSARY: dict[str, str] = {
|
|
| 55 |
"mine": "ore mine (resource node harvesters gather from)",
|
| 56 |
"gun": "gun turret (anti-vehicle base defence)",
|
| 57 |
"pbox": "pillbox (anti-infantry base defence)",
|
|
|
|
| 58 |
"tsla": "Tesla coil (powerful anti-everything defence)",
|
| 59 |
"sam": "SAM site (anti-air defence)",
|
| 60 |
}
|
|
|
|
| 55 |
"mine": "ore mine (resource node harvesters gather from)",
|
| 56 |
"gun": "gun turret (anti-vehicle base defence)",
|
| 57 |
"pbox": "pillbox (anti-infantry base defence)",
|
| 58 |
+
"brik": "concrete wall (inert obstacle; no weapon โ channels but does not kill)",
|
| 59 |
"tsla": "Tesla coil (powerful anti-everything defence)",
|
| 60 |
"sam": "SAM site (anti-air defence)",
|
| 61 |
}
|
|
@@ -57,7 +57,11 @@
|
|
| 57 |
# across fire-then-retreat cycles while preserving the survival
|
| 58 |
# bar.
|
| 59 |
#
|
| 60 |
-
# Topology (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
# โข Raiders stage centre-west, spread across three cells (not
|
| 62 |
# stacked โ a stack pin-piles in retreat).
|
| 63 |
# โข The heavy starts centre-east on the MID latitude (y=20) under
|
|
@@ -65,10 +69,15 @@
|
|
| 65 |
# must out-pace it, and the hunt advance is what brings the
|
| 66 |
# heavy into vision so the kite cycle has a target.
|
| 67 |
# โข Raiders stage OFF the heavy's latitude (a north corridor on
|
| 68 |
-
# easy/medium
|
| 69 |
-
#
|
| 70 |
-
#
|
| 71 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
#
|
| 73 |
# Validate (no model / no network):
|
| 74 |
# python3 -m pytest tests/test_combat_kite_and_pull.py -q
|
|
@@ -101,7 +110,22 @@ meta:
|
|
| 101 |
- "economy-of-force"
|
| 102 |
author: openra-bench
|
| 103 |
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
base:
|
| 107 |
agent: {faction: allies, cash: 0}
|
|
|
|
| 57 |
# across fire-then-retreat cycles while preserving the survival
|
| 58 |
# bar.
|
| 59 |
#
|
| 60 |
+
# Topology (combat-kite-pull-arena โ tailored OPEN 128ร40 arena,
|
| 61 |
+
# playable x 2..125, y 2..37; same dimensions as the legacy
|
| 62 |
+
# rush-hour-arena it replaces, but cordon-only with NO interior
|
| 63 |
+
# obstacles so the kiter has unimpeded reverse-fire mobility along
|
| 64 |
+
# the lane โ a stalled retreat cannot be blamed on terrain):
|
| 65 |
# โข Raiders stage centre-west, spread across three cells (not
|
| 66 |
# stacked โ a stack pin-piles in retreat).
|
| 67 |
# โข The heavy starts centre-east on the MID latitude (y=20) under
|
|
|
|
| 69 |
# must out-pace it, and the hunt advance is what brings the
|
| 70 |
# heavy into vision so the kite cycle has a target.
|
| 71 |
# โข Raiders stage OFF the heavy's latitude (a north corridor on
|
| 72 |
+
# easy/medium; on hard the corridor flips per seed between
|
| 73 |
+
# y=10 north and y=30 south for spawn variation).
|
| 74 |
+
# โข The arena is fully open inside the cordon โ the kiter has the
|
| 75 |
+
# ENTIRE west half available to retreat into without snagging on
|
| 76 |
+
# terrain, which is the load-bearing freedom the kite-and-pull
|
| 77 |
+
# cycle needs to keep the heavy at standoff range.
|
| 78 |
+
# โข Persistent unarmed enemy `fact` far east (x=124) keeps the
|
| 79 |
+
# episode alive past the heavy's death so the win/fail evaluator
|
| 80 |
+
# runs (CLAUDE.md auto-done footgun).
|
| 81 |
#
|
| 82 |
# Validate (no model / no network):
|
| 83 |
# python3 -m pytest tests/test_combat_kite_and_pull.py -q
|
|
|
|
| 110 |
- "economy-of-force"
|
| 111 |
author: openra-bench
|
| 112 |
|
| 113 |
+
# combat-kite-pull-arena: tailored OPEN 128ร40 arena (playable x in
|
| 114 |
+
# [2..125], y in [2..37]). Generator: `arena` with NO interior
|
| 115 |
+
# obstacles โ every cell inside the cordon is clear ground so the
|
| 116 |
+
# kiter has unimpeded reverse-fire mobility along the lane and any
|
| 117 |
+
# failed retreat is the policy's fault, not the terrain's. Same
|
| 118 |
+
# dimensions as the legacy `rush-hour-arena` it replaces (so the
|
| 119 |
+
# proven actor coords carry across unchanged) but owned by THIS
|
| 120 |
+
# pack so the obstacle-free guarantee is reasoned about precisely
|
| 121 |
+
# and isn't subject to drift in the shared training map.
|
| 122 |
+
base_map:
|
| 123 |
+
generator: arena
|
| 124 |
+
name: combat-kite-pull-arena
|
| 125 |
+
width: 128
|
| 126 |
+
height: 40
|
| 127 |
+
cordon: 2
|
| 128 |
+
title: Kite and Pull Arena 128x40
|
| 129 |
|
| 130 |
base:
|
| 131 |
agent: {faction: allies, cash: 0}
|
|
@@ -1,131 +1,91 @@
|
|
| 1 |
-
# def-walls-vs-towers โ
|
| 2 |
#
|
| 3 |
-
# REASONING focus:
|
| 4 |
-
#
|
| 5 |
-
#
|
| 6 |
-
#
|
| 7 |
-
#
|
| 8 |
-
#
|
| 9 |
-
# base merely delays the rush; the wall neutralises nothing.
|
| 10 |
-
# โข ACTIVE โ gun turrets (800cr each, TurretGun armament, anti-
|
| 11 |
-
# ground fire): a tower that actually KILLS the attacking band.
|
| 12 |
-
# Active defence destroys the threat instead of merely delaying it.
|
| 13 |
#
|
| 14 |
-
#
|
| 15 |
-
#
|
| 16 |
-
#
|
| 17 |
-
# walls-only spend produces 0 kills โ fails the kill clause and the
|
| 18 |
-
# episode times out โ LOSS. A stall spends nothing โ the rush razes the
|
| 19 |
-
# fact โ LOSS. Only the intended gun turrets satisfy count + kill +
|
| 20 |
-
# fact-alive.
|
| 21 |
#
|
| 22 |
-
#
|
| 23 |
-
#
|
| 24 |
-
#
|
| 25 |
-
#
|
| 26 |
-
#
|
| 27 |
-
#
|
| 28 |
-
#
|
| 29 |
-
#
|
| 30 |
-
#
|
| 31 |
-
#
|
| 32 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
#
|
| 34 |
-
#
|
| 35 |
-
# โข
|
| 36 |
-
#
|
| 37 |
-
#
|
| 38 |
-
#
|
| 39 |
-
#
|
| 40 |
-
# channels and delays the rush but kills NOTHING โ the
|
| 41 |
-
# `units_killed_gte` clause is never satisfied (0 kills) so the
|
| 42 |
-
# win never latches and the episode runs out the clock โ LOSS via
|
| 43 |
-
# `after_ticks`. A real reachable LOSS, not a draw.
|
| 44 |
-
# โข pure-army (only e1, no defensive buildings): FAILS the gun count
|
| 45 |
-
# clause; a thin home-trained rifle screen cannot out-trade the
|
| 46 |
-
# heavier rush band either โ fact often falls โ LOSS.
|
| 47 |
-
# โข intended TOWERS (4 gun turrets wrapping the fact): TurretGun fire
|
| 48 |
-
# shreds the rush โ count + kill + fact-alive + within_ticks all
|
| 49 |
-
# satisfied โ WIN.
|
| 50 |
#
|
| 51 |
-
#
|
| 52 |
-
# โข
|
| 53 |
-
#
|
| 54 |
-
#
|
| 55 |
-
#
|
| 56 |
-
#
|
| 57 |
-
#
|
| 58 |
-
#
|
| 59 |
-
#
|
| 60 |
-
#
|
| 61 |
-
#
|
| 62 |
-
#
|
| 63 |
-
#
|
| 64 |
-
# โข
|
| 65 |
-
#
|
| 66 |
-
#
|
| 67 |
-
#
|
| 68 |
-
#
|
| 69 |
-
#
|
| 70 |
-
#
|
| 71 |
-
#
|
| 72 |
-
#
|
| 73 |
-
#
|
| 74 |
-
# the
|
| 75 |
-
# โข `units_killed_gte:K` is the load-bearing clause: walls produce 0
|
| 76 |
-
# kills, gun turrets produce many. This is the predicate that
|
| 77 |
-
# makes ACTIVE defence (not passive obstruction) the only winning
|
| 78 |
-
# play.
|
| 79 |
-
# โข `building_count_gte:{fact,1}` (PRESENT-TENSE) is the fact-alive
|
| 80 |
-
# check โ `has_building` is a one-shot ever-seen set that stays
|
| 81 |
-
# true after the fact is razed (documented CLAUDE.md footgun).
|
| 82 |
-
# โข `after_ticks: BUDGET+1` fail clause is reachable within max_turns
|
| 83 |
-
# (no interrupts โ exactly 90 ticks/step โ max tick =
|
| 84 |
-
# 93+90ยท(N-1)); a non-finisher (stall, walls-only) hits it and
|
| 85 |
-
# LOSES rather than draws.
|
| 86 |
-
# โข A persistent unarmed enemy `fact` at (120,20) keeps the episode
|
| 87 |
-
# alive past full rush elimination so the win/fail evaluator sees
|
| 88 |
-
# the terminal frame (no DRAW collapse on enemy-wipe auto-done).
|
| 89 |
-
# โข The `rusher` bot relentlessly charges the agent's centroid (the
|
| 90 |
-
# fact) โ so the scheduled wave converges on the protected asset
|
| 91 |
-
# whatever defence is built in front of it.
|
| 92 |
|
| 93 |
meta:
|
| 94 |
id: def-walls-vs-towers
|
| 95 |
-
title: 'Defense Doctrine โ
|
| 96 |
capability: reasoning
|
| 97 |
real_world_meaning: >
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
turrets WINS.
|
| 107 |
robotics_analogue: >
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
DELAYS and CHANNELS an assault; it is dead weight unless paired
|
| 116 |
-
with the covering FIRES the obstacle funnels the enemy into.
|
| 117 |
benchmark_anchor:
|
| 118 |
- 'security architecture'
|
| 119 |
- 'passive vs active mitigation'
|
| 120 |
- 'military fortification'
|
| 121 |
author: openra-bench
|
| 122 |
|
| 123 |
-
# rush-hour-arena (128ร40, playable x in [2..126], y in [2..38]). Agent
|
| 124 |
-
# base on the west (fact at (10,20) + tent + 2รpowr, NO pre-placed
|
| 125 |
-
# combat units); the rush wave is injected mid-east via a scheduled
|
| 126 |
-
# spawn_actors event and the rusher bot charges the agent centroid (the
|
| 127 |
-
# fact). The agent must spend its one budget on ACTIVE gun turrets that
|
| 128 |
-
# kill the rush โ not on inert brik walls that only delay it.
|
| 129 |
base_map: rush-hour-arena
|
| 130 |
|
| 131 |
base:
|
|
@@ -140,71 +100,49 @@ base:
|
|
| 140 |
- attack_move
|
| 141 |
- stop
|
| 142 |
planning: true
|
| 143 |
-
#
|
| 144 |
-
#
|
| 145 |
-
#
|
| 146 |
-
#
|
| 147 |
-
# budget the `after_ticks` fail clause reliably bites in).
|
| 148 |
interrupts: {}
|
| 149 |
termination:
|
| 150 |
max_ticks: 12000
|
| 151 |
actors: [] # every level supplies its own actor list via overrides.
|
| 152 |
|
| 153 |
levels:
|
| 154 |
-
# โโ EASY โโ
|
| 155 |
-
#
|
| 156 |
-
# injected at tick 1800
|
| 157 |
-
#
|
| 158 |
-
#
|
| 159 |
-
#
|
| 160 |
-
# kills 0 โ fails the kill clause โ the win never latches โ LOSS via
|
| 161 |
-
# `after_ticks`. Stall loses (fact razed). max_turns 60 โ reachable
|
| 162 |
-
# tick 93+90ยท59 = 5403; deadline 5400.
|
| 163 |
easy:
|
| 164 |
-
# Original (pre-verbosity-sweep) description preserved for
|
| 165 |
-
# contributors. The trimmed version below removes scripted-policy
|
| 166 |
-
# spoilers and cell-coord dumps; load-bearing intent kept.
|
| 167 |
-
#
|
| 168 |
-
# A rusher band of 8 rifle infantry will charge your construction yard
|
| 169 |
-
# (fact, at (10,20)). You have ONE budget of 3200cr โ enough for
|
| 170 |
-
# EITHER ~16 concrete walls (brik, 200cr, no weapon โ an inert
|
| 171 |
-
# barrier) OR 4 gun turrets (gun, 800cr each โ active anti-ground
|
| 172 |
-
# towers that KILL). Build 4 gun turrets wrapping the fact so their
|
| 173 |
-
# fire shreds the rush. Win requires 4 gun turrets built, at least 4
|
| 174 |
-
# enemy units killed, and the fact still standing before tick 5400.
|
| 175 |
-
# Walls only delay the rush โ they kill nothing, so a walls-only spend
|
| 176 |
-
# FAILS the kill bar. Stall and pure-army also LOSE.
|
| 177 |
description: >
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
starting_cash:
|
| 184 |
overrides:
|
| 185 |
actors:
|
| 186 |
# Pre-placed agent base โ fact + tent + 2รpowr. NO combat
|
| 187 |
-
# units near the base
|
| 188 |
-
#
|
| 189 |
-
#
|
| 190 |
-
#
|
| 191 |
- {type: fact, owner: agent, position: [10, 20]}
|
| 192 |
- {type: tent, owner: agent, position: [6, 18]}
|
| 193 |
- {type: powr, owner: agent, position: [6, 22]}
|
| 194 |
- {type: powr, owner: agent, position: [6, 16]}
|
| 195 |
# ONE non-combatant agent e1 parked in the far SW corner,
|
| 196 |
-
#
|
| 197 |
-
# so `units_summary` is non-empty
|
| 198 |
-
#
|
| 199 |
-
# not let a walls-only / stall play pass the kill clause.
|
| 200 |
- {type: e1, owner: agent, position: [3, 36], stance: 2}
|
| 201 |
# Unarmed high-HP marker (anti-DRAW): keeps the episode alive
|
| 202 |
# past full rush elimination so the win/fail check fires.
|
| 203 |
- {type: fact, owner: enemy, position: [120, 20]}
|
| 204 |
-
# Scheduled rush wave โ injected at tick 1800, mid-east on the
|
| 205 |
-
# y=20 lane. The rusher bot charges the agent centroid (the
|
| 206 |
-
# fact). 8รe1 overwhelms a passive wall / pure-army defence but
|
| 207 |
-
# a cluster of 4 active gun turrets shreds it.
|
| 208 |
scheduled_events:
|
| 209 |
- tick: 1800
|
| 210 |
type: spawn_actors
|
|
@@ -212,7 +150,7 @@ levels:
|
|
| 212 |
- {type: e1, owner: enemy, position: [60, 20], stance: 3, count: 8}
|
| 213 |
win_condition:
|
| 214 |
all_of:
|
| 215 |
-
- {building_count_gte: {type:
|
| 216 |
- {units_killed_gte: 4}
|
| 217 |
- {building_count_gte: {type: fact, n: 1}}
|
| 218 |
- {within_ticks: 5400}
|
|
@@ -222,53 +160,51 @@ levels:
|
|
| 222 |
- {not: {building_count_gte: {type: fact, n: 1}}}
|
| 223 |
max_turns: 60
|
| 224 |
|
| 225 |
-
# โโ MEDIUM โโ
|
| 226 |
-
#
|
| 227 |
-
#
|
| 228 |
-
#
|
| 229 |
-
#
|
| 230 |
-
#
|
| 231 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
medium:
|
| 233 |
-
# Original (pre-verbosity-sweep) description preserved for
|
| 234 |
-
# contributors. The trimmed version below removes scripted-policy
|
| 235 |
-
# spoilers and cell-coord dumps; load-bearing intent kept.
|
| 236 |
-
#
|
| 237 |
-
# A heavier rush โ 8 rifle infantry + 2 rocket soldiers โ will charge
|
| 238 |
-
# your construction yard (fact, at (10,20)). One budget of 3200cr:
|
| 239 |
-
# EITHER ~16 inert concrete walls (brik) OR 4 active gun turrets
|
| 240 |
-
# (gun). Build 4 gun turrets wrapping the fact. Win requires 4 gun
|
| 241 |
-
# turrets built, at least 6 enemy units killed, and the fact still
|
| 242 |
-
# standing before tick 5400. Walls kill nothing (they only delay) so a
|
| 243 |
-
# walls-only spend FAILS the kill bar; stall and pure-army also LOSE.
|
| 244 |
description: >
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
| 250 |
overrides:
|
| 251 |
actors:
|
| 252 |
- {type: fact, owner: agent, position: [10, 20]}
|
| 253 |
- {type: tent, owner: agent, position: [6, 18]}
|
| 254 |
- {type: powr, owner: agent, position: [6, 22]}
|
| 255 |
- {type: powr, owner: agent, position: [6, 16]}
|
| 256 |
-
# Non-combatant SW-corner e1 (see easy) โ non-empty
|
| 257 |
-
# units_summary, zero kill contribution.
|
| 258 |
- {type: e1, owner: agent, position: [3, 36], stance: 2}
|
| 259 |
-
# Anti-DRAW marker.
|
| 260 |
- {type: fact, owner: enemy, position: [120, 20]}
|
| 261 |
-
#
|
| 262 |
scheduled_events:
|
| 263 |
- tick: 1800
|
| 264 |
type: spawn_actors
|
| 265 |
actors:
|
| 266 |
-
- {type: e1, owner: enemy, position: [60, 20], stance: 3, count:
|
| 267 |
-
- {type: e3, owner: enemy, position: [62, 20], stance: 3, count:
|
| 268 |
win_condition:
|
| 269 |
all_of:
|
| 270 |
-
- {building_count_gte: {type:
|
| 271 |
-
- {
|
|
|
|
| 272 |
- {building_count_gte: {type: fact, n: 1}}
|
| 273 |
- {within_ticks: 5400}
|
| 274 |
fail_condition:
|
|
@@ -277,52 +213,33 @@ levels:
|
|
| 277 |
- {not: {building_count_gte: {type: fact, n: 1}}}
|
| 278 |
max_turns: 60
|
| 279 |
|
| 280 |
-
# โโ HARD โโ
|
| 281 |
-
#
|
| 282 |
-
#
|
| 283 |
-
#
|
| 284 |
-
#
|
| 285 |
-
#
|
| 286 |
-
#
|
| 287 |
-
#
|
| 288 |
-
#
|
| 289 |
-
#
|
| 290 |
-
# (the on-latitude wave is 7 units โ bar 6 leaves a one-unit margin
|
| 291 |
-
# so a slightly imperfect turret cluster still clears it, while a
|
| 292 |
-
# walls-only spend at 0 kills is nowhere near).
|
| 293 |
-
# max_turns 60 โ reachable tick 5403; deadline 5400.
|
| 294 |
hard:
|
| 295 |
-
# Original (pre-verbosity-sweep) description preserved for
|
| 296 |
-
# contributors. The trimmed version below removes scripted-policy
|
| 297 |
-
# spoilers and cell-coord dumps; load-bearing intent kept.
|
| 298 |
-
#
|
| 299 |
-
# The construction yard (fact) flips between NORTH (y=14) and SOUTH
|
| 300 |
-
# (y=26) by seed; the turret cluster must follow. One budget of
|
| 301 |
-
# 3200cr: EITHER ~16 inert concrete walls (brik) OR 4 active gun
|
| 302 |
-
# turrets (gun). Build 4 gun turrets wrapping the CURRENT fact (either
|
| 303 |
-
# (10,14) or (10,26) โ read it from the observation). Win requires 4
|
| 304 |
-
# gun turrets built, at least 6 enemy units killed, and the fact still
|
| 305 |
-
# standing before tick 5400. A walls-only spend kills nothing; stall
|
| 306 |
-
# and pure-army also LOSE.
|
| 307 |
description: >
|
| 308 |
-
Your yard spawns NORTH (y=14) or SOUTH (y=26) by seed
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
|
|
|
| 313 |
overrides:
|
| 314 |
actors:
|
| 315 |
-
# spawn_point 0 โ NORTH base. Fact at (10,14)
|
| 316 |
-
# The non-combatant corner e1 parks in the far SW corner,
|
| 317 |
-
# well clear of the y=14 rush lane (zero kill contribution).
|
| 318 |
- {type: fact, owner: agent, position: [10, 14], spawn_point: 0}
|
| 319 |
- {type: tent, owner: agent, position: [6, 12], spawn_point: 0}
|
| 320 |
- {type: powr, owner: agent, position: [6, 10], spawn_point: 0}
|
| 321 |
- {type: powr, owner: agent, position: [6, 16], spawn_point: 0}
|
| 322 |
- {type: e1, owner: agent, position: [3, 36], stance: 2, spawn_point: 0}
|
| 323 |
-
# spawn_point 1 โ SOUTH base (mirror
|
| 324 |
-
# non-combatant corner e1 parks in the far NW corner, well
|
| 325 |
-
# clear of the y=26 rush lane.
|
| 326 |
- {type: fact, owner: agent, position: [10, 26], spawn_point: 1}
|
| 327 |
- {type: tent, owner: agent, position: [6, 24], spawn_point: 1}
|
| 328 |
- {type: powr, owner: agent, position: [6, 28], spawn_point: 1}
|
|
@@ -330,24 +247,39 @@ levels:
|
|
| 330 |
- {type: e1, owner: agent, position: [3, 4], stance: 2, spawn_point: 1}
|
| 331 |
# Anti-DRAW marker on the symmetry axis.
|
| 332 |
- {type: fact, owner: enemy, position: [120, 20]}
|
| 333 |
-
#
|
| 334 |
-
#
|
| 335 |
-
#
|
| 336 |
-
#
|
| 337 |
-
#
|
| 338 |
-
#
|
|
|
|
|
|
|
|
|
|
| 339 |
scheduled_events:
|
| 340 |
- tick: 1800
|
| 341 |
type: spawn_actors
|
| 342 |
actors:
|
| 343 |
-
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
win_condition:
|
| 348 |
all_of:
|
| 349 |
-
- {building_count_gte: {type:
|
| 350 |
-
- {
|
|
|
|
| 351 |
- {building_count_gte: {type: fact, n: 1}}
|
| 352 |
- {within_ticks: 5400}
|
| 353 |
fail_condition:
|
|
|
|
| 1 |
+
# def-walls-vs-towers โ Walls vs Towers (tier-differentiated doctrine)
|
| 2 |
#
|
| 3 |
+
# REASONING focus: the agent must choose between PASSIVE OBSTACLE (brik
|
| 4 |
+
# concrete walls โ 200cr, HP 40000, NO Armament) and ACTIVE TURRET (pbox
|
| 5 |
+
# pillbox โ 600cr, M60mg burst โ anti-infantry direct fire). The correct
|
| 6 |
+
# answer DIFFERS PER TIER, so a single memorised "always build towers"
|
| 7 |
+
# (or "always build walls") fails one of the tiers. Each tier's geometry
|
| 8 |
+
# and predicate make exactly one doctrine correct.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
#
|
| 10 |
+
# EASY โ frontal rush in the open. Walls kill nothing and the rush
|
| 11 |
+
# walks around a thin barrier. ACTIVE pbox cluster wraps the
|
| 12 |
+
# fact and shreds the wave. Win requires pbox count + kills.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
#
|
| 14 |
+
# MEDIUM โ a numerically overwhelming horde funnels at the fact on
|
| 15 |
+
# one lane. The win predicate REQUIRES a wall belt PLUS a
|
| 16 |
+
# pbox at the choke: a wall barrier funnels the horde to a
|
| 17 |
+
# single-cell pass where a single pbox serially shreds the
|
| 18 |
+
# queue. Pure-walls (0 kills) and pure-pbox (no brik) both
|
| 19 |
+
# FAIL the predicate.
|
| 20 |
+
#
|
| 21 |
+
# HARD โ mixed approach: a HEAVY infantry horde on the agent's
|
| 22 |
+
# current lane AND a separate probe on the opposite lane.
|
| 23 |
+
# The agent must allocate budget across BOTH postures:
|
| 24 |
+
# wall+pbox choke for the horde, flanking pbox for the
|
| 25 |
+
# probe. Predicate requires pbox count, brik count, AND a
|
| 26 |
+
# kill quota. The fact also flips between two seed-driven
|
| 27 |
+
# y-positions (hard-tier contract), so the build cannot be
|
| 28 |
+
# memorised by coord.
|
| 29 |
#
|
| 30 |
+
# Real-world anchor:
|
| 31 |
+
# โข Doctrine selection โ the SAME budget buys different defensive
|
| 32 |
+
# postures (open-arc covering fires vs channelised killbox); only
|
| 33 |
+
# the right posture for THIS geometry wins. A general who always
|
| 34 |
+
# builds towers loses on the horde lane; a general who always
|
| 35 |
+
# builds walls loses on the open arc.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
#
|
| 37 |
+
# Engine facts (per CLAUDE.md):
|
| 38 |
+
# โข pbox = pillbox; cost 600; HP 40000; Allied; prereq tent. The
|
| 39 |
+
# engine now assigns M60mg (damage 1000, burst 5, reload 30, range 4)
|
| 40 |
+
# to garrison-only defenses with no explicit Armament (pinned by
|
| 41 |
+
# tests/test_pbox_fires.py). A built pbox shoots โ kills count
|
| 42 |
+
# toward `units_killed_gte`.
|
| 43 |
+
# โข brik = concrete wall; cost 200; HP 40000; NO Armament โ inert
|
| 44 |
+
# obstacle that channels and delays a charge but produces zero
|
| 45 |
+
# kill output.
|
| 46 |
+
# โข The rusher bot relentlessly charges the agent centroid (the fact)
|
| 47 |
+
# so the scheduled wave converges on the protected asset whatever
|
| 48 |
+
# defence is built in front of it. A wall in the path FORCES the
|
| 49 |
+
# rusher to detour, channelising it through whatever gap is left.
|
| 50 |
+
# โข place_building works at any in-bounds cell (no adjacency check),
|
| 51 |
+
# so the wall/pbox cluster can be assembled anywhere reachable.
|
| 52 |
+
# โข Each pack step โ 90 ticks. max_turns=60 โ max reachable tick
|
| 53 |
+
# 93+90ยท59 = 5403; deadlines (within_ticks 5400, after_ticks 5401)
|
| 54 |
+
# sit safely below โ a non-finisher reaches a real LOSS rather
|
| 55 |
+
# than draws.
|
| 56 |
+
# โข An unarmed enemy `fact` marker at the far east keeps the episode
|
| 57 |
+
# alive past full wave elimination (engine auto-`done` mitigation).
|
| 58 |
+
# โข Hard tier defines TWO agent spawn_point groups (north y=14 /
|
| 59 |
+
# south y=26) so the fact and the whole defence cluster flip by
|
| 60 |
+
# seed, satisfying the hard-tier contract.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
meta:
|
| 63 |
id: def-walls-vs-towers
|
| 64 |
+
title: 'Defense Doctrine โ Walls vs Towers per Tier'
|
| 65 |
capability: reasoning
|
| 66 |
real_world_meaning: >
|
| 67 |
+
Defensive doctrine selection: the same budget buys different
|
| 68 |
+
postures. An open frontal arc needs ACTIVE covering fires (pbox
|
| 69 |
+
pillboxes). A numerically overwhelming horde on one lane needs
|
| 70 |
+
PASSIVE channelisation (a brik wall barrier) plus a single pbox
|
| 71 |
+
at the choke to shred the queue. A mixed two-lane attack forces
|
| 72 |
+
a TRADEOFF: choke one lane, cover the other. A memorised
|
| 73 |
+
"always-towers" or "always-walls" play fails the tier where its
|
| 74 |
+
posture is wrong.
|
|
|
|
| 75 |
robotics_analogue: >
|
| 76 |
+
Defensive doctrine: an open arc demands covering fires; a single
|
| 77 |
+
overwhelming axis demands channelisation (obstacle belt + fires
|
| 78 |
+
at the choke); a mixed threat demands force allocation across
|
| 79 |
+
postures. The same budget produces wildly different outcomes
|
| 80 |
+
depending on which doctrine matches the geometry โ a general who
|
| 81 |
+
always builds towers loses the horde lane and one who always
|
| 82 |
+
builds walls loses the open arc.
|
|
|
|
|
|
|
| 83 |
benchmark_anchor:
|
| 84 |
- 'security architecture'
|
| 85 |
- 'passive vs active mitigation'
|
| 86 |
- 'military fortification'
|
| 87 |
author: openra-bench
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
base_map: rush-hour-arena
|
| 90 |
|
| 91 |
base:
|
|
|
|
| 100 |
- attack_move
|
| 101 |
- stop
|
| 102 |
planning: true
|
| 103 |
+
# Static posture decision: the threat axis is known up-front (the
|
| 104 |
+
# rusher always charges the fact). Disabling interrupts also makes
|
| 105 |
+
# the tick budget deterministic (exactly 90 ticks/step โ max_turns
|
| 106 |
+
# is a hard tick budget the `after_ticks` fail clause bites in).
|
|
|
|
| 107 |
interrupts: {}
|
| 108 |
termination:
|
| 109 |
max_ticks: 12000
|
| 110 |
actors: [] # every level supplies its own actor list via overrides.
|
| 111 |
|
| 112 |
levels:
|
| 113 |
+
# โโ EASY โโ frontal rush in the open. Budget 2400cr = 4 pbox
|
| 114 |
+
# (4ร600) OR 12 brik (12ร200). The rush wave is 8รe1 on the lane,
|
| 115 |
+
# injected at tick 1800 (after the intended 4-pbox build completes).
|
| 116 |
+
# Win requires 4 pbox built AND โฅ4 kills AND the fact alive. A
|
| 117 |
+
# walls-only spend kills 0 โ fails the kill clause โ LOSS via
|
| 118 |
+
# after_ticks. Pure-army and stall LOSE for the same reason.
|
|
|
|
|
|
|
|
|
|
| 119 |
easy:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
description: >
|
| 121 |
+
Eight riflemen charge your yard from the east. Your $2400 buys
|
| 122 |
+
either four active pillboxes or twelve inert concrete walls. Walls
|
| 123 |
+
don't shoot โ kills come from the pillboxes. Build four pillboxes
|
| 124 |
+
wrapping the yard so their fire shreds the rush. Yard intact, four
|
| 125 |
+
kills, within about 60 turns.
|
| 126 |
+
starting_cash: 2400
|
| 127 |
overrides:
|
| 128 |
actors:
|
| 129 |
# Pre-placed agent base โ fact + tent + 2รpowr. NO combat
|
| 130 |
+
# units near the base; pre-placed defenders would rack up
|
| 131 |
+
# kills for free and let a walls-only spend pass the kill
|
| 132 |
+
# clause off them. Two powr cover the 4-pbox power draw
|
| 133 |
+
# (4ร-20=-80; 2รpowr=+200).
|
| 134 |
- {type: fact, owner: agent, position: [10, 20]}
|
| 135 |
- {type: tent, owner: agent, position: [6, 18]}
|
| 136 |
- {type: powr, owner: agent, position: [6, 22]}
|
| 137 |
- {type: powr, owner: agent, position: [6, 16]}
|
| 138 |
# ONE non-combatant agent e1 parked in the far SW corner,
|
| 139 |
+
# well clear of the y=20 rush lane (zero kill contribution;
|
| 140 |
+
# exists only so `units_summary` is non-empty for the
|
| 141 |
+
# hard-tier env-reset check).
|
|
|
|
| 142 |
- {type: e1, owner: agent, position: [3, 36], stance: 2}
|
| 143 |
# Unarmed high-HP marker (anti-DRAW): keeps the episode alive
|
| 144 |
# past full rush elimination so the win/fail check fires.
|
| 145 |
- {type: fact, owner: enemy, position: [120, 20]}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
scheduled_events:
|
| 147 |
- tick: 1800
|
| 148 |
type: spawn_actors
|
|
|
|
| 150 |
- {type: e1, owner: enemy, position: [60, 20], stance: 3, count: 8}
|
| 151 |
win_condition:
|
| 152 |
all_of:
|
| 153 |
+
- {building_count_gte: {type: pbox, n: 4}}
|
| 154 |
- {units_killed_gte: 4}
|
| 155 |
- {building_count_gte: {type: fact, n: 1}}
|
| 156 |
- {within_ticks: 5400}
|
|
|
|
| 160 |
- {not: {building_count_gte: {type: fact, n: 1}}}
|
| 161 |
max_turns: 60
|
| 162 |
|
| 163 |
+
# โโ MEDIUM โโ numerically overwhelming horde. The walls+pbox CHOKE
|
| 164 |
+
# is the correct doctrine. Budget 2400cr funds (1 pbox + 9 brik) =
|
| 165 |
+
# 600 + 1800 = 2400 with cash to spare for any extra wall.
|
| 166 |
+
#
|
| 167 |
+
# Predicate doctrine โ REQUIRE both wall belt AND pbox AND kills:
|
| 168 |
+
# building_count_gte:{pbox,1} โ at least one active turret at
|
| 169 |
+
# the choke (kills come from here).
|
| 170 |
+
# building_count_gte:{brik,6} โ a genuine wall belt (not 1-2
|
| 171 |
+
# incidental walls); the channelisation must be real.
|
| 172 |
+
# units_killed_gte:10 โ the choke + pbox must actually kill
|
| 173 |
+
# half the horde; a single pbox at the choke serially kills
|
| 174 |
+
# queued e1 fast enough to clear this bar.
|
| 175 |
+
#
|
| 176 |
+
# This makes pure-pbox (no brik clause met โ fail) and pure-walls
|
| 177 |
+
# (0 kills โ fail) both LOSE; only the wall+pbox combo wins.
|
| 178 |
medium:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
description: >
|
| 180 |
+
A massive horde โ sixteen riflemen plus four rocket soldiers โ
|
| 181 |
+
funnels at your yard from the east on a single lane. Your $2400
|
| 182 |
+
buys either ~12 inert walls or 4 active pillboxes. Neither alone
|
| 183 |
+
satisfies the doctrine here: pure walls kill nothing, pure
|
| 184 |
+
pillboxes leave the horde un-channelised. Build a wall belt that
|
| 185 |
+
channels the horde to a tight pass, with a pillbox at the choke
|
| 186 |
+
to shred the queue. Yard intact, ten kills, within about 60 turns.
|
| 187 |
+
starting_cash: 2400
|
| 188 |
overrides:
|
| 189 |
actors:
|
| 190 |
- {type: fact, owner: agent, position: [10, 20]}
|
| 191 |
- {type: tent, owner: agent, position: [6, 18]}
|
| 192 |
- {type: powr, owner: agent, position: [6, 22]}
|
| 193 |
- {type: powr, owner: agent, position: [6, 16]}
|
|
|
|
|
|
|
| 194 |
- {type: e1, owner: agent, position: [3, 36], stance: 2}
|
|
|
|
| 195 |
- {type: fact, owner: enemy, position: [120, 20]}
|
| 196 |
+
# Overwhelming horde โ 16 e1 + 4 e3, all on y=20.
|
| 197 |
scheduled_events:
|
| 198 |
- tick: 1800
|
| 199 |
type: spawn_actors
|
| 200 |
actors:
|
| 201 |
+
- {type: e1, owner: enemy, position: [60, 20], stance: 3, count: 16}
|
| 202 |
+
- {type: e3, owner: enemy, position: [62, 20], stance: 3, count: 4}
|
| 203 |
win_condition:
|
| 204 |
all_of:
|
| 205 |
+
- {building_count_gte: {type: pbox, n: 1}}
|
| 206 |
+
- {building_count_gte: {type: brik, n: 6}}
|
| 207 |
+
- {units_killed_gte: 10}
|
| 208 |
- {building_count_gte: {type: fact, n: 1}}
|
| 209 |
- {within_ticks: 5400}
|
| 210 |
fail_condition:
|
|
|
|
| 213 |
- {not: {building_count_gte: {type: fact, n: 1}}}
|
| 214 |
max_turns: 60
|
| 215 |
|
| 216 |
+
# โโ HARD โโ MIXED approach + seed-driven fact flip. The fact sits
|
| 217 |
+
# at y=14 (NORTH) for some seeds and y=26 (SOUTH) for others โ the
|
| 218 |
+
# build cannot be memorised by coord. On EVERY seed two simultaneous
|
| 219 |
+
# waves arrive: a HEAVY horde (10รe1 + 2รe3) on each lane. The rusher
|
| 220 |
+
# converges on the agent centroid (the active fact's lane); the
|
| 221 |
+
# off-lane wave still pours toward the centroid and a flanking pbox
|
| 222 |
+
# is required to interdict it. Predicate requires pbox count + brik
|
| 223 |
+
# count + a kill quota โ solving only one lane fails the kill quota.
|
| 224 |
+
# Budget 3000cr = 2 pbox (1200) + 9 brik (1800) โ the minimum
|
| 225 |
+
# walls-plus-towers spend that satisfies both lanes.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
hard:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
description: >
|
| 228 |
+
Your yard spawns NORTH (y=14) or SOUTH (y=26) by seed. TWO waves
|
| 229 |
+
arrive โ a heavy horde on your yard's lane and another on the
|
| 230 |
+
opposite lane. Your $3000 buys a tradeoff: channel the horde with
|
| 231 |
+
a wall belt + pillbox at the choke AND post a flanking pillbox on
|
| 232 |
+
the opposite lane. Yard intact, ten kills, within about 60 turns.
|
| 233 |
+
starting_cash: 3000
|
| 234 |
overrides:
|
| 235 |
actors:
|
| 236 |
+
# spawn_point 0 โ NORTH base. Fact at (10,14).
|
|
|
|
|
|
|
| 237 |
- {type: fact, owner: agent, position: [10, 14], spawn_point: 0}
|
| 238 |
- {type: tent, owner: agent, position: [6, 12], spawn_point: 0}
|
| 239 |
- {type: powr, owner: agent, position: [6, 10], spawn_point: 0}
|
| 240 |
- {type: powr, owner: agent, position: [6, 16], spawn_point: 0}
|
| 241 |
- {type: e1, owner: agent, position: [3, 36], stance: 2, spawn_point: 0}
|
| 242 |
+
# spawn_point 1 โ SOUTH base (mirror).
|
|
|
|
|
|
|
| 243 |
- {type: fact, owner: agent, position: [10, 26], spawn_point: 1}
|
| 244 |
- {type: tent, owner: agent, position: [6, 24], spawn_point: 1}
|
| 245 |
- {type: powr, owner: agent, position: [6, 28], spawn_point: 1}
|
|
|
|
| 247 |
- {type: e1, owner: agent, position: [3, 4], stance: 2, spawn_point: 1}
|
| 248 |
# Anti-DRAW marker on the symmetry axis.
|
| 249 |
- {type: fact, owner: enemy, position: [120, 20]}
|
| 250 |
+
# Two simultaneous waves on BOTH lanes (the spawn_actors list is
|
| 251 |
+
# not spawn_point-filtered, so both lanes always spawn). The
|
| 252 |
+
# rusher converges on the agent centroid (the active fact's
|
| 253 |
+
# lane); the off-lane wave is a LIGHT probe (4รe1) that a single
|
| 254 |
+
# flanking pbox at the active-lane choke can mop up as it walks
|
| 255 |
+
# past, while the heavy on-lane horde is shredded by the wall
|
| 256 |
+
# belt + pbox choke. Heavy ON the active lane forces the wall
|
| 257 |
+
# belt; light probe OFF the active lane is the "flank" budget
|
| 258 |
+
# tradeoff the predicate enforces.
|
| 259 |
scheduled_events:
|
| 260 |
- tick: 1800
|
| 261 |
type: spawn_actors
|
| 262 |
actors:
|
| 263 |
+
# NORTH-lane wave โ HEAVY horde (12 attackers) plus a
|
| 264 |
+
# LIGHT probe (4 e1). On a NORTH-base seed the heavy
|
| 265 |
+
# horde is on-lane (channeled by the wall belt); on a
|
| 266 |
+
# SOUTH-base seed the heavy north wave is off-lane and
|
| 267 |
+
# detours through y=20 to reach the south fact, walking
|
| 268 |
+
# past the choke on its way down.
|
| 269 |
+
- {type: e1, owner: enemy, position: [60, 14], stance: 3, count: 10}
|
| 270 |
+
- {type: e3, owner: enemy, position: [62, 14], stance: 3, count: 2}
|
| 271 |
+
# SOUTH-lane wave โ symmetric. Both waves always spawn;
|
| 272 |
+
# whichever wave matches the active fact's latitude is
|
| 273 |
+
# the "on-lane" horde the wall belt channels, the other
|
| 274 |
+
# is the "off-lane" wave a 2nd pbox + the wall belt
|
| 275 |
+
# together kill as they detour past.
|
| 276 |
+
- {type: e1, owner: enemy, position: [60, 26], stance: 3, count: 10}
|
| 277 |
+
- {type: e3, owner: enemy, position: [62, 26], stance: 3, count: 2}
|
| 278 |
win_condition:
|
| 279 |
all_of:
|
| 280 |
+
- {building_count_gte: {type: pbox, n: 2}}
|
| 281 |
+
- {building_count_gte: {type: brik, n: 6}}
|
| 282 |
+
- {units_killed_gte: 10}
|
| 283 |
- {building_count_gte: {type: fact, n: 1}}
|
| 284 |
- {within_ticks: 5400}
|
| 285 |
fail_condition:
|
|
@@ -1,43 +1,29 @@
|
|
| 1 |
"""def-walls-vs-towers scenario family, full loop on Rust.
|
| 2 |
|
| 3 |
-
The pack tests
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
*
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
* `building_count_gte:{fact,1}` โ the fact must still STAND (the
|
| 20 |
-
PRESENT-TENSE predicate, not `has_building:fact` which is a one-shot
|
| 21 |
-
ever-seen set โ CLAUDE.md footgun);
|
| 22 |
-
* `within_ticks` paired with `after_ticks` โ a non-finisher (stall,
|
| 23 |
-
walls-only) is a real reachable timeout LOSS (no interrupts โ each
|
| 24 |
-
step is exactly 90 ticks, so max_turns is a hard tick budget the
|
| 25 |
-
`after_ticks` deadline reliably bites in).
|
| 26 |
-
|
| 27 |
-
The rush arrives as a `scheduled_events: spawn_actors` wave at a fixed
|
| 28 |
-
tick (1800) โ after the agent has had time to build 4 gun turrets
|
| 29 |
-
serially โ so the race is fair: the intended build completes before
|
| 30 |
-
the wave, while a staller / walls-only spend still cannot satisfy the
|
| 31 |
-
kill quota.
|
| 32 |
|
| 33 |
The scripted-policy validations prove deterministically that:
|
| 34 |
|
| 35 |
-
* the intended
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
level + every hard seed โ a real LOSS, not a draw;
|
| 39 |
* the hard tier defines >=2 spawn_point groups (north fact y=14 /
|
| 40 |
-
south fact y=26) so the
|
| 41 |
"""
|
| 42 |
|
| 43 |
from __future__ import annotations
|
|
@@ -65,28 +51,33 @@ def stall(rs, C):
|
|
| 65 |
return [C.observe()]
|
| 66 |
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
def _build_and_place(rs, C, kind, cells):
|
| 69 |
-
"""
|
| 70 |
-
|
| 71 |
-
build."""
|
| 72 |
own_b = rs.get("own_buildings") or []
|
| 73 |
n = sum(1 for b in own_b if b.get("type") == kind)
|
| 74 |
if n >= len(cells):
|
| 75 |
return [C.observe()]
|
| 76 |
-
prod = rs.get("production") or []
|
| 77 |
-
prod_items = [p.get("item") for p in prod if isinstance(p, dict)]
|
| 78 |
cmds = []
|
| 79 |
-
if kind not in
|
| 80 |
cmds.append(C.build(kind))
|
| 81 |
cmds.append(C.place_building(kind, cells[n][0], cells[n][1]))
|
| 82 |
return cmds or [C.observe()]
|
| 83 |
|
| 84 |
|
| 85 |
-
def
|
| 86 |
-
"""
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
cluster must follow it."""
|
| 90 |
state = {"cells": None}
|
| 91 |
|
| 92 |
def policy(rs, C):
|
|
@@ -96,20 +87,16 @@ def make_adaptive_towers():
|
|
| 96 |
if not facts:
|
| 97 |
return [C.observe()]
|
| 98 |
fy = facts[0].get("cell_y", facts[0].get("y"))
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
state["cells"] = [(12, fy - 2), (12, fy), (12, fy + 2), (13, fy)]
|
| 102 |
-
return _build_and_place(rs, C, "gun", state["cells"])
|
| 103 |
|
| 104 |
return policy
|
| 105 |
|
| 106 |
|
| 107 |
-
def
|
| 108 |
-
"""
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
so the `units_killed_gte` clause is never satisfied and the win
|
| 112 |
-
never latches; the episode times out โ LOSS."""
|
| 113 |
state = {"cells": None}
|
| 114 |
|
| 115 |
def policy(rs, C):
|
|
@@ -119,22 +106,103 @@ def make_walls_only():
|
|
| 119 |
if not facts:
|
| 120 |
return [C.observe()]
|
| 121 |
fy = facts[0].get("cell_y", facts[0].get("y"))
|
| 122 |
-
state["cells"] =
|
| 123 |
-
(16, fy - 7 + i) for i in range(8)
|
| 124 |
-
]
|
| 125 |
return _build_and_place(rs, C, "brik", state["cells"])
|
| 126 |
|
| 127 |
return policy
|
| 128 |
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
def pure_army(rs, C):
|
| 131 |
"""PURE-ARMY: only ever train e1 โ never builds a defensive
|
| 132 |
-
building. FAILS the `building_count_gte:
|
| 133 |
home-trained rifle screen cannot out-trade the heavier rush band
|
| 134 |
either, so the fact often falls โ LOSS."""
|
| 135 |
-
|
| 136 |
-
prod_items = [p.get("item") for p in prod if isinstance(p, dict)]
|
| 137 |
-
if "e1" not in prod_items:
|
| 138 |
return [C.build("e1")]
|
| 139 |
return [C.observe()]
|
| 140 |
|
|
@@ -167,14 +235,16 @@ def test_pack_compiles_with_three_levels_and_rusher_bot():
|
|
| 167 |
assert str(bot).lower() == "rusher", (lvl, bot)
|
| 168 |
|
| 169 |
|
| 170 |
-
def
|
| 171 |
-
"""Cash is
|
| 172 |
-
|
| 173 |
-
|
|
|
|
| 174 |
pack = load_pack(PACK)
|
| 175 |
-
for lvl in LEVELS
|
| 176 |
-
|
| 177 |
-
|
|
|
|
| 178 |
|
| 179 |
|
| 180 |
def test_no_preplaced_combat_units_near_the_base():
|
|
@@ -191,7 +261,6 @@ def test_no_preplaced_combat_units_near_the_base():
|
|
| 191 |
for a in c.scenario.actors
|
| 192 |
if a.owner == "agent" and a.type == "e1"
|
| 193 |
]
|
| 194 |
-
# Every pre-placed agent e1 is a corner non-combatant.
|
| 195 |
for a in agent_units:
|
| 196 |
x, y = a.position
|
| 197 |
assert x <= 3 and (y <= 6 or y >= 34), (lvl, a.position)
|
|
@@ -199,8 +268,8 @@ def test_no_preplaced_combat_units_near_the_base():
|
|
| 199 |
|
| 200 |
def test_uses_scheduled_wave_event():
|
| 201 |
"""The rush arrives via a `scheduled_events: spawn_actors` wave so
|
| 202 |
-
the build race is fair (the wave lands after the intended
|
| 203 |
-
|
| 204 |
pack = load_pack(PACK)
|
| 205 |
for lvl in LEVELS:
|
| 206 |
c = compile_level(pack, lvl)
|
|
@@ -250,7 +319,7 @@ def test_fact_alive_clause_uses_present_tense_predicate():
|
|
| 250 |
assert fact_clauses, f"{lvl}: missing present-tense fact-alive fail clause"
|
| 251 |
|
| 252 |
|
| 253 |
-
def
|
| 254 |
"""The load-bearing clause: every level's win requires
|
| 255 |
`units_killed_gte` โ the predicate a walls-only (0-kill) spend
|
| 256 |
cannot satisfy. Without it, inert walls would win."""
|
|
@@ -266,9 +335,58 @@ def test_win_requires_a_kill_quota():
|
|
| 266 |
assert int(kill_clauses[0]["units_killed_gte"]) >= 4, lvl
|
| 267 |
|
| 268 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
def test_hard_has_two_spawn_point_groups_and_fact_flips():
|
| 270 |
"""Hard-tier contract: >=2 distinct agent spawn_point groups so the
|
| 271 |
-
fact (and therefore the
|
| 272 |
groups must define the NORTH (y=14) and SOUTH (y=26) fact pair."""
|
| 273 |
c = compile_level(load_pack(PACK), "hard")
|
| 274 |
groups = {
|
|
@@ -291,76 +409,134 @@ def test_hard_has_two_spawn_point_groups_and_fact_flips():
|
|
| 291 |
assert 2 <= x <= 126 and 2 <= y <= 38, (a.type, a.position)
|
| 292 |
|
| 293 |
|
| 294 |
-
# โโ solvency: intended
|
| 295 |
|
| 296 |
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
c = compile_level(load_pack(PACK), level)
|
| 300 |
for seed in SEEDS:
|
| 301 |
-
r = run_level(c,
|
| 302 |
assert r.outcome == "win", (
|
| 303 |
-
f"
|
| 304 |
f"got {r.outcome} (tick={r.signals.game_tick}, "
|
| 305 |
f"kills={r.signals.units_killed}, "
|
| 306 |
-
f"lost={r.signals.units_lost}, "
|
| 307 |
f"buildings={r.signals.own_buildings})"
|
| 308 |
)
|
| 309 |
|
| 310 |
|
| 311 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
|
| 313 |
|
| 314 |
@pytest.mark.parametrize("level", LEVELS)
|
| 315 |
-
|
| 316 |
-
"
|
| 317 |
-
|
| 318 |
-
("stall", lambda: stall),
|
| 319 |
-
("walls_only", make_walls_only),
|
| 320 |
-
("pure_army", lambda: pure_army),
|
| 321 |
-
],
|
| 322 |
-
)
|
| 323 |
-
def test_passive_and_lazy_policies_lose_every_level_and_seed(
|
| 324 |
-
level, policy_name, policy_factory
|
| 325 |
-
):
|
| 326 |
-
"""Stall (rush razes fact OR clock), walls-only (inert walls โ 0
|
| 327 |
-
kills, kill clause never met, clock runs out), and pure-army (gun
|
| 328 |
-
count clause unmet) must ALL LOSE on every level + every seed โ no
|
| 329 |
-
draw. walls-only LOSING is the load-bearing discrimination: a
|
| 330 |
-
passive-obstacle spend, however large, never produces kill output."""
|
| 331 |
c = compile_level(load_pack(PACK), level)
|
| 332 |
-
fn = policy_factory()
|
| 333 |
for seed in SEEDS:
|
| 334 |
-
r = run_level(c,
|
| 335 |
assert r.outcome == "loss", (
|
| 336 |
-
f"{level} seed{seed}
|
| 337 |
-
f"not a draw); got {r.outcome} (tick={r.signals.game_tick}, "
|
| 338 |
-
f"kills={r.signals.units_killed}, "
|
| 339 |
-
f"buildings={r.signals.own_buildings})"
|
| 340 |
)
|
| 341 |
|
| 342 |
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
for seed in SEEDS:
|
| 350 |
-
r = run_level(c,
|
| 351 |
-
assert r.
|
| 352 |
-
f"{lvl} seed{seed}
|
| 353 |
-
f"
|
|
|
|
| 354 |
)
|
| 355 |
|
| 356 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
# โโ determinism โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 358 |
|
| 359 |
|
| 360 |
def test_intended_run_is_deterministic_on_easy():
|
| 361 |
c = compile_level(load_pack(PACK), "easy")
|
| 362 |
-
a = run_level(c,
|
| 363 |
-
b = run_level(c,
|
| 364 |
assert (a.outcome, a.turns, a.signals.units_killed) == (
|
| 365 |
b.outcome,
|
| 366 |
b.turns,
|
|
|
|
| 1 |
"""def-walls-vs-towers scenario family, full loop on Rust.
|
| 2 |
|
| 3 |
+
The pack tests TIER-DIFFERENTIATED defensive DOCTRINE selection: the
|
| 4 |
+
agent must choose between PASSIVE OBSTACLE (`brik` concrete walls โ
|
| 5 |
+
200cr, HP 40000, NO Armament) and ACTIVE TURRET (`pbox` pillbox โ
|
| 6 |
+
600cr, M60mg burst โ anti-infantry direct fire). The correct posture
|
| 7 |
+
DIFFERS PER TIER:
|
| 8 |
+
|
| 9 |
+
* EASY โ frontal rush in the open. 4 pbox cluster wraps the fact
|
| 10 |
+
and shreds the wave. Walls kill nothing โ LOSS.
|
| 11 |
+
* MEDIUM โ overwhelming horde funnels on one lane. Win predicate
|
| 12 |
+
requires BOTH a wall belt AND a pbox at the choke AND a
|
| 13 |
+
kill quota: a wall barrier funnels the horde to a single
|
| 14 |
+
pass where the pbox serially shreds the queue. Pure-walls
|
| 15 |
+
(0 kills) and pure-pbox (no brik clause) both LOSE.
|
| 16 |
+
* HARD โ two simultaneous waves + seed-driven fact flip. The agent
|
| 17 |
+
must allocate budget across both postures (2 pbox + 8
|
| 18 |
+
brik). Pure-walls and pure-pbox both LOSE.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
The scripted-policy validations prove deterministically that:
|
| 21 |
|
| 22 |
+
* the intended PER-TIER doctrine WINS every seed (1..4);
|
| 23 |
+
* stall, pure-walls (brik-only), pure-pbox (no brik on medium/hard),
|
| 24 |
+
and pure-army all LOSE every seed โ a real LOSS, not a draw;
|
|
|
|
| 25 |
* the hard tier defines >=2 spawn_point groups (north fact y=14 /
|
| 26 |
+
south fact y=26) so the defence cluster must follow the fact.
|
| 27 |
"""
|
| 28 |
|
| 29 |
from __future__ import annotations
|
|
|
|
| 51 |
return [C.observe()]
|
| 52 |
|
| 53 |
|
| 54 |
+
def _prod_items(rs):
|
| 55 |
+
"""The Defense queue surfaces as a flat list of strings โ handle
|
| 56 |
+
that AND the legacy dict-with-item shape robustly."""
|
| 57 |
+
return [
|
| 58 |
+
(p if isinstance(p, str) else p.get("item"))
|
| 59 |
+
for p in (rs.get("production") or [])
|
| 60 |
+
]
|
| 61 |
+
|
| 62 |
+
|
| 63 |
def _build_and_place(rs, C, kind, cells):
|
| 64 |
+
"""Place the next `kind` building in `cells` if the previous one
|
| 65 |
+
finished; queue the next build only if not already in the queue."""
|
|
|
|
| 66 |
own_b = rs.get("own_buildings") or []
|
| 67 |
n = sum(1 for b in own_b if b.get("type") == kind)
|
| 68 |
if n >= len(cells):
|
| 69 |
return [C.observe()]
|
|
|
|
|
|
|
| 70 |
cmds = []
|
| 71 |
+
if kind not in _prod_items(rs):
|
| 72 |
cmds.append(C.build(kind))
|
| 73 |
cmds.append(C.place_building(kind, cells[n][0], cells[n][1]))
|
| 74 |
return cmds or [C.observe()]
|
| 75 |
|
| 76 |
|
| 77 |
+
def make_pbox_only(cells_fn):
|
| 78 |
+
"""Pure-ACTIVE policy: build N pbox at `cells_fn(fy)`. Wraps the
|
| 79 |
+
fact with active turrets that shoot the rush. Wins on easy
|
| 80 |
+
(predicate matches) and LOSES on medium/hard (no brik clause)."""
|
|
|
|
| 81 |
state = {"cells": None}
|
| 82 |
|
| 83 |
def policy(rs, C):
|
|
|
|
| 87 |
if not facts:
|
| 88 |
return [C.observe()]
|
| 89 |
fy = facts[0].get("cell_y", facts[0].get("y"))
|
| 90 |
+
state["cells"] = cells_fn(fy)
|
| 91 |
+
return _build_and_place(rs, C, "pbox", state["cells"])
|
|
|
|
|
|
|
| 92 |
|
| 93 |
return policy
|
| 94 |
|
| 95 |
|
| 96 |
+
def make_brik_only(cells_fn):
|
| 97 |
+
"""Pure-PASSIVE policy: build N brik (inert walls). The brik clause
|
| 98 |
+
may pass on medium/hard but `units_killed_gte` never does โ walls
|
| 99 |
+
have no weapon. LOSES every tier."""
|
|
|
|
|
|
|
| 100 |
state = {"cells": None}
|
| 101 |
|
| 102 |
def policy(rs, C):
|
|
|
|
| 106 |
if not facts:
|
| 107 |
return [C.observe()]
|
| 108 |
fy = facts[0].get("cell_y", facts[0].get("y"))
|
| 109 |
+
state["cells"] = cells_fn(fy)
|
|
|
|
|
|
|
| 110 |
return _build_and_place(rs, C, "brik", state["cells"])
|
| 111 |
|
| 112 |
return policy
|
| 113 |
|
| 114 |
|
| 115 |
+
def make_intended_easy():
|
| 116 |
+
"""EASY doctrine: 4 pbox wrapping the fact on the east approach."""
|
| 117 |
+
return make_pbox_only(
|
| 118 |
+
lambda fy: [(12, fy - 2), (12, fy), (12, fy + 2), (13, fy)]
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def _build_combo(rs, C, plan):
|
| 123 |
+
"""Two-phase build: complete all pbox first, then the brik belt.
|
| 124 |
+
Pbox first because they kill โ building the wall belt before the
|
| 125 |
+
pbox lets the horde reach the fact before kill output is online."""
|
| 126 |
+
own_b = rs.get("own_buildings") or []
|
| 127 |
+
n_pbox = sum(1 for b in own_b if b.get("type") == "pbox")
|
| 128 |
+
n_brik = sum(1 for b in own_b if b.get("type") == "brik")
|
| 129 |
+
prod_items = _prod_items(rs)
|
| 130 |
+
cmds = []
|
| 131 |
+
if n_pbox < len(plan["pbox"]):
|
| 132 |
+
if "pbox" not in prod_items:
|
| 133 |
+
cmds.append(C.build("pbox"))
|
| 134 |
+
cmds.append(C.place_building("pbox", plan["pbox"][n_pbox][0], plan["pbox"][n_pbox][1]))
|
| 135 |
+
elif n_brik < len(plan["brik"]):
|
| 136 |
+
if "brik" not in prod_items:
|
| 137 |
+
cmds.append(C.build("brik"))
|
| 138 |
+
cmds.append(C.place_building("brik", plan["brik"][n_brik][0], plan["brik"][n_brik][1]))
|
| 139 |
+
else:
|
| 140 |
+
cmds.append(C.observe())
|
| 141 |
+
return cmds
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def make_intended_medium():
|
| 145 |
+
"""MEDIUM doctrine: 1 pbox at the choke + 8 brik wall belt that
|
| 146 |
+
leaves a single-cell pass at the fact's y, funnelling the horde
|
| 147 |
+
onto the pbox burst."""
|
| 148 |
+
state = {"plan": None}
|
| 149 |
+
|
| 150 |
+
def policy(rs, C):
|
| 151 |
+
if state["plan"] is None:
|
| 152 |
+
own_b = rs.get("own_buildings") or []
|
| 153 |
+
facts = [b for b in own_b if b.get("type") == "fact"]
|
| 154 |
+
if not facts:
|
| 155 |
+
return [C.observe()]
|
| 156 |
+
fy = facts[0].get("cell_y", facts[0].get("y"))
|
| 157 |
+
state["plan"] = {
|
| 158 |
+
"pbox": [(12, fy)],
|
| 159 |
+
"brik": [
|
| 160 |
+
(14, fy - 4), (14, fy - 3), (14, fy - 2), (14, fy - 1),
|
| 161 |
+
(14, fy + 1), (14, fy + 2), (14, fy + 3), (14, fy + 4),
|
| 162 |
+
],
|
| 163 |
+
}
|
| 164 |
+
return _build_combo(rs, C, state["plan"])
|
| 165 |
+
|
| 166 |
+
return policy
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def make_intended_hard():
|
| 170 |
+
"""HARD doctrine: 2 pbox at the choke + 8 brik full-lane wall belt
|
| 171 |
+
that blocks BOTH attack vectors converging on the fact. The pbox
|
| 172 |
+
cluster sits behind the wall belt where it can shred whatever
|
| 173 |
+
funnels through the single-cell gap left by the wall."""
|
| 174 |
+
state = {"plan": None}
|
| 175 |
+
|
| 176 |
+
def policy(rs, C):
|
| 177 |
+
if state["plan"] is None:
|
| 178 |
+
own_b = rs.get("own_buildings") or []
|
| 179 |
+
facts = [b for b in own_b if b.get("type") == "fact"]
|
| 180 |
+
if not facts:
|
| 181 |
+
return [C.observe()]
|
| 182 |
+
fy = facts[0].get("cell_y", facts[0].get("y"))
|
| 183 |
+
# 2nd pbox is placed on the FACT side (north for the north
|
| 184 |
+
# fact, south for the south fact) to cover the off-lane
|
| 185 |
+
# approach as it walks past.
|
| 186 |
+
second_y = fy - 2 if fy == 14 else fy + 2
|
| 187 |
+
state["plan"] = {
|
| 188 |
+
"pbox": [(12, fy), (12, second_y)],
|
| 189 |
+
"brik": [
|
| 190 |
+
(13, fy - 3), (13, fy - 2), (13, fy - 1),
|
| 191 |
+
(13, fy + 1), (13, fy + 2), (13, fy + 3),
|
| 192 |
+
(13, fy + 4), (13, fy - 4),
|
| 193 |
+
],
|
| 194 |
+
}
|
| 195 |
+
return _build_combo(rs, C, state["plan"])
|
| 196 |
+
|
| 197 |
+
return policy
|
| 198 |
+
|
| 199 |
+
|
| 200 |
def pure_army(rs, C):
|
| 201 |
"""PURE-ARMY: only ever train e1 โ never builds a defensive
|
| 202 |
+
building. FAILS the `building_count_gte:pbox` clause; a thin
|
| 203 |
home-trained rifle screen cannot out-trade the heavier rush band
|
| 204 |
either, so the fact often falls โ LOSS."""
|
| 205 |
+
if "e1" not in _prod_items(rs):
|
|
|
|
|
|
|
| 206 |
return [C.build("e1")]
|
| 207 |
return [C.observe()]
|
| 208 |
|
|
|
|
| 235 |
assert str(bot).lower() == "rusher", (lvl, bot)
|
| 236 |
|
| 237 |
|
| 238 |
+
def test_starting_cash_is_per_tier_doctrine_budget():
|
| 239 |
+
"""Cash is sized per tier so the intended doctrine fits and the
|
| 240 |
+
wrong doctrine does not: easy/medium fund either 4 pbox (4ร600) or
|
| 241 |
+
12 brik (12ร200) within 2400cr; hard funds 2 pbox + 8 brik = 2800
|
| 242 |
+
within 3000cr."""
|
| 243 |
pack = load_pack(PACK)
|
| 244 |
+
cash_by_level = {lvl: compile_level(pack, lvl).starting_cash for lvl in LEVELS}
|
| 245 |
+
assert cash_by_level["easy"] == 2400, cash_by_level
|
| 246 |
+
assert cash_by_level["medium"] == 2400, cash_by_level
|
| 247 |
+
assert cash_by_level["hard"] == 3000, cash_by_level
|
| 248 |
|
| 249 |
|
| 250 |
def test_no_preplaced_combat_units_near_the_base():
|
|
|
|
| 261 |
for a in c.scenario.actors
|
| 262 |
if a.owner == "agent" and a.type == "e1"
|
| 263 |
]
|
|
|
|
| 264 |
for a in agent_units:
|
| 265 |
x, y = a.position
|
| 266 |
assert x <= 3 and (y <= 6 or y >= 34), (lvl, a.position)
|
|
|
|
| 268 |
|
| 269 |
def test_uses_scheduled_wave_event():
|
| 270 |
"""The rush arrives via a `scheduled_events: spawn_actors` wave so
|
| 271 |
+
the build race is fair (the wave lands after the intended build
|
| 272 |
+
can complete)."""
|
| 273 |
pack = load_pack(PACK)
|
| 274 |
for lvl in LEVELS:
|
| 275 |
c = compile_level(pack, lvl)
|
|
|
|
| 319 |
assert fact_clauses, f"{lvl}: missing present-tense fact-alive fail clause"
|
| 320 |
|
| 321 |
|
| 322 |
+
def test_win_requires_a_kill_quota_per_tier():
|
| 323 |
"""The load-bearing clause: every level's win requires
|
| 324 |
`units_killed_gte` โ the predicate a walls-only (0-kill) spend
|
| 325 |
cannot satisfy. Without it, inert walls would win."""
|
|
|
|
| 335 |
assert int(kill_clauses[0]["units_killed_gte"]) >= 4, lvl
|
| 336 |
|
| 337 |
|
| 338 |
+
def test_medium_and_hard_require_wall_plus_tower_combo():
|
| 339 |
+
"""Tier-differentiated doctrine: medium/hard predicates must require
|
| 340 |
+
BOTH a pbox AND a brik clause. Without the brik clause a pure-pbox
|
| 341 |
+
spend would win medium/hard and the tier doctrine would collapse to
|
| 342 |
+
"always build towers"."""
|
| 343 |
+
for lvl in ("medium", "hard"):
|
| 344 |
+
c = compile_level(load_pack(PACK), lvl)
|
| 345 |
+
wc = c.win_condition.model_dump(exclude_none=True)
|
| 346 |
+
all_of = wc.get("all_of") or []
|
| 347 |
+
pbox_clauses = [
|
| 348 |
+
cl for cl in all_of
|
| 349 |
+
if isinstance(cl, dict)
|
| 350 |
+
and (cl.get("building_count_gte") or {}).get("type") == "pbox"
|
| 351 |
+
]
|
| 352 |
+
brik_clauses = [
|
| 353 |
+
cl for cl in all_of
|
| 354 |
+
if isinstance(cl, dict)
|
| 355 |
+
and (cl.get("building_count_gte") or {}).get("type") == "brik"
|
| 356 |
+
]
|
| 357 |
+
assert pbox_clauses, f"{lvl}: missing pbox count clause"
|
| 358 |
+
assert brik_clauses, f"{lvl}: missing brik count clause"
|
| 359 |
+
# The brik count must require a genuine wall belt, not 1-2
|
| 360 |
+
# incidental walls โ otherwise a 4-pbox spend that happens to
|
| 361 |
+
# toss out a couple of walls passes the predicate.
|
| 362 |
+
assert int(brik_clauses[0]["building_count_gte"]["n"]) >= 6, lvl
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
def test_easy_requires_pbox_not_brik():
|
| 366 |
+
"""Easy tier is the pure-active-doctrine tier: the predicate must
|
| 367 |
+
require pbox and NOT require any brik (a 4-pbox spend is the
|
| 368 |
+
intended easy win, and forcing brik on easy would collapse the
|
| 369 |
+
tier differentiation)."""
|
| 370 |
+
c = compile_level(load_pack(PACK), "easy")
|
| 371 |
+
wc = c.win_condition.model_dump(exclude_none=True)
|
| 372 |
+
all_of = wc.get("all_of") or []
|
| 373 |
+
pbox_clauses = [
|
| 374 |
+
cl for cl in all_of
|
| 375 |
+
if isinstance(cl, dict)
|
| 376 |
+
and (cl.get("building_count_gte") or {}).get("type") == "pbox"
|
| 377 |
+
]
|
| 378 |
+
brik_clauses = [
|
| 379 |
+
cl for cl in all_of
|
| 380 |
+
if isinstance(cl, dict)
|
| 381 |
+
and (cl.get("building_count_gte") or {}).get("type") == "brik"
|
| 382 |
+
]
|
| 383 |
+
assert pbox_clauses, "easy: missing pbox count clause"
|
| 384 |
+
assert not brik_clauses, "easy: must not require brik (pure-active doctrine)"
|
| 385 |
+
|
| 386 |
+
|
| 387 |
def test_hard_has_two_spawn_point_groups_and_fact_flips():
|
| 388 |
"""Hard-tier contract: >=2 distinct agent spawn_point groups so the
|
| 389 |
+
fact (and therefore the defence cluster) flips by seed. The two
|
| 390 |
groups must define the NORTH (y=14) and SOUTH (y=26) fact pair."""
|
| 391 |
c = compile_level(load_pack(PACK), "hard")
|
| 392 |
groups = {
|
|
|
|
| 409 |
assert 2 <= x <= 126 and 2 <= y <= 38, (a.type, a.position)
|
| 410 |
|
| 411 |
|
| 412 |
+
# โโ solvency: intended per-tier doctrine wins every seed โโโโโโโโโโโโโ
|
| 413 |
|
| 414 |
|
| 415 |
+
def test_intended_easy_pbox_wins_every_seed():
|
| 416 |
+
c = compile_level(load_pack(PACK), "easy")
|
|
|
|
| 417 |
for seed in SEEDS:
|
| 418 |
+
r = run_level(c, make_intended_easy(), seed=seed)
|
| 419 |
assert r.outcome == "win", (
|
| 420 |
+
f"easy seed{seed}: intended 4-pbox cluster must WIN; "
|
| 421 |
f"got {r.outcome} (tick={r.signals.game_tick}, "
|
| 422 |
f"kills={r.signals.units_killed}, "
|
|
|
|
| 423 |
f"buildings={r.signals.own_buildings})"
|
| 424 |
)
|
| 425 |
|
| 426 |
|
| 427 |
+
def test_intended_medium_walls_plus_tower_wins_every_seed():
|
| 428 |
+
c = compile_level(load_pack(PACK), "medium")
|
| 429 |
+
for seed in SEEDS:
|
| 430 |
+
r = run_level(c, make_intended_medium(), seed=seed)
|
| 431 |
+
assert r.outcome == "win", (
|
| 432 |
+
f"medium seed{seed}: intended 1 pbox + 8 brik choke must "
|
| 433 |
+
f"WIN; got {r.outcome} (tick={r.signals.game_tick}, "
|
| 434 |
+
f"kills={r.signals.units_killed}, "
|
| 435 |
+
f"buildings={r.signals.own_buildings})"
|
| 436 |
+
)
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
def test_intended_hard_mixed_doctrine_wins_every_seed():
|
| 440 |
+
c = compile_level(load_pack(PACK), "hard")
|
| 441 |
+
for seed in SEEDS:
|
| 442 |
+
r = run_level(c, make_intended_hard(), seed=seed)
|
| 443 |
+
assert r.outcome == "win", (
|
| 444 |
+
f"hard seed{seed}: intended 2 pbox + 8 brik mixed doctrine "
|
| 445 |
+
f"must WIN; got {r.outcome} (tick={r.signals.game_tick}, "
|
| 446 |
+
f"kills={r.signals.units_killed}, "
|
| 447 |
+
f"buildings={r.signals.own_buildings})"
|
| 448 |
+
)
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
# โโ no-cheat: stall + wrong doctrine all LOSE every tier ร seed โโโโโโ
|
| 452 |
|
| 453 |
|
| 454 |
@pytest.mark.parametrize("level", LEVELS)
|
| 455 |
+
def test_stall_loses_every_level_and_seed(level):
|
| 456 |
+
"""Stall (observe-only): the rush razes the fact OR the clock runs
|
| 457 |
+
out โ real LOSS, not a draw."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
c = compile_level(load_pack(PACK), level)
|
|
|
|
| 459 |
for seed in SEEDS:
|
| 460 |
+
r = run_level(c, stall, seed=seed)
|
| 461 |
assert r.outcome == "loss", (
|
| 462 |
+
f"{level} seed{seed} stall: must LOSE; got {r.outcome}"
|
|
|
|
|
|
|
|
|
|
| 463 |
)
|
| 464 |
|
| 465 |
|
| 466 |
+
@pytest.mark.parametrize("level", LEVELS)
|
| 467 |
+
def test_pure_walls_loses_every_level_and_seed(level):
|
| 468 |
+
"""Pure-walls (brik-only): walls have no weapon, so `units_killed`
|
| 469 |
+
stays at zero on every tier โ the kill clause is never satisfied
|
| 470 |
+
and the episode times out โ LOSS. The mechanism-level proof that
|
| 471 |
+
passive obstruction alone is never the right doctrine for ANY
|
| 472 |
+
tier (even where walls are PART of the right doctrine)."""
|
| 473 |
+
c = compile_level(load_pack(PACK), level)
|
| 474 |
+
pol = make_brik_only(lambda fy: [(14, fy - 6 + i) for i in range(12) if fy - 6 + i != fy])
|
| 475 |
+
for seed in SEEDS:
|
| 476 |
+
r = run_level(c, pol, seed=seed)
|
| 477 |
+
assert r.outcome == "loss", (
|
| 478 |
+
f"{level} seed{seed} pure-walls: must LOSE; got {r.outcome} "
|
| 479 |
+
f"(kills={r.signals.units_killed})"
|
| 480 |
+
)
|
| 481 |
+
# Mechanism check: pure-walls always kills zero, on every tier.
|
| 482 |
+
assert r.signals.units_killed == 0, (
|
| 483 |
+
f"{level} seed{seed}: pure-walls must kill 0 (inert "
|
| 484 |
+
f"obstacle); got {r.signals.units_killed}"
|
| 485 |
+
)
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
def test_pure_pbox_wins_easy_but_loses_medium_and_hard():
|
| 489 |
+
"""The TIER-DIFFERENTIATION proof: a 4-pbox cluster (the easy
|
| 490 |
+
doctrine, hard-coded as always-towers) WINS easy but LOSES
|
| 491 |
+
medium and hard โ proving the predicate / geometry genuinely
|
| 492 |
+
forces a different doctrine per tier. An "always build towers"
|
| 493 |
+
play that memorises easy fails the higher tiers."""
|
| 494 |
+
pack = load_pack(PACK)
|
| 495 |
+
|
| 496 |
+
def cells(fy):
|
| 497 |
+
return [(12, fy - 2), (12, fy), (12, fy + 2), (13, fy)]
|
| 498 |
+
|
| 499 |
+
# Easy โ pure-pbox is the correct doctrine, must WIN.
|
| 500 |
+
c_easy = compile_level(pack, "easy")
|
| 501 |
+
for seed in SEEDS:
|
| 502 |
+
r = run_level(c_easy, make_pbox_only(cells), seed=seed)
|
| 503 |
+
assert r.outcome == "win", (
|
| 504 |
+
f"easy seed{seed} pure-pbox: must WIN (the easy doctrine); "
|
| 505 |
+
f"got {r.outcome}"
|
| 506 |
+
)
|
| 507 |
+
|
| 508 |
+
# Medium + hard โ pure-pbox fails the brik clause, must LOSE.
|
| 509 |
+
for lvl in ("medium", "hard"):
|
| 510 |
+
c = compile_level(pack, lvl)
|
| 511 |
for seed in SEEDS:
|
| 512 |
+
r = run_level(c, make_pbox_only(cells), seed=seed)
|
| 513 |
+
assert r.outcome == "loss", (
|
| 514 |
+
f"{lvl} seed{seed} pure-pbox: must LOSE (wrong "
|
| 515 |
+
f"doctrine โ no wall belt); got {r.outcome} "
|
| 516 |
+
f"(kills={r.signals.units_killed})"
|
| 517 |
)
|
| 518 |
|
| 519 |
|
| 520 |
+
@pytest.mark.parametrize("level", LEVELS)
|
| 521 |
+
def test_pure_army_loses_every_level_and_seed(level):
|
| 522 |
+
"""Pure-army (only train e1): fails the pbox count clause and a
|
| 523 |
+
thin home-trained rifle screen cannot out-trade the heavier rush
|
| 524 |
+
band, so the fact often falls โ LOSS."""
|
| 525 |
+
c = compile_level(load_pack(PACK), level)
|
| 526 |
+
for seed in SEEDS:
|
| 527 |
+
r = run_level(c, pure_army, seed=seed)
|
| 528 |
+
assert r.outcome == "loss", (
|
| 529 |
+
f"{level} seed{seed} pure-army: must LOSE; got {r.outcome}"
|
| 530 |
+
)
|
| 531 |
+
|
| 532 |
+
|
| 533 |
# โโ determinism โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 534 |
|
| 535 |
|
| 536 |
def test_intended_run_is_deterministic_on_easy():
|
| 537 |
c = compile_level(load_pack(PACK), "easy")
|
| 538 |
+
a = run_level(c, make_intended_easy(), seed=3)
|
| 539 |
+
b = run_level(c, make_intended_easy(), seed=3)
|
| 540 |
assert (a.outcome, a.turns, a.signals.units_killed) == (
|
| 541 |
b.outcome,
|
| 542 |
b.turns,
|
|
@@ -746,19 +746,22 @@ UPGRADED = [
|
|
| 746 |
# selected. MicroRTS pillbox placement / military quadrant
|
| 747 |
# doctrine anchor.
|
| 748 |
"build-defensive-skirt-corners",
|
| 749 |
-
# Wave-11 REASONING pack:
|
| 750 |
-
#
|
| 751 |
-
#
|
| 752 |
-
#
|
| 753 |
-
# walls
|
| 754 |
-
#
|
| 755 |
-
#
|
| 756 |
-
#
|
| 757 |
-
#
|
| 758 |
-
#
|
| 759 |
-
#
|
| 760 |
-
#
|
| 761 |
-
#
|
|
|
|
|
|
|
|
|
|
| 762 |
"def-walls-vs-towers",
|
| 763 |
# Wave-7 REASONING asymmetric-underdog pack โ SC2 asymmetric /
|
| 764 |
# guerrilla tactics / asymmetric warfare anchor. 2ร 2tnk vs 4ร e1
|
|
|
|
| 746 |
# selected. MicroRTS pillbox placement / military quadrant
|
| 747 |
# doctrine anchor.
|
| 748 |
"build-defensive-skirt-corners",
|
| 749 |
+
# Wave-11 REASONING pack: tier-differentiated defensive DOCTRINE
|
| 750 |
+
# โ passive `brik` walls (200cr, no Armament) vs active `pbox`
|
| 751 |
+
# pillboxes (600cr, M60mg burst). The CORRECT doctrine differs
|
| 752 |
+
# per tier: easy = pure-pbox (frontal rush in the open); medium =
|
| 753 |
+
# walls+pbox CHOKE (overwhelming horde funnels through a single
|
| 754 |
+
# gap onto the pbox burst); hard = MIXED 2 pbox + 8 brik with a
|
| 755 |
+
# seed-driven fact flip. The win predicate requires `pbox` count,
|
| 756 |
+
# `brik` count (medium/hard only), AND a kill quota โ so pure-
|
| 757 |
+
# walls (0 kills) and pure-pbox (no brik) both LOSE on medium/hard
|
| 758 |
+
# while still winning easy. Hard defines two agent spawn_point
|
| 759 |
+
# groups (NORTH fact y=14 / SOUTH fact y=26) round-robined by
|
| 760 |
+
# seed; the scheduled rush waves are staged at BOTH candidate
|
| 761 |
+
# latitudes so the rusher converges on whichever fact the seed
|
| 762 |
+
# selected, and a memorised "wall belt near (10,20)" plan cannot
|
| 763 |
+
# generalise. Security architecture passive-vs-active mitigation
|
| 764 |
+
# / military fortification anchor.
|
| 765 |
"def-walls-vs-towers",
|
| 766 |
# Wave-7 REASONING asymmetric-underdog pack โ SC2 asymmetric /
|
| 767 |
# guerrilla tactics / asymmetric warfare anchor. 2ร 2tnk vs 4ร e1
|