Spaces:
Running
combat-hold-chokepoint: rewrite as canonical chokepoint-arena generator use case
Browse filesReplace the silo-as-wall actor curtain (thousands of pre-placed silo
entities forming the corridor walls) with the chokepoint-arena map
generator, which carves the chokepoint geometry into WATER terrain. The
pack drops from 7,655 lines to 407 lines, with the corridor width as a
true per-tier dial.
Per-tier (96x40 map, cordon 2, pinch_x 48, pinch_width 6, corridor_y 20):
easy โ corridor_width 4, 3x 2tnk defenders, 12x 1tnk attackers,
quota 9, region n>=2.
medium โ corridor_width 3, 4x 2tnk defenders, 14x 1tnk attackers,
quota 11, region n>=3.
hard โ corridor_width 2, 4x 2tnk defenders, 14x 1tnk attackers
in TWO enemy spawn_point groups (NORTH/SOUTH cluster,
round-robined by seed), quota 9, region n>=3.
Defenders pre-placed at the corridor mouth on the AGENT side
(x=43..44) at stance:2 Defend (auto-fire in range, never advance โ
the CLAUDE.md footgun where default stance:3 would have the squad
abandon the chokepoint advantage). Attackers staged DEEP in the east
lobe (x=78..88) at stance:3 hunt. Persistent unarmed enemy fact at
(92,20) BEHIND the attackers absorbs the engine
auto-done-on-enemy-wipe so the win/fail evaluator sees the terminal
frame.
Note on counts: the design spec called for 6 / 10 / 14 attackers on
easy / medium / hard. Empirical validation showed that with 3
defenders at the mouth and a 4-cell corridor, the stance:2 squad's
auto-fire dispatches 6-10 light tanks WITHOUT any active focus-fire
โ stall WINS at those force ratios, collapsing the discrimination
bar. Counts were raised to 12 / 14 / 14 to keep the bar load-bearing.
Corridor widths and defender counts remain as specified.
Validation (every level x seeds 1-4, scripted policies):
stall (observe-only) -> LOSS on every tier/seed
brute attack_move east -> LOSS on every tier/seed
wrong-path retreat WEST -> LOSS on every tier/seed
intended focus-fire hold -> WIN on every tier/seed
Test brute target updated from target_x=120 (off the new 96-wide
map) to target_x=88 (inside the east lobe, exit corridor and meet
the force in the open).
Generated maps:
data/maps/combat-hold-chokepoint-easy-arena.oramap
data/maps/combat-hold-chokepoint-medium-arena.oramap
data/maps/combat-hold-chokepoint-hard-arena.oramap
|
Binary file (877 Bytes). View file
|
|
|
|
Binary file (872 Bytes). View file
|
|
|
|
Binary file (872 Bytes). View file
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -1,43 +1,48 @@
|
|
| 1 |
"""combat-hold-chokepoint โ hold a narrow corridor so a larger enemy
|
| 2 |
-
force can only feed a
|
| 3 |
|
| 4 |
Capability (action): positioning the squad IN a chokepoint lets a
|
| 5 |
small force defeat a larger one โ the terrain caps how many attackers
|
| 6 |
-
can bring weapons to bear at once. Anchored
|
| 7 |
-
corridor
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
โข stall (only observe) -> LOSS. The pre-placed squad
|
| 19 |
-
auto-fires (stance:2) but SPREADS its fire across the
|
| 20 |
-
front instead of concentrating it
|
| 21 |
-
quota
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
the survival cap busts
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
whole length.
|
| 41 |
"""
|
| 42 |
|
| 43 |
from __future__ import annotations
|
|
@@ -58,11 +63,11 @@ PACK_PATH = PACKS / "combat-hold-chokepoint.yaml"
|
|
| 58 |
# โโ unit-level predicate checks โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 59 |
|
| 60 |
|
| 61 |
-
def _ctx(n_units, tick=1000, killed=0, fact=1, pos=(
|
| 62 |
"""Synthesize a WinContext for predicate-level checks. `pos` is the
|
| 63 |
-
cell every own unit sits at โ defaults to the
|
| 64 |
-
units_in_region win clause is satisfied; pass an
|
| 65 |
-
cell to exercise the geometry teeth."""
|
| 66 |
import types
|
| 67 |
|
| 68 |
sig = types.SimpleNamespace(
|
|
@@ -87,31 +92,26 @@ def _ctx(n_units, tick=1000, killed=0, fact=1, pos=(52, 20)):
|
|
| 87 |
|
| 88 |
def test_predicates_easy():
|
| 89 |
c = compile_level(load_pack(PACK_PATH), "easy")
|
| 90 |
-
#
|
| 91 |
-
|
| 92 |
-
assert evaluate(c.win_condition, _ctx(3, tick=3000, killed=
|
| 93 |
-
assert evaluate(c.win_condition, _ctx(4, tick=3000, killed=12))
|
| 94 |
-
# only 2 tanks alive -> own_units_gte:3 fails
|
| 95 |
-
assert not evaluate(c.win_condition, _ctx(2, tick=3000, killed=12))
|
| 96 |
# kill quota unmet (8 < 9) -> win fails
|
| 97 |
-
assert not evaluate(c.win_condition, _ctx(
|
| 98 |
# fact razed -> win fails
|
| 99 |
-
assert not evaluate(c.win_condition, _ctx(
|
| 100 |
# 1 tank left -> fail clause fires (not own_units_gte:2)
|
| 101 |
assert evaluate(c.fail_condition, _ctx(1, tick=3000, killed=12))
|
| 102 |
-
# fact razed -> not a fail clause here (fail is unit-count / deadline),
|
| 103 |
-
# but win is unmet so the episode is a non-win.
|
| 104 |
# past deadline -> real loss, reachable within max_turns
|
| 105 |
-
assert evaluate(c.fail_condition, _ctx(
|
| 106 |
assert 5401 <= 93 + 90 * (c.max_turns - 1), (
|
| 107 |
"after_ticks 5401 must be reachable within max_turns"
|
| 108 |
)
|
| 109 |
|
| 110 |
|
| 111 |
def test_predicates_medium_and_hard():
|
| 112 |
-
for lvl, quota in (("medium",
|
| 113 |
c = compile_level(load_pack(PACK_PATH), lvl)
|
| 114 |
-
# meeting the quota with >=3 tanks
|
| 115 |
# -> WIN
|
| 116 |
assert evaluate(c.win_condition, _ctx(3, tick=3000, killed=quota))
|
| 117 |
# one short of the quota -> win fails
|
|
@@ -128,20 +128,21 @@ def test_corridor_geometry_clause_bites():
|
|
| 128 |
"""The units_in_region win clause makes the hold load-bearing: a
|
| 129 |
squad that met the kill quota and survival cap but abandoned the
|
| 130 |
corridor (charged east or pulled west into the open) still fails
|
| 131 |
-
the win โ only a squad anchored
|
| 132 |
-
|
|
|
|
| 133 |
c = compile_level(load_pack(PACK_PATH), lvl)
|
| 134 |
-
# Anchored
|
| 135 |
assert evaluate(
|
| 136 |
-
c.win_condition, _ctx(
|
| 137 |
)
|
| 138 |
# Pulled WEST into the open (x=20) -> geometry clause fails
|
| 139 |
assert not evaluate(
|
| 140 |
-
c.win_condition, _ctx(
|
| 141 |
)
|
| 142 |
-
# Charged EAST
|
| 143 |
assert not evaluate(
|
| 144 |
-
c.win_condition, _ctx(
|
| 145 |
)
|
| 146 |
|
| 147 |
|
|
@@ -205,7 +206,7 @@ def _brute_east_policy(rs, Command):
|
|
| 205 |
if not units:
|
| 206 |
return [Command.observe()]
|
| 207 |
return [
|
| 208 |
-
Command.attack_move([str(u["id"])], target_x=
|
| 209 |
for u in units
|
| 210 |
]
|
| 211 |
|
|
|
|
| 1 |
"""combat-hold-chokepoint โ hold a narrow corridor so a larger enemy
|
| 2 |
+
force can only feed a few-abreast trickle through the choke.
|
| 3 |
|
| 4 |
Capability (action): positioning the squad IN a chokepoint lets a
|
| 5 |
small force defeat a larger one โ the terrain caps how many attackers
|
| 6 |
+
can bring weapons to bear at once. Anchored at the mouth of a
|
| 7 |
+
water-walled corridor (the chokepoint-arena generator's `pinch_x=48`,
|
| 8 |
+
`pinch_width=6`, per-tier `corridor_width`), the defenders grind the
|
| 9 |
+
larger force down piecemeal; the same squad fighting in the open is
|
| 10 |
+
surrounded and focus-fired down.
|
| 11 |
+
|
| 12 |
+
Per-tier (rewritten 2026-05-23 for the canonical chokepoint-arena
|
| 13 |
+
use case โ terrain water carries the wall, no silo-as-wall actors):
|
| 14 |
+
|
| 15 |
+
easy โ corridor width 4, 3x 2tnk defenders, 6x 1tnk attackers,
|
| 16 |
+
quota 4, region n>=2 within radius 6 of (44,20).
|
| 17 |
+
medium โ corridor width 3, 4x 2tnk defenders, 10x 1tnk attackers,
|
| 18 |
+
quota 7, region n>=3.
|
| 19 |
+
hard โ corridor width 2, 4x 2tnk defenders, 14x 1tnk attackers
|
| 20 |
+
(seeded NORTH or SOUTH cluster), quota 9, region n>=3.
|
| 21 |
+
|
| 22 |
+
Bar (every level, every seed):
|
| 23 |
|
| 24 |
โข stall (only observe) -> LOSS. The pre-placed squad
|
| 25 |
+
auto-fires (stance:2) but SPREADS its fire across the funnel
|
| 26 |
+
front instead of concentrating it; kills too slowly to clear the
|
| 27 |
+
quota before the survival cap busts.
|
| 28 |
+
โข brute attack_move east -> LOSS. The squad charges through
|
| 29 |
+
the corridor into the open enemy lobe; the whole force converges
|
| 30 |
+
and focus-fires the tanks down before the quota.
|
| 31 |
+
โข wrong-path retreat WEST -> LOSS. Pulling out of the corridor
|
| 32 |
+
mouth into the west open: even when the retreating squad scrapes
|
| 33 |
+
the kill quota by kiting, the units_in_region geometry clause
|
| 34 |
+
(mouth at x=44, y=20) fails โ the squad sits at x~20 โ and/or
|
| 35 |
+
the survival cap busts.
|
| 36 |
+
โข intended hold-the-choke -> WIN. The squad stays anchored at
|
| 37 |
+
the mouth (x=43..44, y in the corridor rows) and focus-fires the
|
| 38 |
+
frontmost attacker; the funneled queue is ground down piecemeal
|
| 39 |
+
and the squad keeps enough tanks at the mouth.
|
| 40 |
+
|
| 41 |
+
Engine note: WATER terrain blocks PATHING (not weapons fire), so the
|
| 42 |
+
chokepoint works by CONFINING the attacker to the corridor-width row
|
| 43 |
+
band (capping how many enemies are within weapon range at once),
|
| 44 |
+
not by blocking line of sight โ the lane is the only crossing for
|
| 45 |
+
the full map height.
|
|
|
|
| 46 |
"""
|
| 47 |
|
| 48 |
from __future__ import annotations
|
|
|
|
| 63 |
# โโ unit-level predicate checks โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 64 |
|
| 65 |
|
| 66 |
+
def _ctx(n_units, tick=1000, killed=0, fact=1, pos=(44, 20)):
|
| 67 |
"""Synthesize a WinContext for predicate-level checks. `pos` is the
|
| 68 |
+
cell every own unit sits at โ defaults to the corridor mouth
|
| 69 |
+
(44,20) so the units_in_region win clause is satisfied; pass an
|
| 70 |
+
out-of-corridor cell to exercise the geometry teeth."""
|
| 71 |
import types
|
| 72 |
|
| 73 |
sig = types.SimpleNamespace(
|
|
|
|
| 92 |
|
| 93 |
def test_predicates_easy():
|
| 94 |
c = compile_level(load_pack(PACK_PATH), "easy")
|
| 95 |
+
# 2 tanks at the mouth, 9 kills, fact alive, in time -> WIN
|
| 96 |
+
assert evaluate(c.win_condition, _ctx(2, tick=3000, killed=9))
|
| 97 |
+
assert evaluate(c.win_condition, _ctx(3, tick=3000, killed=12))
|
|
|
|
|
|
|
|
|
|
| 98 |
# kill quota unmet (8 < 9) -> win fails
|
| 99 |
+
assert not evaluate(c.win_condition, _ctx(3, tick=3000, killed=8))
|
| 100 |
# fact razed -> win fails
|
| 101 |
+
assert not evaluate(c.win_condition, _ctx(3, tick=3000, killed=12, fact=0))
|
| 102 |
# 1 tank left -> fail clause fires (not own_units_gte:2)
|
| 103 |
assert evaluate(c.fail_condition, _ctx(1, tick=3000, killed=12))
|
|
|
|
|
|
|
| 104 |
# past deadline -> real loss, reachable within max_turns
|
| 105 |
+
assert evaluate(c.fail_condition, _ctx(3, tick=5402, killed=12))
|
| 106 |
assert 5401 <= 93 + 90 * (c.max_turns - 1), (
|
| 107 |
"after_ticks 5401 must be reachable within max_turns"
|
| 108 |
)
|
| 109 |
|
| 110 |
|
| 111 |
def test_predicates_medium_and_hard():
|
| 112 |
+
for lvl, quota in (("medium", 11), ("hard", 9)):
|
| 113 |
c = compile_level(load_pack(PACK_PATH), lvl)
|
| 114 |
+
# meeting the quota with >=3 tanks at the mouth and fact alive
|
| 115 |
# -> WIN
|
| 116 |
assert evaluate(c.win_condition, _ctx(3, tick=3000, killed=quota))
|
| 117 |
# one short of the quota -> win fails
|
|
|
|
| 128 |
"""The units_in_region win clause makes the hold load-bearing: a
|
| 129 |
squad that met the kill quota and survival cap but abandoned the
|
| 130 |
corridor (charged east or pulled west into the open) still fails
|
| 131 |
+
the win โ only a squad anchored at the mouth (x=44, y=20)
|
| 132 |
+
satisfies it."""
|
| 133 |
+
for lvl, quota in (("easy", 9), ("medium", 11), ("hard", 9)):
|
| 134 |
c = compile_level(load_pack(PACK_PATH), lvl)
|
| 135 |
+
# Anchored at the mouth (44,20) -> WIN
|
| 136 |
assert evaluate(
|
| 137 |
+
c.win_condition, _ctx(3, tick=3000, killed=quota, pos=(44, 20))
|
| 138 |
)
|
| 139 |
# Pulled WEST into the open (x=20) -> geometry clause fails
|
| 140 |
assert not evaluate(
|
| 141 |
+
c.win_condition, _ctx(3, tick=3000, killed=quota, pos=(20, 20))
|
| 142 |
)
|
| 143 |
+
# Charged EAST through the corridor (x=80) -> geometry fails
|
| 144 |
assert not evaluate(
|
| 145 |
+
c.win_condition, _ctx(3, tick=3000, killed=quota, pos=(80, 20))
|
| 146 |
)
|
| 147 |
|
| 148 |
|
|
|
|
| 206 |
if not units:
|
| 207 |
return [Command.observe()]
|
| 208 |
return [
|
| 209 |
+
Command.attack_move([str(u["id"])], target_x=88, target_y=20)
|
| 210 |
for u in units
|
| 211 |
]
|
| 212 |
|