yxc20098 commited on
Commit
73e261f
ยท
1 Parent(s): 6129f0d

combat-hold-chokepoint: rewrite as canonical chokepoint-arena generator use case

Browse files

Replace 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

data/maps/combat-hold-chokepoint-easy-arena.oramap ADDED
Binary file (877 Bytes). View file
 
data/maps/combat-hold-chokepoint-hard-arena.oramap ADDED
Binary file (872 Bytes). View file
 
data/maps/combat-hold-chokepoint-medium-arena.oramap ADDED
Binary file (872 Bytes). View file
 
openra_bench/scenarios/packs/combat-hold-chokepoint.yaml CHANGED
The diff for this file is too large to render. See raw diff
 
tests/test_combat_hold_chokepoint.py CHANGED
@@ -1,43 +1,48 @@
1
  """combat-hold-chokepoint โ€” hold a narrow corridor so a larger enemy
2
- force can only feed a 3-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 in a 3-cell silo-walled
7
- corridor, 4 medium tanks grind a 12-16 strong light-tank force down
8
- piecemeal; the same squad fighting in the open is surrounded and
9
- focus-fired down.
10
-
11
- Bar (recalibrated 2026-05-20 after the OpenRA-Rust engine movement
12
- fixes โ€” moving units now fire and take fire en route, and attack_unit
13
- on an out-of-sight target paths normally. Those fixes made a static
14
- auto-firing squad strong enough that the old 12-tank / quota-8 easy
15
- tier was beatable by a pure stall; the recalibration bumps easy to 14
16
- attackers + quota 9 and adds a corridor-geometry win clause):
 
 
 
 
 
 
17
 
18
  โ€ข stall (only observe) -> LOSS. The pre-placed squad
19
- auto-fires (stance:2) but SPREADS its fire across the 3-abreast
20
- front instead of concentrating it, kills too slowly (~7-8 < the
21
- quota of 9), and is worn down -> own_units_gte:2 busts AND the
22
- kill quota is unmet (killed 7-8, lost 3-4).
23
- โ€ข brute attack_move east -> LOSS. The squad charges out of the
24
- corridor into the open; the whole force converges and focus-fires
25
- all 4 tanks -> loss cap busted before the kill quota (killed 0-1,
26
- lost 3-4), and the squad is no longer in the choke region.
27
- โ€ข wrong-path retreat WEST -> LOSS. Pulling the squad west out of
28
- the corridor into the open: even when it scrapes the kill quota
29
- the survival cap busts and the units_in_region geometry clause
30
- fails (the squad is at x~20, not in the lane).
31
- โ€ข intended hold-the-choke -> WIN. The squad stays anchored in
32
- the corridor and focus-fires the frontmost enemy; the 3-abreast
33
- funnel is ground down; the squad keeps >=3 tanks in the choke
34
- region and hits the kill quota (killed 9-10, lost 0-1).
35
-
36
- Engine note: `silo` walls block PATHING but not weapons fire, so the
37
- chokepoint works by CONFINING the attacker to the 3-row lane (capping
38
- how many enemies are within weapon range at once), not by blocking
39
- line of sight โ€” this is why the corridor brackets the lane along its
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=(52, 20)):
62
  """Synthesize a WinContext for predicate-level checks. `pos` is the
63
- cell every own unit sits at โ€” defaults to the choke (52,20) so the
64
- units_in_region win clause is satisfied; pass an out-of-corridor
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
- # 3 tanks alive (in the choke region), 9 kills, fact alive, in
91
- # time -> WIN
92
- assert evaluate(c.win_condition, _ctx(3, tick=3000, killed=9))
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(4, tick=3000, killed=8))
98
  # fact razed -> win fails
99
- assert not evaluate(c.win_condition, _ctx(4, tick=3000, killed=12, fact=0))
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(4, 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", 9), ("hard", 9)):
113
  c = compile_level(load_pack(PACK_PATH), lvl)
114
- # meeting the quota with >=3 tanks in the choke 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,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 in the choke satisfies it."""
132
- for lvl, quota in (("easy", 9), ("medium", 9), ("hard", 9)):
 
133
  c = compile_level(load_pack(PACK_PATH), lvl)
134
- # Anchored in the choke (52,20) -> WIN
135
  assert evaluate(
136
- c.win_condition, _ctx(4, tick=3000, killed=quota, pos=(52, 20))
137
  )
138
  # Pulled WEST into the open (x=20) -> geometry clause fails
139
  assert not evaluate(
140
- c.win_condition, _ctx(4, tick=3000, killed=quota, pos=(20, 20))
141
  )
142
- # Charged EAST out of the corridor (x=100) -> geometry fails
143
  assert not evaluate(
144
- c.win_condition, _ctx(4, tick=3000, killed=quota, pos=(100, 20))
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=120, target_y=20)
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