Spaces:
Running
def-tower-line-vs-cluster: per-tier maps that match topology to threat geometry
Browse filesRedesign the pack so each tier has its own forcing geometry and demands
a different topology:
easy โ open arena 112x40, 4 stance:3 squads on rows y=4/12/28/36
with per-row stance:0 decoys at (12,ROW). Squads spawn at
tick 2000 via scheduled_events AFTER all 4 pboxes are
built. LINE wins (1 pbox per row at x=60); CLUSTER fails
every LINE-rung region clause (y=20 cluster is 8+ cells
from every attacker row).
medium โ chokepoint-arena 112x40 with one 5-cell corridor at
y=18..22 x=60. 10 stance:3 attackers (8 e1 + 2 e3) funnel
through. CLUSTER (4 pbox in the corridor disc) wins;
LINE attempts at off-corridor rows are rejected by the
engine (wall water) -> count clause fails.
hard โ open arena 112x40 with TWO enemy spawn_point groups:
spawn_point 0 = concentrated stack at (90,20) (cluster
wins), spawn_point 1 = stance:0 squads parked at
(63,ROW) just inside pbox range-4 (line wins). Win
predicate uses any_of over cluster bar / line bar +
units_killed_gte:8 as the load-bearing discriminator.
Per-tier custom maps are materialized via generator specs inside
each level's overrides.base_map (arena / chokepoint-arena / arena).
Scripted-policy bar (4 seeds x 3 tiers, no model needed):
stall (observe only) : LOSS x 12 (all tiers, all seeds)
pure_army (no pbox) : LOSS x 12 (count clause)
wrong-topology : LOSS x 12 on easy+medium (region or
count clause); LOSS on the unmatched
seed on hard (kill quota)
intended topology : WIN x 4 easy, WIN x 4 medium,
WIN x 2 hard per topology (matched
seed) -> WIN coverage of all 4 hard
seeds across the two topologies
Tests are updated end-to-end (tests/test_def_tower_line_vs_cluster.py):
new lazy/wrong-topology/intended assertions per tier, hard-tier
spawn-point check switched to the enemy-side axis. Hard-tier
catalogue comment in tests/test_hard_tier.py updated to reflect the
enemy-side spawn axis.
No engine changes.
|
Binary file (886 Bytes). View file
|
|
|
|
Binary file (881 Bytes). View file
|
|
|
|
Binary file (929 Bytes). View file
|
|
|
|
@@ -1,49 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
meta:
|
| 2 |
id: def-tower-line-vs-cluster
|
| 3 |
-
title: 'Defense Topology โ
|
| 4 |
capability: reasoning
|
| 5 |
real_world_meaning: >
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
robotics_analogue: >
|
| 16 |
-
Firewall /
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
| 23 |
benchmark_anchor:
|
| 24 |
- "graph theory min-cut (concentrate defenses at chokepoints)"
|
| 25 |
- "military bunker placement doctrine"
|
| 26 |
- "firewall topology: dense at chokepoints"
|
|
|
|
| 27 |
- "Lanchester defense concentration"
|
| 28 |
author: openra-bench
|
| 29 |
|
| 30 |
-
#
|
| 31 |
-
#
|
| 32 |
-
# centroid (the agent fact on the west), which forces its path THROUGH
|
| 33 |
-
# that lane mouth on every seed. Agent base is pre-placed at the west
|
| 34 |
-
# (fact + tent + powr pre-built so the pbox queue is ready turn 1).
|
| 35 |
-
# Pack-level cash is overridden per-level (3/4/5 pbox at 600cr each).
|
| 36 |
-
# An unarmed high-HP enemy `fact` far east keeps the engine alive past
|
| 37 |
-
# any rusher annihilation so the win/fail check actually runs.
|
| 38 |
base_map: rush-hour-arena
|
| 39 |
-
starting_cash: 2400
|
| 40 |
|
| 41 |
base:
|
| 42 |
-
agent:
|
| 43 |
-
|
| 44 |
-
enemy:
|
| 45 |
-
faction: soviet
|
| 46 |
-
bot_type: rusher
|
| 47 |
tools:
|
| 48 |
- observe
|
| 49 |
- build
|
|
@@ -53,75 +105,122 @@ base:
|
|
| 53 |
- attack_move
|
| 54 |
- stop
|
| 55 |
planning: true
|
| 56 |
-
# No interrupts โ
|
| 57 |
-
#
|
| 58 |
-
#
|
| 59 |
-
#
|
| 60 |
-
#
|
| 61 |
-
|
| 62 |
termination:
|
| 63 |
max_ticks: 12000
|
| 64 |
-
actors:
|
| 65 |
-
# Pre-placed agent base on the WEST (so rusher path is forced
|
| 66 |
-
# through the mid-map choke on the way to the fact centroid).
|
| 67 |
-
- {type: fact, owner: agent, position: [10, 20]}
|
| 68 |
-
- {type: tent, owner: agent, position: [14, 18]}
|
| 69 |
-
- {type: powr, owner: agent, position: [14, 22]}
|
| 70 |
|
| 71 |
levels:
|
| 72 |
-
# โโ EASY โโ
|
| 73 |
-
#
|
| 74 |
-
#
|
| 75 |
-
#
|
| 76 |
-
#
|
| 77 |
-
#
|
| 78 |
-
#
|
| 79 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
easy:
|
| 81 |
-
# Original (pre-verbosity-sweep) description preserved for
|
| 82 |
-
# contributors. The trimmed version below removes scripted-policy
|
| 83 |
-
# spoilers and cell-coord dumps; load-bearing intent kept.
|
| 84 |
-
#
|
| 85 |
-
# A rusher band of 5 rifle infantry must traverse the narrow lane at
|
| 86 |
-
# (60, 18..22) to reach your base on the west. Build 3 pillboxes (pbox
|
| 87 |
-
# โ 600cr each, budget exactly 1800) AND place ALL 3 inside the choke
|
| 88 |
-
# at (60, 20) within radius 5, so the rusher walks into overlapping
|
| 89 |
-
# fields of fire at the bottleneck. A spread line (any pbox away from
|
| 90 |
-
# the choke) cannot satisfy both the count and the region bars at
|
| 91 |
-
# once. Stalling, pure-army (no pbox), and spread layouts all lose.
|
| 92 |
-
# Your fact must survive.
|
| 93 |
description: >
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
| 100 |
overrides:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
actors:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
- {type: fact, owner: agent, position: [10, 20]}
|
| 103 |
- {type: tent, owner: agent, position: [14, 18]}
|
| 104 |
- {type: powr, owner: agent, position: [14, 22]}
|
| 105 |
-
#
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
#
|
| 111 |
-
#
|
| 112 |
-
#
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
- {type: e1, owner: enemy, position: [100, 20], stance: 3, count: 5}
|
| 118 |
# Unarmed high-HP marker (anti-DRAW): keeps the episode alive
|
| 119 |
-
# past full
|
| 120 |
-
- {type: fact, owner: enemy, position: [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
win_condition:
|
| 122 |
all_of:
|
| 123 |
-
- building_count_gte: {type: pbox, n:
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
- building_count_gte: {type: fact, n: 1}
|
| 126 |
- within_ticks: 5400
|
| 127 |
fail_condition:
|
|
@@ -130,53 +229,68 @@ levels:
|
|
| 130 |
- not: {building_count_gte: {type: fact, n: 1}}
|
| 131 |
max_turns: 60
|
| 132 |
|
| 133 |
-
# โโ MEDIUM โโ
|
| 134 |
-
#
|
| 135 |
-
#
|
| 136 |
-
#
|
| 137 |
-
#
|
| 138 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
medium:
|
| 140 |
-
# Original (pre-verbosity-sweep) description preserved for
|
| 141 |
-
# contributors. The trimmed version below removes scripted-policy
|
| 142 |
-
# spoilers and cell-coord dumps; load-bearing intent kept.
|
| 143 |
-
#
|
| 144 |
-
# Same forced rusher path through the (60, 20) choke. Build 4
|
| 145 |
-
# pillboxes (budget 2400cr = exactly 4 pbox at 600 each) AND place at
|
| 146 |
-
# least 3 of them inside the choke region (radius 5 around (60, 20)).
|
| 147 |
-
# A spread-line (1 at the choke, 3 elsewhere) meets the count but
|
| 148 |
-
# FAILS the region bar; the intended cluster (3 at the choke + 1
|
| 149 |
-
# spare) wins. Stalling, pure-army, and spread-line layouts all lose;
|
| 150 |
-
# the fact must survive.
|
| 151 |
description: >
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
| 157 |
starting_cash: 2400
|
| 158 |
overrides:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
actors:
|
| 160 |
- {type: fact, owner: agent, position: [10, 20]}
|
| 161 |
- {type: tent, owner: agent, position: [14, 18]}
|
| 162 |
- {type: powr, owner: agent, position: [14, 22]}
|
| 163 |
-
#
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
- {type: e1, owner: agent, position: [
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
#
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
#
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
- {type: fact, owner: enemy, position: [120, 20]}
|
| 176 |
win_condition:
|
| 177 |
all_of:
|
| 178 |
- building_count_gte: {type: pbox, n: 4}
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
- building_count_gte: {type: fact, n: 1}
|
| 181 |
- within_ticks: 5400
|
| 182 |
fail_condition:
|
|
@@ -185,74 +299,101 @@ levels:
|
|
| 185 |
- not: {building_count_gte: {type: fact, n: 1}}
|
| 186 |
max_turns: 60
|
| 187 |
|
| 188 |
-
# โโ HARD โโ
|
| 189 |
-
#
|
| 190 |
-
#
|
| 191 |
-
#
|
| 192 |
-
#
|
| 193 |
-
#
|
| 194 |
-
#
|
| 195 |
-
#
|
| 196 |
-
#
|
| 197 |
-
#
|
| 198 |
-
#
|
| 199 |
-
#
|
| 200 |
-
#
|
| 201 |
-
#
|
| 202 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
hard:
|
| 204 |
-
# Original (pre-verbosity-sweep) description preserved for
|
| 205 |
-
# contributors. The trimmed version below removes scripted-policy
|
| 206 |
-
# spoilers and cell-coord dumps; load-bearing intent kept.
|
| 207 |
-
#
|
| 208 |
-
# Agent base latitude flips between NORTH (y=10) and SOUTH (y=30) by
|
| 209 |
-
# seed. Build 5 pillboxes (budget 3000cr = exactly 5 pbox at 600 each)
|
| 210 |
-
# AND place at least 4 of them inside ONE of the two candidate choke
|
| 211 |
-
# regions (radius 5 around either (60, 20) or (40, 20)). Spreading
|
| 212 |
-
# across both chokes (e.g. 3+2) FAILS the 4-of-5 cluster bar; a thin
|
| 213 |
-
# perimeter line FAILS both choke bars. The fact must survive.
|
| 214 |
description: >
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
overrides:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
actors:
|
| 223 |
-
#
|
| 224 |
-
#
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
- {type:
|
| 228 |
-
- {type:
|
| 229 |
-
- {type:
|
| 230 |
-
- {type: e1,
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
#
|
| 234 |
-
- {type:
|
| 235 |
-
- {type:
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
-
|
| 242 |
-
#
|
| 243 |
-
#
|
| 244 |
-
#
|
| 245 |
-
#
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
#
|
| 249 |
-
- {type:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
win_condition:
|
| 251 |
all_of:
|
| 252 |
-
- building_count_gte: {type: pbox, n:
|
| 253 |
- any_of:
|
|
|
|
| 254 |
- building_in_region: {type: pbox, x: 60, y: 20, radius: 5, count: 4}
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
- building_count_gte: {type: fact, n: 1}
|
| 257 |
- within_ticks: 6300
|
| 258 |
fail_condition:
|
|
|
|
| 1 |
+
# def-tower-line-vs-cluster โ Choose LINE or CLUSTER by Threat Geometry
|
| 2 |
+
#
|
| 3 |
+
# REASONING focus: defense topology MUST MATCH attacker forcing geometry.
|
| 4 |
+
# โข A WIDE-FRONT attack (multiple squads spread across the map width)
|
| 5 |
+
# demands a LINE of towers โ one per attacker row โ so no squad
|
| 6 |
+
# slips past on an uncovered row. A single dense cluster covers
|
| 7 |
+
# only its centre row and leaves the flanks open.
|
| 8 |
+
# โข A CONCENTRATED THRUST through ONE chokepoint demands a CLUSTER
|
| 9 |
+
# of towers AT the choke โ overlapping fields of fire shred the
|
| 10 |
+
# funnelled column in passing. A thin LINE spread along the
|
| 11 |
+
# perimeter wastes coverage on lanes the enemy never uses, and
|
| 12 |
+
# the choke itself ends up under-gunned.
|
| 13 |
+
#
|
| 14 |
+
# Each tier presents a DIFFERENT forcing geometry (per-tier map):
|
| 15 |
+
# easy โ open arena 112x40, 4 attacker squads on 4 separate rows
|
| 16 |
+
# (y=4/12/28/36), each squad walks straight west toward
|
| 17 |
+
# its own per-row decoy โ LINE wins, CLUSTER fails (every
|
| 18 |
+
# attacker row is out of weapon range from y=20).
|
| 19 |
+
# medium โ chokepoint-arena 112x40 with a single corridor at y=18..22
|
| 20 |
+
# x=60 โ CLUSTER wins, LINE spreads thin (line attempts at
|
| 21 |
+
# rows y=8/16/24/32 land on water and never build โ count
|
| 22 |
+
# clause fails; the agent can only place inside the corridor).
|
| 23 |
+
# hard โ open arena 112x40 with TWO enemy spawn_point groups; the
|
| 24 |
+
# seed flips between a CONCENTRATED-thrust composition (9
|
| 25 |
+
# stacked at y=20) and a WIDE-FRONT composition (4 squads
|
| 26 |
+
# on 4 rows). The win predicate is `any_of` over the cluster
|
| 27 |
+
# and the line bars, but the load-bearing `units_killed_gte`
|
| 28 |
+
# clause is only cleared by the topology that MATCHES the
|
| 29 |
+
# active threat โ a memorised CLUSTER vs the wide-front seed
|
| 30 |
+
# misses the flanks and fails the kill quota; a memorised
|
| 31 |
+
# LINE vs the concentrated seed produces too thin a kill
|
| 32 |
+
# rate at any one cell.
|
| 33 |
+
#
|
| 34 |
+
# pbox is the load-bearing weapon. After the engine pbox-weapon fix
|
| 35 |
+
# (`fix(engine): pbox gets a direct-fire Armament`), a BUILT pbox is an
|
| 36 |
+
# active direct-fire anti-infantry tower (M60mg, Range 4c0, Burst 5,
|
| 37 |
+
# Damage 1000 โ one burst one-shots an e1). The only agent combat
|
| 38 |
+
# units are stance:0 HoldFire DECOYS on the easy tier (they never fire
|
| 39 |
+
# and so cannot carry kill output) and a single non-combatant SW-corner
|
| 40 |
+
# e1 on the other tiers (so the hard-tier env-reset `units_summary`
|
| 41 |
+
# check is non-empty); the pbox layout is the sole source of kills.
|
| 42 |
+
#
|
| 43 |
+
# An unarmed high-HP enemy `fact` marker far east keeps the engine alive
|
| 44 |
+
# past attacker annihilation so the win/fail predicate sees the terminal
|
| 45 |
+
# frame (no DRAW collapse on enemy-wipe auto-`done`).
|
| 46 |
+
#
|
| 47 |
+
# Discrimination (four-script bar โ scripted, no model needed):
|
| 48 |
+
# โข stall (observe-only): no pbox built, attackers raze fact โ LOSS.
|
| 49 |
+
# โข wrong-topology (CLUSTER on easy / LINE on medium / memorised
|
| 50 |
+
# CLUSTER on hard's wide-front seed): satisfies neither the region
|
| 51 |
+
# clauses for the active forcing geometry NOR the kill quota,
|
| 52 |
+
# and the surviving attackers raze the fact โ LOSS via either the
|
| 53 |
+
# fact-alive clause or `after_ticks`.
|
| 54 |
+
# โข pure-army (only e1, no pbox): fails count clause โ LOSS via
|
| 55 |
+
# `after_ticks` (the home-trained infantry may rack up kills but
|
| 56 |
+
# can never satisfy `building_count_gte:pbox`).
|
| 57 |
+
# โข intended TOPOLOGY (line on easy / cluster on medium / matched
|
| 58 |
+
# topology on each hard seed): count + region + kill + fact-alive
|
| 59 |
+
# all satisfied โ WIN.
|
| 60 |
+
|
| 61 |
meta:
|
| 62 |
id: def-tower-line-vs-cluster
|
| 63 |
+
title: 'Defense Topology โ Match Towers to Threat Geometry (Line or Cluster)'
|
| 64 |
capability: reasoning
|
| 65 |
real_world_meaning: >
|
| 66 |
+
Defensive structure placement is geometry-sensitive: a wide-front
|
| 67 |
+
attack across many rows demands a LINE that covers each row; a
|
| 68 |
+
concentrated thrust through one chokepoint demands a CLUSTER of
|
| 69 |
+
overlapping fire at the choke. Both topologies waste budget on the
|
| 70 |
+
wrong geometry โ a cluster against a spread attack leaves the
|
| 71 |
+
flanks open; a line against a funnel under-defends the choke. The
|
| 72 |
+
capability is reading the attacker's forcing geometry from the
|
| 73 |
+
map and the threat composition, then committing the defense
|
| 74 |
+
topology that matches.
|
| 75 |
robotics_analogue: >
|
| 76 |
+
Firewall / WAF rule placement. When traffic enters across many
|
| 77 |
+
paths (broad ingress, microservices), the right architecture is a
|
| 78 |
+
rule per path โ a layered spread. When all traffic MUST traverse
|
| 79 |
+
a single ingress (one API gateway, one bridge), the right
|
| 80 |
+
architecture is dense layered inspection AT that point. A defender
|
| 81 |
+
who deploys the wrong topology for the wrong threat surface either
|
| 82 |
+
overspends on one path and leaves others open, or under-protects
|
| 83 |
+
the actual ingress.
|
| 84 |
benchmark_anchor:
|
| 85 |
- "graph theory min-cut (concentrate defenses at chokepoints)"
|
| 86 |
- "military bunker placement doctrine"
|
| 87 |
- "firewall topology: dense at chokepoints"
|
| 88 |
+
- "perimeter doctrine (cover the full approach width)"
|
| 89 |
- "Lanchester defense concentration"
|
| 90 |
author: openra-bench
|
| 91 |
|
| 92 |
+
# Per-tier custom maps (see overrides.base_map in each level). The
|
| 93 |
+
# pack-level base_map is the shared default if a level omits its own.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
base_map: rush-hour-arena
|
|
|
|
| 95 |
|
| 96 |
base:
|
| 97 |
+
agent: {faction: allies}
|
| 98 |
+
enemy: {faction: soviet}
|
|
|
|
|
|
|
|
|
|
| 99 |
tools:
|
| 100 |
- observe
|
| 101 |
- build
|
|
|
|
| 105 |
- attack_move
|
| 106 |
- stop
|
| 107 |
planning: true
|
| 108 |
+
# No interrupts โ topology choice is a STATIC up-front decision
|
| 109 |
+
# (the attacker geometry is visible from turn 1: the model reads
|
| 110 |
+
# enemy positions and picks line / cluster accordingly). Disabling
|
| 111 |
+
# interrupts also makes the tick budget deterministic (exactly 90
|
| 112 |
+
# ticks/step) so the `after_ticks` fail clause reliably bites in.
|
| 113 |
+
interrupts: {}
|
| 114 |
termination:
|
| 115 |
max_ticks: 12000
|
| 116 |
+
actors: [] # every level supplies its own actor list via overrides.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
levels:
|
| 119 |
+
# โโ EASY โโ WIDE-FRONT attack. 4 squads of 3รe1 (12 attackers) on
|
| 120 |
+
# four separate rows (y=4 / 12 / 28 / 36) at x=95. Each squad targets
|
| 121 |
+
# an agent decoy (`e1`, stance:0 HoldFire) on the SAME row at x=4,
|
| 122 |
+
# so the squad walks STRAIGHT WEST along its row (stance:3 โ
|
| 123 |
+
# AttackAnything โ advance toward nearest visible enemy; the
|
| 124 |
+
# row-local decoy is always nearer than the central fact). The
|
| 125 |
+
# pre-placed decoys are the squad's forcing-geometry anchor โ
|
| 126 |
+
# without them stance:3 would steer every squad diagonally toward
|
| 127 |
+
# the fact at (10,20), collapsing the wide-front spread into a
|
| 128 |
+
# single converging y=20 column. The decoys are stance:0 HoldFire
|
| 129 |
+
# so they never auto-fire and cannot contribute to the kill quota
|
| 130 |
+
# (the pbox LINE must produce every kill).
|
| 131 |
+
#
|
| 132 |
+
# A CLUSTER at (60,20) is a range-4 disc (M60mg, Range 4c0). The
|
| 133 |
+
# nearest row (y=12 or y=28) is 8 cells from y=20 โ twice the
|
| 134 |
+
# weapon range โ so the cluster cannot fire on ANY squad as it
|
| 135 |
+
# passes. Cluster kills 0 โ `units_killed_gte:8` fails AND the
|
| 136 |
+
# squads kill the decoys then march to the fact, razing it. LOSS
|
| 137 |
+
# on both clauses.
|
| 138 |
+
#
|
| 139 |
+
# The intended LINE places one pbox at each of the four squad rows:
|
| 140 |
+
# (60,4) / (60,12) / (60,28) / (60,36). Each squad walks straight
|
| 141 |
+
# west on its own row into its pbox's range-4 disc; the M60mg burst
|
| 142 |
+
# one-shots each e1 in passing. All 12 attackers die before reaching
|
| 143 |
+
# the decoys; kill quota cleared; fact intact. Budget exactly 4 pbox
|
| 144 |
+
# (2400cr). max_turns 60 โ reachable tick 93+90ยท59 = 5403;
|
| 145 |
+
# deadline 5400.
|
| 146 |
easy:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
description: >
|
| 148 |
+
Four rifle squads (3 each) advance west on four separate rows โ
|
| 149 |
+
y=4, 12, 28, 36 โ each toward its own decoy on the same row at
|
| 150 |
+
x=4. Budget $2400 buys four pillboxes. Place one on each of the
|
| 151 |
+
four attacker rows at x=60 so every squad walks into a pillbox's
|
| 152 |
+
range. Clustering all four at the centre (y=20) leaves every row
|
| 153 |
+
out of weapon range; the cluster kills nothing and the squads
|
| 154 |
+
raze your yard. Yard intact, eight kills, within about 60 turns.
|
| 155 |
+
starting_cash: 2400
|
| 156 |
overrides:
|
| 157 |
+
# Wide-open arena 112x40 โ no terrain features. The forcing
|
| 158 |
+
# geometry is supplied by the SPREAD of attacker spawn rows
|
| 159 |
+
# together with per-row decoys that keep each squad walking
|
| 160 |
+
# along its OWN latitude; a cluster vs this spread is
|
| 161 |
+
# geometrically out of range on every row.
|
| 162 |
+
base_map:
|
| 163 |
+
generator: arena
|
| 164 |
+
name: def-tower-lvsc-easy-arena
|
| 165 |
+
width: 112
|
| 166 |
+
height: 40
|
| 167 |
+
cordon: 2
|
| 168 |
+
title: def-tower-line-vs-cluster easy wide-front 112x40
|
| 169 |
actors:
|
| 170 |
+
# Pre-placed agent base โ the fact at (10,20) is the
|
| 171 |
+
# win-condition asset; tent/powr offsets keep them out of the
|
| 172 |
+
# y=20 lane. The fact is far enough from every squad's path
|
| 173 |
+
# that, until the row's decoy dies, it is not the squad's
|
| 174 |
+
# nearest visible enemy.
|
| 175 |
- {type: fact, owner: agent, position: [10, 20]}
|
| 176 |
- {type: tent, owner: agent, position: [14, 18]}
|
| 177 |
- {type: powr, owner: agent, position: [14, 22]}
|
| 178 |
+
# Per-row agent decoy units (`e1`, stance:0 HoldFire). Each
|
| 179 |
+
# decoy is the row-local AGENT UNIT for the rusher bot to
|
| 180 |
+
# charge โ rusher routes per-unit toward the nearest agent
|
| 181 |
+
# MASS cell, which on rows y=4 / 12 / 28 / 36 is the row's
|
| 182 |
+
# own decoy at (12,ROW) (closer than the y=20 fact by both
|
| 183 |
+
# Chebyshev and path distance). Stance:0 means the decoys
|
| 184 |
+
# never auto-fire, so they cannot rack up kills off the
|
| 185 |
+
# rush โ the pbox LINE is the sole kill source.
|
| 186 |
+
- {type: e1, owner: agent, position: [12, 4], stance: 0}
|
| 187 |
+
- {type: e1, owner: agent, position: [12, 12], stance: 0}
|
| 188 |
+
- {type: e1, owner: agent, position: [12, 28], stance: 0}
|
| 189 |
+
- {type: e1, owner: agent, position: [12, 36], stance: 0}
|
|
|
|
| 190 |
# Unarmed high-HP marker (anti-DRAW): keeps the episode alive
|
| 191 |
+
# past full enemy elimination so the win/fail check fires.
|
| 192 |
+
- {type: fact, owner: enemy, position: [108, 20]}
|
| 193 |
+
# Scheduled rush wave โ injected at tick 2000, AFTER all 4
|
| 194 |
+
# LINE pboxes are built (Defense queue is serial; each pbox
|
| 195 |
+
# takes ~450 ticks to produce, so the 4th completes ~tick 1800
|
| 196 |
+
# in the intended build order). The four squads stage in a
|
| 197 |
+
# tight band at x=95 on the four attacker rows, then march
|
| 198 |
+
# west; each squad's nearest agent building is its own row
|
| 199 |
+
# silo at (12,ROW), so the squad walks STRAIGHT WEST along
|
| 200 |
+
# its row into the LINE pbox at (60,ROW). Squads do not bend
|
| 201 |
+
# toward the fact because the row silo is closer than the
|
| 202 |
+
# fact on every spawn cell.
|
| 203 |
+
scheduled_events:
|
| 204 |
+
- tick: 2000
|
| 205 |
+
type: spawn_actors
|
| 206 |
+
actors:
|
| 207 |
+
- {type: e1, owner: enemy, position: [95, 4], stance: 3, count: 3}
|
| 208 |
+
- {type: e1, owner: enemy, position: [95, 12], stance: 3, count: 3}
|
| 209 |
+
- {type: e1, owner: enemy, position: [95, 28], stance: 3, count: 3}
|
| 210 |
+
- {type: e1, owner: enemy, position: [95, 36], stance: 3, count: 3}
|
| 211 |
win_condition:
|
| 212 |
all_of:
|
| 213 |
+
- building_count_gte: {type: pbox, n: 4}
|
| 214 |
+
# Each of the 4 attacker rows must host at least one pbox at
|
| 215 |
+
# x=60 (radius 3 โ vertical cells yยฑ3). A CLUSTER at (60,20)
|
| 216 |
+
# has all 4 pbox within radius 3 of y=20, which is NOT within
|
| 217 |
+
# radius 3 of any of y=4 (dist 16) / y=12 (dist 8) / y=28
|
| 218 |
+
# (dist 8) / y=36 (dist 16) โ so EVERY row clause fails.
|
| 219 |
+
- building_in_region: {type: pbox, x: 60, y: 4, radius: 3, count: 1}
|
| 220 |
+
- building_in_region: {type: pbox, x: 60, y: 12, radius: 3, count: 1}
|
| 221 |
+
- building_in_region: {type: pbox, x: 60, y: 28, radius: 3, count: 1}
|
| 222 |
+
- building_in_region: {type: pbox, x: 60, y: 36, radius: 3, count: 1}
|
| 223 |
+
- units_killed_gte: 8
|
| 224 |
- building_count_gte: {type: fact, n: 1}
|
| 225 |
- within_ticks: 5400
|
| 226 |
fail_condition:
|
|
|
|
| 229 |
- not: {building_count_gte: {type: fact, n: 1}}
|
| 230 |
max_turns: 60
|
| 231 |
|
| 232 |
+
# โโ MEDIUM โโ CONCENTRATED thrust through ONE chokepoint. The map
|
| 233 |
+
# is a chokepoint-arena 112x40 with vertical walls at x=60 forming
|
| 234 |
+
# a single 5-cell-wide corridor at y=18..22. The entire attacker
|
| 235 |
+
# band (8รe1 + 2รe3) stages east of the corridor and MUST funnel
|
| 236 |
+
# through y=18..22 to reach the fact on the west. A CLUSTER of 4
|
| 237 |
+
# pbox at (60,20) (all inside the corridor) puts every attacker
|
| 238 |
+
# within range of all 4 pboxes simultaneously โ 4 simultaneous
|
| 239 |
+
# M60mg bursts shred the column in passing. A LINE attempt at
|
| 240 |
+
# y=8 / 16 / 24 / 32 at x=60 lands on the wall water (every cell
|
| 241 |
+
# except y=18..22 at xโ60 is impassable) โ placement fails for 3
|
| 242 |
+
# of 4 attempts โ pbox count < 4 โ LOSS. A LINE attempt OUT of the
|
| 243 |
+
# wall column (e.g. at x=50) puts only one pbox within range-4 of
|
| 244 |
+
# the corridor mouth โ kill rate falls far below the quota โ LOSS.
|
| 245 |
+
# Budget exactly 4 pbox (2400cr). max_turns 60 โ deadline 5400.
|
| 246 |
medium:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
description: >
|
| 248 |
+
A single 5-cell corridor at x=60, y=18..22 is the only way west.
|
| 249 |
+
Ten attackers (eight rifles + two rocket soldiers) must funnel
|
| 250 |
+
through it to reach your construction yard at (10,20). Budget
|
| 251 |
+
$2400 buys four pillboxes. Cluster all four inside the corridor
|
| 252 |
+
so overlapping fire shreds the column at the choke. Spreading
|
| 253 |
+
across rows the corridor doesn't reach is a wasted budget. Yard
|
| 254 |
+
intact, ten kills, within about 60 turns.
|
| 255 |
starting_cash: 2400
|
| 256 |
overrides:
|
| 257 |
+
base_map:
|
| 258 |
+
generator: chokepoint-arena
|
| 259 |
+
name: def-tower-lvsc-medium-arena
|
| 260 |
+
width: 112
|
| 261 |
+
height: 40
|
| 262 |
+
cordon: 2
|
| 263 |
+
pinch_x: 60
|
| 264 |
+
pinch_width: 10
|
| 265 |
+
corridor_y: 20
|
| 266 |
+
corridor_width: 5
|
| 267 |
+
title: def-tower-line-vs-cluster medium chokepoint 112x40
|
| 268 |
actors:
|
| 269 |
- {type: fact, owner: agent, position: [10, 20]}
|
| 270 |
- {type: tent, owner: agent, position: [14, 18]}
|
| 271 |
- {type: powr, owner: agent, position: [14, 22]}
|
| 272 |
+
# Non-combatant corner e1 (in-corridor latitude so it doesn't
|
| 273 |
+
# land on a wall). y=20 is in the corridor latitude; park at
|
| 274 |
+
# (4, 20).
|
| 275 |
+
- {type: e1, owner: agent, position: [4, 20], stance: 2}
|
| 276 |
+
# Attacker band โ all funnelled through the corridor. Staged
|
| 277 |
+
# in the corridor latitude (y=18..22) at xโ95 so they walk
|
| 278 |
+
# straight west into the choke.
|
| 279 |
+
- {type: e1, owner: enemy, position: [95, 20], stance: 3, count: 8}
|
| 280 |
+
- {type: e3, owner: enemy, position: [97, 20], stance: 3, count: 2}
|
| 281 |
+
# Anti-DRAW marker โ east of the corridor, on the corridor
|
| 282 |
+
# latitude so it's in a valid (non-wall) cell.
|
| 283 |
+
- {type: fact, owner: enemy, position: [108, 20]}
|
|
|
|
| 284 |
win_condition:
|
| 285 |
all_of:
|
| 286 |
- building_count_gte: {type: pbox, n: 4}
|
| 287 |
+
# All 4 pbox must sit inside the choke disc (radius 5 of the
|
| 288 |
+
# corridor centre (60,20)). A wide LINE attempt that places
|
| 289 |
+
# pbox outside the corridor (or outside this disc) fails the
|
| 290 |
+
# region clause OR the count clause (if its picks landed on
|
| 291 |
+
# wall water and were rejected by the engine placement check).
|
| 292 |
+
- building_in_region: {type: pbox, x: 60, y: 20, radius: 5, count: 4}
|
| 293 |
+
- units_killed_gte: 10
|
| 294 |
- building_count_gte: {type: fact, n: 1}
|
| 295 |
- within_ticks: 5400
|
| 296 |
fail_condition:
|
|
|
|
| 299 |
- not: {building_count_gte: {type: fact, n: 1}}
|
| 300 |
max_turns: 60
|
| 301 |
|
| 302 |
+
# โโ HARD โโ seed-varied forcing geometry. The map is the open arena
|
| 303 |
+
# (112x40, NO walls โ both topologies are physically buildable so
|
| 304 |
+
# the discrimination comes from the kill quota, not from placement
|
| 305 |
+
# rejection). Two ENEMY spawn_point groups round-robin by seed:
|
| 306 |
+
# spawn_point 0 = CONCENTRATED thrust: 9รe1 + 2รe3 stacked on the
|
| 307 |
+
# y=20 lane at x=90 โ cluster wins, line wastes
|
| 308 |
+
# coverage on empty rows.
|
| 309 |
+
# spawn_point 1 = WIDE-FRONT: 4 squads of 3รe1 on y=4/12/28/36 at
|
| 310 |
+
# x=90, plus per-row decoys to keep each squad on
|
| 311 |
+
# its own latitude โ line wins, cluster leaves
|
| 312 |
+
# the flanks far out of range.
|
| 313 |
+
# Persistent enemy fact marker is duplicated across both enemy spawn
|
| 314 |
+
# groups (CLAUDE.md: enemy spawn_point filter is per-OWNER and
|
| 315 |
+
# filters out marker actors without spawn_point). The per-row decoy
|
| 316 |
+
# rifles are likewise spawn_point-filtered (they're only needed on
|
| 317 |
+
# the wide-front seed โ on the concentrated seed they'd silently
|
| 318 |
+
# tilt the centroid logic). The agent base is always at the fixed
|
| 319 |
+
# canonical position (no spawn_point on the base actors, so the
|
| 320 |
+
# agent-side filter stays inactive and the base places every seed).
|
| 321 |
+
# The win predicate is `any_of` between the cluster bar (4-of-4 at
|
| 322 |
+
# (60,20)) and the line bar (1 each at 4 rungs). The kill quota
|
| 323 |
+
# (`units_killed_gte: 8`) is the load-bearing clause โ placing the
|
| 324 |
+
# wrong topology for the active seed fails the kill quota even when
|
| 325 |
+
# the region clause is technically satisfied. max_turns 70 โ
|
| 326 |
+
# reachable tick 93+90ยท69 = 6303; deadline 6300.
|
| 327 |
hard:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
description: >
|
| 329 |
+
Open arena. The enemy composition flips by seed: either ONE
|
| 330 |
+
concentrated column on y=20 or FOUR squads spread across
|
| 331 |
+
y=4/12/28/36 (with per-row decoys at x=4 that keep each spread
|
| 332 |
+
squad on its own latitude). Read the threat from your
|
| 333 |
+
observation and pick the topology that matches โ cluster the
|
| 334 |
+
four pillboxes at (60,20) for the concentrated thrust, OR
|
| 335 |
+
spread one per row at x=60 for the wide front. The wrong
|
| 336 |
+
topology fails the kill quota. Yard intact, eight kills,
|
| 337 |
+
within about 70 turns.
|
| 338 |
+
starting_cash: 2400
|
| 339 |
overrides:
|
| 340 |
+
base_map:
|
| 341 |
+
generator: arena
|
| 342 |
+
name: def-tower-lvsc-hard-arena
|
| 343 |
+
width: 112
|
| 344 |
+
height: 40
|
| 345 |
+
cordon: 2
|
| 346 |
+
title: def-tower-line-vs-cluster hard open 112x40
|
| 347 |
actors:
|
| 348 |
+
# Single agent base (no spawn_point) โ only the ENEMY side is
|
| 349 |
+
# spawn_point-filtered, so all agent base actors place on
|
| 350 |
+
# every seed (agent-side filter is inactive when no agent
|
| 351 |
+
# actor declares spawn_point).
|
| 352 |
+
- {type: fact, owner: agent, position: [10, 20]}
|
| 353 |
+
- {type: tent, owner: agent, position: [14, 18]}
|
| 354 |
+
- {type: powr, owner: agent, position: [14, 22]}
|
| 355 |
+
- {type: e1, owner: agent, position: [4, 36], stance: 0}
|
| 356 |
+
# spawn_point 0 โ CONCENTRATED thrust (stacked column at y=20).
|
| 357 |
+
# 11 attackers total (9 e1 + 2 e3). All stance:3 โ advance
|
| 358 |
+
# toward agent fact.
|
| 359 |
+
- {type: e1, owner: enemy, position: [90, 20], stance: 3, count: 9, spawn_point: 0}
|
| 360 |
+
- {type: e3, owner: enemy, position: [92, 20], stance: 3, count: 2, spawn_point: 0}
|
| 361 |
+
# spawn_point 1 โ WIDE-FRONT (4 squads on 4 rows). Each
|
| 362 |
+
# squad is staged JUST EAST of the LINE pbox column at
|
| 363 |
+
# (65, ROW) and held with stance:0 (HoldFire) so the squads
|
| 364 |
+
# do NOT move toward the fact โ they sit on their rows
|
| 365 |
+
# waiting to be shot. A LINE pbox at (60, ROW) has each
|
| 366 |
+
# row-squad in range-5 fire range and shreds it in passing
|
| 367 |
+
# bursts; a CLUSTER at (60, 20) has the y=12 and y=28 rows
|
| 368 |
+
# at vertical distance 8 (twice the M60mg range) and the
|
| 369 |
+
# y=4 and y=36 rows at distance 16 โ no fire reaches any
|
| 370 |
+
# squad and the kill quota stays at zero. Stance:0 squads
|
| 371 |
+
# also can't raze the fact, so the LOSS for cluster /
|
| 372 |
+
# stall comes from the `after_ticks` timeout (failed kill
|
| 373 |
+
# quota), not the fact-alive clause.
|
| 374 |
+
- {type: e1, owner: enemy, position: [63, 4], stance: 0, count: 3, spawn_point: 1}
|
| 375 |
+
- {type: e1, owner: enemy, position: [63, 12], stance: 0, count: 3, spawn_point: 1}
|
| 376 |
+
- {type: e1, owner: enemy, position: [63, 28], stance: 0, count: 3, spawn_point: 1}
|
| 377 |
+
- {type: e1, owner: enemy, position: [63, 36], stance: 0, count: 3, spawn_point: 1}
|
| 378 |
+
# Anti-DRAW marker, duplicated across BOTH enemy spawn groups
|
| 379 |
+
# (CLAUDE.md: enemy spawn_point filter is per-owner; an enemy
|
| 380 |
+
# actor without spawn_point would be filtered out when any
|
| 381 |
+
# enemy actor declares spawn_point).
|
| 382 |
+
- {type: fact, owner: enemy, position: [108, 20], spawn_point: 0}
|
| 383 |
+
- {type: fact, owner: enemy, position: [108, 20], spawn_point: 1}
|
| 384 |
win_condition:
|
| 385 |
all_of:
|
| 386 |
+
- building_count_gte: {type: pbox, n: 4}
|
| 387 |
- any_of:
|
| 388 |
+
# CLUSTER bar (matches concentrated-thrust seed).
|
| 389 |
- building_in_region: {type: pbox, x: 60, y: 20, radius: 5, count: 4}
|
| 390 |
+
# LINE bar (matches wide-front seed โ 1 pbox per row).
|
| 391 |
+
- all_of:
|
| 392 |
+
- building_in_region: {type: pbox, x: 60, y: 4, radius: 3, count: 1}
|
| 393 |
+
- building_in_region: {type: pbox, x: 60, y: 12, radius: 3, count: 1}
|
| 394 |
+
- building_in_region: {type: pbox, x: 60, y: 28, radius: 3, count: 1}
|
| 395 |
+
- building_in_region: {type: pbox, x: 60, y: 36, radius: 3, count: 1}
|
| 396 |
+
- units_killed_gte: 8
|
| 397 |
- building_count_gte: {type: fact, n: 1}
|
| 398 |
- within_ticks: 6300
|
| 399 |
fail_condition:
|
|
@@ -1,35 +1,55 @@
|
|
| 1 |
"""def-tower-line-vs-cluster scenario family, full loop on Rust.
|
| 2 |
|
| 3 |
-
The pack tests DEFENSE TOPOLOGY:
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
*
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
*
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
The scripted-policy validations prove deterministically that:
|
| 24 |
|
| 25 |
-
* the intended
|
| 26 |
-
WINS every level + every
|
| 27 |
-
* stall
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
| 33 |
"""
|
| 34 |
|
| 35 |
from __future__ import annotations
|
|
@@ -47,118 +67,98 @@ PACK = PACKS_DIR / "def-tower-line-vs-cluster.yaml"
|
|
| 47 |
LEVELS = ("easy", "medium", "hard")
|
| 48 |
SEEDS = (1, 2, 3, 4)
|
| 49 |
|
| 50 |
-
#
|
| 51 |
-
|
|
|
|
|
|
|
| 52 |
|
| 53 |
|
| 54 |
# โโ scripted policies โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 55 |
|
| 56 |
|
| 57 |
def stall(rs, C):
|
| 58 |
-
"""Observe-only โ the agent never spends.
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
| 60 |
return [C.observe()]
|
| 61 |
|
| 62 |
|
| 63 |
-
def
|
| 64 |
-
"""
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
(cx0 - 1, cy0), (cx0 + 1, cy0),
|
| 73 |
-
]
|
| 74 |
-
|
| 75 |
-
def policy(rs, C):
|
| 76 |
-
own_b = rs.get("own_buildings") or []
|
| 77 |
-
n = sum(1 for b in own_b if b.get("type") == "pbox")
|
| 78 |
-
prod = rs.get("production") or []
|
| 79 |
-
prod_items = [p.get("item") for p in prod if isinstance(p, dict)]
|
| 80 |
-
cmds = []
|
| 81 |
-
# Once enough pboxes are up, idle (the win clause counts the
|
| 82 |
-
# current building list each turn).
|
| 83 |
-
if n >= len(cells):
|
| 84 |
-
return [C.observe()]
|
| 85 |
-
if "pbox" not in prod_items:
|
| 86 |
-
cmds.append(C.build("pbox"))
|
| 87 |
-
cmds.append(C.place_building("pbox", cells[n][0], cells[n][1]))
|
| 88 |
-
if not cmds:
|
| 89 |
-
cmds.append(C.observe())
|
| 90 |
-
return cmds
|
| 91 |
-
|
| 92 |
-
return policy
|
| 93 |
|
| 94 |
|
| 95 |
-
def
|
| 96 |
-
"""
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
| 101 |
|
| 102 |
def policy(rs, C):
|
| 103 |
own_b = rs.get("own_buildings") or []
|
| 104 |
n = sum(1 for b in own_b if b.get("type") == "pbox")
|
| 105 |
prod = rs.get("production") or []
|
| 106 |
prod_items = [p.get("item") for p in prod if isinstance(p, dict)]
|
| 107 |
-
cmds = []
|
| 108 |
if n >= len(cells):
|
| 109 |
return [C.observe()]
|
|
|
|
| 110 |
if "pbox" not in prod_items:
|
| 111 |
cmds.append(C.build("pbox"))
|
| 112 |
cmds.append(C.place_building("pbox", cells[n][0], cells[n][1]))
|
| 113 |
-
|
| 114 |
-
cmds.append(C.observe())
|
| 115 |
-
return cmds
|
| 116 |
|
| 117 |
return policy
|
| 118 |
|
| 119 |
|
| 120 |
-
def
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
if "e1" not in prod_items:
|
| 127 |
-
return [C.build("e1")]
|
| 128 |
-
return [C.observe()]
|
| 129 |
|
| 130 |
|
| 131 |
# โโ scenario-shape invariants โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 132 |
|
| 133 |
|
| 134 |
-
def
|
| 135 |
pack = load_pack(PACK)
|
| 136 |
assert pack.meta.id == "def-tower-line-vs-cluster"
|
| 137 |
assert pack.meta.capability == "reasoning"
|
| 138 |
assert set(pack.levels) == {"easy", "medium", "hard"}
|
| 139 |
-
# Required-by-spec benchmark anchors
|
|
|
|
|
|
|
|
|
|
| 140 |
anchors = pack.meta.benchmark_anchor
|
| 141 |
assert any("min-cut" in a.lower() or "chokepoint" in a.lower() for a in anchors), anchors
|
| 142 |
assert any("bunker" in a.lower() for a in anchors), anchors
|
| 143 |
-
|
| 144 |
-
#
|
| 145 |
for lvl in LEVELS:
|
| 146 |
c = compile_level(pack, lvl)
|
| 147 |
-
assert c.map_supported
|
| 148 |
-
bot = getattr(c.scenario.enemy, "bot_type", None) or getattr(
|
| 149 |
-
c.scenario.enemy, "bot", None
|
| 150 |
-
)
|
| 151 |
-
assert str(bot).lower() == "rusher", (lvl, bot)
|
| 152 |
|
| 153 |
|
| 154 |
def test_starting_cash_is_exact_pbox_budget():
|
| 155 |
-
"""The cash is
|
| 156 |
-
|
| 157 |
-
the count clause is satisfied โ the topology decision is
|
|
|
|
| 158 |
pack = load_pack(PACK)
|
| 159 |
-
for lvl
|
| 160 |
c = compile_level(pack, lvl)
|
| 161 |
-
assert c.starting_cash ==
|
| 162 |
|
| 163 |
|
| 164 |
@pytest.mark.parametrize("level", LEVELS)
|
|
@@ -185,8 +185,7 @@ def test_fact_alive_clause_uses_present_tense_predicate():
|
|
| 185 |
"""The fact-survival clause must use the PRESENT-TENSE predicate
|
| 186 |
(`building_count_gte:{type:fact,n:1}`) rather than `has_building`,
|
| 187 |
which is a one-shot "ever seen" set that stays true after the fact
|
| 188 |
-
is destroyed (a documented CLAUDE.md footgun).
|
| 189 |
-
razing the fact would not trigger a LOSS."""
|
| 190 |
for lvl in LEVELS:
|
| 191 |
c = compile_level(load_pack(PACK), lvl)
|
| 192 |
fc = c.fail_condition.model_dump(exclude_none=True)
|
|
@@ -200,39 +199,73 @@ def test_fact_alive_clause_uses_present_tense_predicate():
|
|
| 200 |
assert fact_clauses, f"{lvl}: missing present-tense fact-alive fail clause"
|
| 201 |
|
| 202 |
|
| 203 |
-
def
|
| 204 |
-
"""Hard-tier contract: โฅ2 distinct
|
| 205 |
-
|
| 206 |
-
|
|
|
|
| 207 |
c = compile_level(load_pack(PACK), "hard")
|
| 208 |
-
|
| 209 |
a.spawn_point for a in c.scenario.actors
|
| 210 |
-
if a.owner == "
|
| 211 |
}
|
| 212 |
-
assert
|
| 213 |
-
# In-bounds check (
|
|
|
|
| 214 |
for a in c.scenario.actors:
|
| 215 |
x, y = a.position
|
| 216 |
-
assert 2 <= x <=
|
| 217 |
|
| 218 |
|
| 219 |
-
# โโ solvency: intended
|
| 220 |
|
| 221 |
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
for seed in SEEDS:
|
| 226 |
r = run_level(c, make_cluster(), seed=seed)
|
| 227 |
assert r.outcome == "win", (
|
| 228 |
-
f"
|
| 229 |
f"got {r.outcome} (tick={r.signals.game_tick}, "
|
| 230 |
f"kills={r.signals.units_killed}, "
|
| 231 |
-
f"lost={r.signals.units_lost}, "
|
| 232 |
f"buildings={r.signals.own_buildings})"
|
| 233 |
)
|
| 234 |
|
| 235 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
# โโ no-cheat: every lazy / wrong-topology policy LOSES (not draws) โโโ
|
| 237 |
|
| 238 |
|
|
@@ -240,17 +273,14 @@ def test_intended_cluster_wins_every_level_and_seed(level):
|
|
| 240 |
@pytest.mark.parametrize(
|
| 241 |
"policy_name,policy_factory",
|
| 242 |
[
|
| 243 |
-
("stall",
|
| 244 |
-
("
|
| 245 |
-
("pure_army", lambda lvl: pure_army),
|
| 246 |
],
|
| 247 |
)
|
| 248 |
-
def
|
| 249 |
-
|
| 250 |
-
)
|
| 251 |
-
|
| 252 |
-
unmet), and pure-army (count clause unmet) must ALL LOSE on every
|
| 253 |
-
level + every seed โ no draw."""
|
| 254 |
c = compile_level(load_pack(PACK), level)
|
| 255 |
fn = policy_factory(level)
|
| 256 |
for seed in SEEDS:
|
|
@@ -262,13 +292,71 @@ def test_lazy_and_wrong_topology_policies_lose_every_level_and_seed(
|
|
| 262 |
)
|
| 263 |
|
| 264 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
# โโ determinism โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 266 |
|
| 267 |
|
| 268 |
def test_intended_run_is_deterministic_on_easy():
|
| 269 |
c = compile_level(load_pack(PACK), "easy")
|
| 270 |
-
a = run_level(c,
|
| 271 |
-
b = run_level(c,
|
| 272 |
assert (a.outcome, a.turns, a.signals.units_killed) == (
|
| 273 |
b.outcome,
|
| 274 |
b.turns,
|
|
|
|
| 1 |
"""def-tower-line-vs-cluster scenario family, full loop on Rust.
|
| 2 |
|
| 3 |
+
The pack tests DEFENSE TOPOLOGY: the agent must match the topology of
|
| 4 |
+
its pillbox layout (LINE across rows OR CLUSTER at one cell) to the
|
| 5 |
+
attacker's forcing geometry. Each tier presents a different geometry
|
| 6 |
+
and therefore demands a different topology:
|
| 7 |
+
|
| 8 |
+
* easy โ open arena 112x40, 4 attacker squads on 4 separate rows
|
| 9 |
+
(y=4/12/28/36) โ LINE wins, CLUSTER fails (the line rung
|
| 10 |
+
clauses are all missed by a y=20 cluster).
|
| 11 |
+
* mediumโ chokepoint-arena 112x40, one 5-cell corridor at y=18..22
|
| 12 |
+
x=60 โ CLUSTER wins, LINE fails (the off-corridor rows are
|
| 13 |
+
impassable wall water, so a line attempt at y=8/16/24/32
|
| 14 |
+
either has no place to go or is rejected outright).
|
| 15 |
+
* hard โ open arena 112x40 with TWO enemy spawn_point groups: a
|
| 16 |
+
CONCENTRATED-thrust composition (9รe1 + 2รe3 stacked at
|
| 17 |
+
y=20) and a WIDE-FRONT composition (4 stance:0 squads on
|
| 18 |
+
y=4/12/28/36). The win predicate is `any_of` over the
|
| 19 |
+
cluster bar and the line bar; the kill quota
|
| 20 |
+
`units_killed_gte:8` is the load-bearing clause โ a
|
| 21 |
+
memorised CLUSTER vs the wide-front seed (or a memorised
|
| 22 |
+
LINE vs the concentrated seed) cannot fire on the active
|
| 23 |
+
threat and so cannot satisfy the kill quota.
|
| 24 |
+
|
| 25 |
+
The win predicate makes the topology decision load-bearing โ total
|
| 26 |
+
pbox count alone is not sufficient:
|
| 27 |
+
|
| 28 |
+
* `building_count_gte:{pbox, n:4}` โ the agent built the full budget;
|
| 29 |
+
* `building_in_region` row/disc clauses โ the pbox layout MATCHES the
|
| 30 |
+
forcing geometry (LINE rungs on easy, CLUSTER disc on medium, EITHER
|
| 31 |
+
on hard);
|
| 32 |
+
* `units_killed_gte:N` โ the pbox layout actively KILLS the attackers
|
| 33 |
+
(a stall / wrong-topology layout that satisfies the region clause
|
| 34 |
+
by luck still fails to kill enough);
|
| 35 |
+
* `building_count_gte:{fact,n:1}` (present-tense โ `has_building` is
|
| 36 |
+
the one-shot "ever-seen" set, see CLAUDE.md footgun);
|
| 37 |
+
* `within_ticks` paired with `after_ticks` in the fail clause โ a
|
| 38 |
+
non-finisher is a real reachable timeout LOSS (no interrupts on this
|
| 39 |
+
pack โ each step is exactly 90 ticks, so max_turns is a hard tick
|
| 40 |
+
budget that the `after_ticks` deadline reliably bites in).
|
| 41 |
|
| 42 |
The scripted-policy validations prove deterministically that:
|
| 43 |
|
| 44 |
+
* the intended TOPOLOGY (line on easy / cluster on medium / matched
|
| 45 |
+
topology per seed on hard) WINS every level + every applicable seed;
|
| 46 |
+
* stall, pure-army, and the WRONG-TOPOLOGY (cluster on easy / line on
|
| 47 |
+
medium) all LOSE on every level + every seed โ a real LOSS, not a
|
| 48 |
+
draw (the count or region or kill clause is unmet AND the
|
| 49 |
+
`after_ticks` deadline reliably bites);
|
| 50 |
+
* the hard tier defines โฅ2 distinct enemy spawn_point groups (Wave-9
|
| 51 |
+
per-owner spawn_point activation) so a single memorised topology
|
| 52 |
+
cannot generalise across seeds.
|
| 53 |
"""
|
| 54 |
|
| 55 |
from __future__ import annotations
|
|
|
|
| 67 |
LEVELS = ("easy", "medium", "hard")
|
| 68 |
SEEDS = (1, 2, 3, 4)
|
| 69 |
|
| 70 |
+
# Pbox cells for each topology. Outer rows are placed FIRST so they
|
| 71 |
+
# are up by the time the (slowest-arriving) flank squads reach x=60.
|
| 72 |
+
LINE_CELLS = [(60, 4), (60, 36), (60, 12), (60, 28)]
|
| 73 |
+
CLUSTER_CELLS = [(58, 20), (60, 19), (60, 21), (62, 20)]
|
| 74 |
|
| 75 |
|
| 76 |
# โโ scripted policies โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 77 |
|
| 78 |
|
| 79 |
def stall(rs, C):
|
| 80 |
+
"""Observe-only โ the agent never spends. The win predicate
|
| 81 |
+
requires `building_count_gte:pbox,4` AND `units_killed_gte`, so
|
| 82 |
+
the win never latches and the episode times out via
|
| 83 |
+
`after_ticks`. (On every tier the attackers are configured so a
|
| 84 |
+
pure stall also cannot satisfy the fact-alive clause.)"""
|
| 85 |
return [C.observe()]
|
| 86 |
|
| 87 |
|
| 88 |
+
def pure_army(rs, C):
|
| 89 |
+
"""PURE-ARMY: only train e1 โ never build a pbox. FAILS the
|
| 90 |
+
`building_count_gte:pbox,4` clause regardless of how many kills
|
| 91 |
+
the home-trained infantry rack up."""
|
| 92 |
+
prod = rs.get("production") or []
|
| 93 |
+
prod_items = [p.get("item") for p in prod if isinstance(p, dict)]
|
| 94 |
+
if "e1" not in prod_items:
|
| 95 |
+
return [C.build("e1")]
|
| 96 |
+
return [C.observe()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
|
| 99 |
+
def _make_topology(cells):
|
| 100 |
+
"""Build one pbox at each cell, in order, then idle. The
|
| 101 |
+
place_building order is rejected by the engine if the cell is
|
| 102 |
+
impassable terrain โ which is how the medium tier's LINE attempt
|
| 103 |
+
silently fails the count clause (only the one in-corridor cell
|
| 104 |
+
succeeds)."""
|
| 105 |
+
cells = list(cells)
|
| 106 |
|
| 107 |
def policy(rs, C):
|
| 108 |
own_b = rs.get("own_buildings") or []
|
| 109 |
n = sum(1 for b in own_b if b.get("type") == "pbox")
|
| 110 |
prod = rs.get("production") or []
|
| 111 |
prod_items = [p.get("item") for p in prod if isinstance(p, dict)]
|
|
|
|
| 112 |
if n >= len(cells):
|
| 113 |
return [C.observe()]
|
| 114 |
+
cmds = []
|
| 115 |
if "pbox" not in prod_items:
|
| 116 |
cmds.append(C.build("pbox"))
|
| 117 |
cmds.append(C.place_building("pbox", cells[n][0], cells[n][1]))
|
| 118 |
+
return cmds or [C.observe()]
|
|
|
|
|
|
|
| 119 |
|
| 120 |
return policy
|
| 121 |
|
| 122 |
|
| 123 |
+
def make_line():
|
| 124 |
+
return _make_topology(LINE_CELLS)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def make_cluster():
|
| 128 |
+
return _make_topology(CLUSTER_CELLS)
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
|
| 131 |
# โโ scenario-shape invariants โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 132 |
|
| 133 |
|
| 134 |
+
def test_pack_compiles_with_three_levels():
|
| 135 |
pack = load_pack(PACK)
|
| 136 |
assert pack.meta.id == "def-tower-line-vs-cluster"
|
| 137 |
assert pack.meta.capability == "reasoning"
|
| 138 |
assert set(pack.levels) == {"easy", "medium", "hard"}
|
| 139 |
+
# Required-by-spec benchmark anchors โ both the chokepoint /
|
| 140 |
+
# min-cut anchor (cluster doctrine) AND the perimeter / wide-front
|
| 141 |
+
# anchor (line doctrine) must be declared, since the pack
|
| 142 |
+
# discriminates both.
|
| 143 |
anchors = pack.meta.benchmark_anchor
|
| 144 |
assert any("min-cut" in a.lower() or "chokepoint" in a.lower() for a in anchors), anchors
|
| 145 |
assert any("bunker" in a.lower() for a in anchors), anchors
|
| 146 |
+
assert any("perimeter" in a.lower() for a in anchors), anchors
|
| 147 |
+
# All tiers compile + the per-tier base_map is Rust-loadable.
|
| 148 |
for lvl in LEVELS:
|
| 149 |
c = compile_level(pack, lvl)
|
| 150 |
+
assert c.map_supported, (lvl, c.scenario.base_map)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
|
| 153 |
def test_starting_cash_is_exact_pbox_budget():
|
| 154 |
+
"""The cash is tight (exactly 4 pbox at 600 each, zero slack on
|
| 155 |
+
every tier). A model that spends on units OR extra power runs out
|
| 156 |
+
before the count clause is satisfied โ the topology decision is
|
| 157 |
+
the spend."""
|
| 158 |
pack = load_pack(PACK)
|
| 159 |
+
for lvl in LEVELS:
|
| 160 |
c = compile_level(pack, lvl)
|
| 161 |
+
assert c.starting_cash == 2400, (lvl, c.starting_cash)
|
| 162 |
|
| 163 |
|
| 164 |
@pytest.mark.parametrize("level", LEVELS)
|
|
|
|
| 185 |
"""The fact-survival clause must use the PRESENT-TENSE predicate
|
| 186 |
(`building_count_gte:{type:fact,n:1}`) rather than `has_building`,
|
| 187 |
which is a one-shot "ever seen" set that stays true after the fact
|
| 188 |
+
is destroyed (a documented CLAUDE.md footgun)."""
|
|
|
|
| 189 |
for lvl in LEVELS:
|
| 190 |
c = compile_level(load_pack(PACK), lvl)
|
| 191 |
fc = c.fail_condition.model_dump(exclude_none=True)
|
|
|
|
| 199 |
assert fact_clauses, f"{lvl}: missing present-tense fact-alive fail clause"
|
| 200 |
|
| 201 |
|
| 202 |
+
def test_hard_has_two_seed_driven_spawn_point_groups():
|
| 203 |
+
"""Hard-tier contract: โฅ2 distinct seed-driven spawn_point groups
|
| 204 |
+
so a memorised topology cannot generalise. This pack uses the
|
| 205 |
+
Wave-9 ENEMY-side spawn_point axis (the agent base is fixed
|
| 206 |
+
across all seeds; only the enemy composition flips)."""
|
| 207 |
c = compile_level(load_pack(PACK), "hard")
|
| 208 |
+
enemy_groups = {
|
| 209 |
a.spawn_point for a in c.scenario.actors
|
| 210 |
+
if a.owner == "enemy" and a.spawn_point is not None
|
| 211 |
}
|
| 212 |
+
assert enemy_groups == {0, 1}, enemy_groups
|
| 213 |
+
# In-bounds check (per-tier playable area; the maps are 112x40
|
| 214 |
+
# with cordon=2, so x in 2..109 and y in 2..37).
|
| 215 |
for a in c.scenario.actors:
|
| 216 |
x, y = a.position
|
| 217 |
+
assert 2 <= x <= 109 and 2 <= y <= 37, (a.type, a.position)
|
| 218 |
|
| 219 |
|
| 220 |
+
# โโ solvency: intended topology wins every level + every seed โโโโโโโโ
|
| 221 |
|
| 222 |
|
| 223 |
+
def test_easy_intended_line_wins_every_seed():
|
| 224 |
+
c = compile_level(load_pack(PACK), "easy")
|
| 225 |
+
for seed in SEEDS:
|
| 226 |
+
r = run_level(c, make_line(), seed=seed)
|
| 227 |
+
assert r.outcome == "win", (
|
| 228 |
+
f"easy seed{seed}: intended LINE topology must WIN; "
|
| 229 |
+
f"got {r.outcome} (tick={r.signals.game_tick}, "
|
| 230 |
+
f"kills={r.signals.units_killed}, "
|
| 231 |
+
f"buildings={r.signals.own_buildings})"
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def test_medium_intended_cluster_wins_every_seed():
|
| 236 |
+
c = compile_level(load_pack(PACK), "medium")
|
| 237 |
for seed in SEEDS:
|
| 238 |
r = run_level(c, make_cluster(), seed=seed)
|
| 239 |
assert r.outcome == "win", (
|
| 240 |
+
f"medium seed{seed}: intended CLUSTER topology must WIN; "
|
| 241 |
f"got {r.outcome} (tick={r.signals.game_tick}, "
|
| 242 |
f"kills={r.signals.units_killed}, "
|
|
|
|
| 243 |
f"buildings={r.signals.own_buildings})"
|
| 244 |
)
|
| 245 |
|
| 246 |
|
| 247 |
+
def test_hard_matched_topology_wins_every_seed():
|
| 248 |
+
"""Hard rotates enemy spawn_point per seed: even-indexed
|
| 249 |
+
spawn_point = 0 (concentrated) โ CLUSTER wins; spawn_point = 1
|
| 250 |
+
(wide-front) โ LINE wins. The seed๏ฟฝ๏ฟฝ๏ฟฝspawn_point round-robin in the
|
| 251 |
+
env is seed%2 on enemy-side, so odd seeds โ spawn_point 1 and
|
| 252 |
+
even seeds โ spawn_point 0."""
|
| 253 |
+
c = compile_level(load_pack(PACK), "hard")
|
| 254 |
+
for seed in SEEDS:
|
| 255 |
+
# Try both topologies; at least one must WIN on this seed
|
| 256 |
+
# (the matched one). The pack's no-cheat bar (below) checks
|
| 257 |
+
# the unmatched one LOSES.
|
| 258 |
+
r_cluster = run_level(c, make_cluster(), seed=seed)
|
| 259 |
+
r_line = run_level(c, make_line(), seed=seed)
|
| 260 |
+
wins = [n for n, r in (("cluster", r_cluster), ("line", r_line))
|
| 261 |
+
if r.outcome == "win"]
|
| 262 |
+
assert wins, (
|
| 263 |
+
f"hard seed{seed}: neither topology won (cluster={r_cluster.outcome}, "
|
| 264 |
+
f"line={r_line.outcome}) โ at least one must match the active "
|
| 265 |
+
f"enemy composition"
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
|
| 269 |
# โโ no-cheat: every lazy / wrong-topology policy LOSES (not draws) โโโ
|
| 270 |
|
| 271 |
|
|
|
|
| 273 |
@pytest.mark.parametrize(
|
| 274 |
"policy_name,policy_factory",
|
| 275 |
[
|
| 276 |
+
("stall", lambda lvl: stall),
|
| 277 |
+
("pure_army", lambda lvl: pure_army),
|
|
|
|
| 278 |
],
|
| 279 |
)
|
| 280 |
+
def test_lazy_policies_lose_every_level_and_seed(level, policy_name, policy_factory):
|
| 281 |
+
"""Stall (no pbox built, attackers raze fact or clock runs out)
|
| 282 |
+
and pure-army (never builds a pbox, fails the count clause) must
|
| 283 |
+
BOTH LOSE on every level + every seed โ no draw."""
|
|
|
|
|
|
|
| 284 |
c = compile_level(load_pack(PACK), level)
|
| 285 |
fn = policy_factory(level)
|
| 286 |
for seed in SEEDS:
|
|
|
|
| 292 |
)
|
| 293 |
|
| 294 |
|
| 295 |
+
def test_easy_wrong_topology_cluster_loses_every_seed():
|
| 296 |
+
"""On easy, the LINE topology is intended; a CLUSTER at (60,20)
|
| 297 |
+
fails the four LINE rung clauses (the cluster's pbox cells are all
|
| 298 |
+
far from y=4/12/28/36) โ the win never latches and the episode
|
| 299 |
+
times out โ LOSS."""
|
| 300 |
+
c = compile_level(load_pack(PACK), "easy")
|
| 301 |
+
for seed in SEEDS:
|
| 302 |
+
r = run_level(c, make_cluster(), seed=seed)
|
| 303 |
+
assert r.outcome == "loss", (
|
| 304 |
+
f"easy seed{seed}: wrong-topology CLUSTER must LOSE; "
|
| 305 |
+
f"got {r.outcome} (tick={r.signals.game_tick}, "
|
| 306 |
+
f"kills={r.signals.units_killed}, "
|
| 307 |
+
f"buildings={r.signals.own_buildings})"
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def test_medium_wrong_topology_line_loses_every_seed():
|
| 312 |
+
"""On medium, the CLUSTER topology is intended; a LINE attempt at
|
| 313 |
+
rows y=4/12/28/36 lands on the wall water (the chokepoint-arena's
|
| 314 |
+
corridor is only y=18..22 at xโ60). Only the in-corridor cell
|
| 315 |
+
actually places; the rest are rejected, so the count clause
|
| 316 |
+
fails."""
|
| 317 |
+
c = compile_level(load_pack(PACK), "medium")
|
| 318 |
+
for seed in SEEDS:
|
| 319 |
+
r = run_level(c, make_line(), seed=seed)
|
| 320 |
+
assert r.outcome == "loss", (
|
| 321 |
+
f"medium seed{seed}: wrong-topology LINE must LOSE; "
|
| 322 |
+
f"got {r.outcome} (tick={r.signals.game_tick}, "
|
| 323 |
+
f"kills={r.signals.units_killed}, "
|
| 324 |
+
f"buildings={r.signals.own_buildings})"
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def test_hard_unmatched_topology_loses_on_at_least_one_seed():
|
| 329 |
+
"""On hard, the enemy composition flips by seed. A memorised
|
| 330 |
+
topology that satisfies the region clause (cluster OR line)
|
| 331 |
+
cannot satisfy the kill quota on the unmatched seed โ at least
|
| 332 |
+
one of the four seeds must be a LOSS for EACH topology
|
| 333 |
+
individually."""
|
| 334 |
+
c = compile_level(load_pack(PACK), "hard")
|
| 335 |
+
cluster_losses = sum(
|
| 336 |
+
1 for seed in SEEDS
|
| 337 |
+
if run_level(c, make_cluster(), seed=seed).outcome == "loss"
|
| 338 |
+
)
|
| 339 |
+
line_losses = sum(
|
| 340 |
+
1 for seed in SEEDS
|
| 341 |
+
if run_level(c, make_line(), seed=seed).outcome == "loss"
|
| 342 |
+
)
|
| 343 |
+
assert cluster_losses >= 1, (
|
| 344 |
+
f"hard: CLUSTER must LOSE on at least one of seeds {SEEDS} "
|
| 345 |
+
f"(the wide-front seed); got 0 losses"
|
| 346 |
+
)
|
| 347 |
+
assert line_losses >= 1, (
|
| 348 |
+
f"hard: LINE must LOSE on at least one of seeds {SEEDS} "
|
| 349 |
+
f"(the concentrated seed); got 0 losses"
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
|
| 353 |
# โโ determinism โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 354 |
|
| 355 |
|
| 356 |
def test_intended_run_is_deterministic_on_easy():
|
| 357 |
c = compile_level(load_pack(PACK), "easy")
|
| 358 |
+
a = run_level(c, make_line(), seed=3)
|
| 359 |
+
b = run_level(c, make_line(), seed=3)
|
| 360 |
assert (a.outcome, a.turns, a.signals.units_killed) == (
|
| 361 |
b.outcome,
|
| 362 |
b.turns,
|
|
@@ -1465,7 +1465,7 @@ UPGRADED = [
|
|
| 1465 |
"scout-detect-enemy-tech", # hard: 2 agent spawn_point groups
|
| 1466 |
"coord-converge-on-target", # hard: 2 agent spawn_point groups
|
| 1467 |
"econ-quantitative-vs-qualitative-spend", # hard: 2 agent spawn_point groups
|
| 1468 |
-
"def-tower-line-vs-cluster", # hard: 2
|
| 1469 |
"coord-cover-and-move", # hard: 2 agent spawn_point groups
|
| 1470 |
"combat-kite-and-pull", # hard: 2 agent spawn_point groups (Wave-12)
|
| 1471 |
"econ-tech-vs-expand-decision", # hard: 2 agent spawn_point groups (Wave-12)
|
|
|
|
| 1465 |
"scout-detect-enemy-tech", # hard: 2 agent spawn_point groups
|
| 1466 |
"coord-converge-on-target", # hard: 2 agent spawn_point groups
|
| 1467 |
"econ-quantitative-vs-qualitative-spend", # hard: 2 agent spawn_point groups
|
| 1468 |
+
"def-tower-line-vs-cluster", # hard: 2 ENEMY spawn_point groups (concentrated vs wide-front)
|
| 1469 |
"coord-cover-and-move", # hard: 2 agent spawn_point groups
|
| 1470 |
"combat-kite-and-pull", # hard: 2 agent spawn_point groups (Wave-12)
|
| 1471 |
"econ-tech-vs-expand-decision", # hard: 2 agent spawn_point groups (Wave-12)
|