yxc20098 commited on
Commit
f3861b8
·
1 Parent(s): 74217dc

fix(scenario): coord-cover-and-move — recalibrate after engine movement fixes

Browse files
openra_bench/scenarios/packs/coord-cover-and-move.yaml CHANGED
@@ -14,18 +14,20 @@
14
  # also cross. Both squads converge on the eastern gathering region
15
  # having routed AROUND the fire zone rather than through it.
16
  #
17
- # Engine balance pass (armor-class weapon selection + stance + parallel
18
- # production + pbox-fires fixes): the original design assumed a dense
19
- # anti-tank-infantry cluster could out-trade a 6-tank charge. Post-fix,
20
- # medium tanks decisively OUT-DPS infantry a 6-tank column charging
21
- # even a 24-strong e3 swarm one-shots the rockets and crosses with
22
- # ZERO losses, collapsing the "mass-charge loses" discriminator. The
23
- # fire zone is therefore now ANCHORED BY 4tnk HEAVY TANKS (1 on easy,
24
- # 2 on medium/hard): the heavy-tank brawl is what genuinely punishes a
25
- # centre charge, while the e3/e1 infantry keep the anti-tank-infantry
26
- # doctrine and the range geometry meaningful. All enemy units are
27
- # stance:2 Defend (auto-fire in range but STATIONARY) so a staller is
28
- # a clean timeout LOSS rather than chased down by hunters.
 
 
29
  #
30
  # Real-world anchor: US Army FM 3-21.8 bounding overwatch — one fire
31
  # team in a stationary OVERWATCH posture provides suppressive fire
@@ -39,8 +41,8 @@
39
  # • stall (observe-only): no squad crosses → win region empty →
40
  # after_ticks timeout LOSS.
41
  # • both-charge-together: all 6 tanks rush a single eastward column
42
- # straight through the centre fire zone; the heavy-tank brawl plus
43
- # the rocket fire bust the attrition cap → LOSS.
44
  # • single-squad-only: send Squad A through alone (Squad B idle
45
  # west); A absorbs the centre fire AND half the force never
46
  # reaches the win region (n=4 region clause unreachable) → LOSS.
@@ -52,16 +54,17 @@
52
  # deadline losing ≤ the cap → WIN.
53
  #
54
  # Why the spec works (engine combat sheet):
55
- # • 4tnk = heavy/mammoth tank high HP and a heavy cannon; its
56
- # brawl is what kills ≥2 of a 6-tank column that charges through
57
- # the centre. Held at the cluster centre (y=20) it reaches a
58
- # charging column on the y≈14..16 / y≈24..26 lanes.
59
- # • e3 = anti-tank rocket soldier (rng4c, sight5c) reinforces the
60
- # centre fire zone and keeps the overwatch range geometry (a tank
61
- # marginally out-ranges e3) meaningful, but does NOT on its own
62
- # out-trade a tank column.
63
  # • 2tnk = medium tank (rng4.75c, sight6c, hp400) — out-DPSes
64
- # infantry but is brawled down by the 4tnk anchor in the centre.
 
65
  # • Periphery geometry: the centre fire zone occupies y≈13..27. A
66
  # squad that rises to y≈6 (NORTH) or y≈34 (SOUTH) BEFORE reaching
67
  # the fire zone's longitude (x≈50) routes entirely outside the
@@ -133,30 +136,33 @@ base:
133
 
134
  levels:
135
  # ── EASY ─────────────────────────────────────────────────────────
136
- # Bare bound-and-cover skill: a 4× e1 rifle-infantry cluster at the
137
- # centre anchored by ONE 4tnk heavy tank. Engine balance pass: the
138
- # post-armor-fix infantry can no longer kill a column of 6 medium
139
- # tanks (they out-DPS the e1/e3 cluster outright), so the fire zone
140
- # is anchored by a 4tnk heavy tank its brawl is what punishes a
141
- # mass charge. A both-charge straight through the centre clips the
142
- # 4tnk's brawl and loses 2 tanks; stall times out; single-squad
143
- # leaves half the force idle. The intended bound-and-cover routes
144
- # both squads through the far periphery (y≈6 / y≈34, outside the
145
- # centre fire zone) and converges east taking zero losses.
 
 
 
146
  easy:
147
  description: >
148
  Two squads (Squad A at (10,15), Squad B at (10,25), 3 medium
149
- tanks each) must cross a centre-of-map fire zone — a rifle-
150
- infantry (e1) cluster anchored by one heavy tank (4tnk) at
151
- roughly (50,20) — then land ≥4 tanks in the EAST gathering
152
- region (x ≈ 100, y ≈ 20, radius 6) before tick 4500, losing ≤1
153
- tank in total. Charging both squads straight through the centre
154
- drives them into the heavy tank's brawl and busts the cap; a
155
- single column alone absorbs every shot AND leaves half the
156
- force behind. Alternate ROLES: one squad anchors at the edge of
157
- enemy range to draw fire while the other sweeps through the
158
- periphery (y outside the centre fire zone) and crosses; then
159
- switch.
160
  overrides:
161
  actors:
162
  # Squad A — northern fire team, staged at (10, 15).
@@ -167,17 +173,17 @@ levels:
167
  - {type: 2tnk, owner: agent, position: [10, 24], stance: 1}
168
  - {type: 2tnk, owner: agent, position: [10, 25], stance: 1}
169
  - {type: 2tnk, owner: agent, position: [10, 26], stance: 1}
170
- # Centre fire zone: 4× rifle infantry + 1 heavy-tank anchor,
171
- # all stance:2 Defend (auto-fire in range but STATIONARY — so
172
- # a stall policy is a clean timeout LOSS, not an attrition
173
- # one). Cells avoid (50,20) — CLAUDE.md footgun: enemy
174
- # clusters at (50,20) silently fail to place. Off-axis
175
  # neighbours (49/51 × 19/21) are verified-placeable.
176
- - {type: e1, owner: enemy, position: [49, 19], stance: 2}
177
- - {type: e1, owner: enemy, position: [49, 21], stance: 2}
178
- - {type: e1, owner: enemy, position: [51, 19], stance: 2}
179
- - {type: e1, owner: enemy, position: [51, 21], stance: 2}
180
- - {type: 4tnk, owner: enemy, position: [50, 20], stance: 2}
181
  # Persistent far enemy `fact` — keeps the episode alive past
182
  # the central cluster being cleaned out so the win/fail
183
  # evaluator sees the terminal frame (no DRAW collapse).
@@ -195,14 +201,15 @@ levels:
195
  max_turns: 50
196
 
197
  # ── MEDIUM ───────────────────────────────────────────────────────
198
- # +1 axis vs easy: ANTI-TANK rocket soldiers (e3) replace the rifle
199
- # infantry AND the heavy-tank anchor doubles to TWO 4tnk. The fire
200
- # zone is genuinely lethal to a 2tnk column crossing the centre.
201
- # Both-charge-together LOSES (the centre brawl busts units_lost_
202
- # lte:1); single-squad alone LOSES (lone column absorbs the centre
203
- # fire AND half the force never reaches the win region). Only true
204
- # bound-and-cover (route the squads through the far periphery,
205
- # alternating cover/move roles) lands ≥4 tanks losing ≤1.
 
206
  medium:
207
  description: >
208
  Two squads (Squad A at (10,15), Squad B at (10,25), 3 medium
 
14
  # also cross. Both squads converge on the eastern gathering region
15
  # having routed AROUND the fire zone rather than through it.
16
  #
17
+ # Engine movement-fix recalibration ((A) attack_unit on out-of-sight
18
+ # targets paths normally no teleport; (B) a moving unit fires AND
19
+ # takes fire en route — no sprint-invincibility): a 6-tank column that
20
+ # attack_moves straight through the centre now genuinely trades fire
21
+ # with the cluster while crossing. The load-bearing punisher is the
22
+ # combination of e3 ANTI-TANK ROCKET soldiers (the real anti-armour
23
+ # DPS) AND 4tnk HEAVY-TANK anchors: probing shows e3 rockets alone or
24
+ # a single 4tnk only cost a charging column ONE tank, but FOUR e3 plus
25
+ # TWO 4tnk reliably cost it TWO. Every level therefore fields an e3
26
+ # rocket cluster anchored by TWO 4tnk (easy uses a smaller 3× e3
27
+ # cluster; medium/hard a e3 cluster, hard adds e1 rifle screens).
28
+ # All enemy units are stance:2 Defend (auto-fire in range but
29
+ # STATIONARY) so a staller is a clean timeout LOSS rather than being
30
+ # chased down by hunters.
31
  #
32
  # Real-world anchor: US Army FM 3-21.8 bounding overwatch — one fire
33
  # team in a stationary OVERWATCH posture provides suppressive fire
 
41
  # • stall (observe-only): no squad crosses → win region empty →
42
  # after_ticks timeout LOSS.
43
  # • both-charge-together: all 6 tanks rush a single eastward column
44
+ # straight through the centre fire zone; the rocket fire plus the
45
+ # heavy-tank brawl bust the attrition cap → LOSS.
46
  # • single-squad-only: send Squad A through alone (Squad B idle
47
  # west); A absorbs the centre fire AND half the force never
48
  # reaches the win region (n=4 region clause unreachable) → LOSS.
 
54
  # deadline losing ≤ the cap → WIN.
55
  #
56
  # Why the spec works (engine combat sheet):
57
+ # • e3 = anti-tank rocket soldier (rng4c, sight5c) the real
58
+ # anti-armour DPS of the fire zone; a e3 cluster plus the two
59
+ # heavy-tank anchors is what kills ≥2 of a 6-tank column charging
60
+ # through the centre.
61
+ # • 4tnk = heavy/mammoth tank high HP and a heavy cannon; the
62
+ # SECOND 4tnk anchor is what pushes a charging column's losses
63
+ # from one tank to two. Held at the cluster centre (y=20) it
64
+ # reaches a charging column on the y≈14..16 / y≈24..26 lanes.
65
  # • 2tnk = medium tank (rng4.75c, sight6c, hp400) — out-DPSes
66
+ # infantry but is ground down by the e3 rockets + 4tnk anchors
67
+ # when it crosses the centre.
68
  # • Periphery geometry: the centre fire zone occupies y≈13..27. A
69
  # squad that rises to y≈6 (NORTH) or y≈34 (SOUTH) BEFORE reaching
70
  # the fire zone's longitude (x≈50) routes entirely outside the
 
136
 
137
  levels:
138
  # ── EASY ─────────────────────────────────────────────────────────
139
+ # Bare bound-and-cover skill: a 3× e3 anti-tank-rocket cluster at the
140
+ # centre anchored by TWO 4tnk heavy tanks. Engine movement-fix
141
+ # recalibration: a charging column now genuinely trades fire while
142
+ # crossing probing shows e3 rockets are the real anti-armour DPS,
143
+ # but it takes TWO heavy-tank anchors alongside them to cost a
144
+ # 6-tank charge TWO tanks. A both-charge straight through the centre
145
+ # eats the e3 rockets + the heavy-tank brawl and loses 2 tanks
146
+ # (busts the cap); stall times out; single-squad leaves half the
147
+ # force idle AND loses 2 crossing. The intended bound-and-cover
148
+ # routes both squads through the far periphery (y≈6 / y≈34, outside
149
+ # the centre fire zone) and converges east — taking zero losses.
150
+ # Easy is the FORGIVING tier: a smaller 3× e3 cluster (medium/hard
151
+ # field 4× e3), so the bounding-lane geometry is the bare skill.
152
  easy:
153
  description: >
154
  Two squads (Squad A at (10,15), Squad B at (10,25), 3 medium
155
+ tanks each) must cross a centre-of-map fire zone — a small
156
+ anti-tank rocket-soldier (e3) cluster anchored by two heavy
157
+ tanks (4tnk) at roughly (50,20) — then land ≥4 tanks in the
158
+ EAST gathering region (x ≈ 100, y ≈ 20, radius 6) before tick
159
+ 4500, losing ≤1 tank in total. Charging both squads straight
160
+ through the centre drives them into the rocket fire and the
161
+ heavy-tank brawl and busts the cap; a single column alone
162
+ absorbs every shot AND leaves half the force behind. Alternate
163
+ ROLES: one squad anchors at the edge of enemy range to draw
164
+ fire while the other sweeps through the periphery (y outside
165
+ the centre fire zone) and crosses; then switch.
166
  overrides:
167
  actors:
168
  # Squad A — northern fire team, staged at (10, 15).
 
173
  - {type: 2tnk, owner: agent, position: [10, 24], stance: 1}
174
  - {type: 2tnk, owner: agent, position: [10, 25], stance: 1}
175
  - {type: 2tnk, owner: agent, position: [10, 26], stance: 1}
176
+ # Centre fire zone: 3× anti-tank rocket soldiers + 2 heavy-
177
+ # tank anchors, all stance:2 Defend (auto-fire in range but
178
+ # STATIONARY — so a stall policy is a clean timeout LOSS, not
179
+ # an attrition one). Cells avoid (50,20) — CLAUDE.md footgun:
180
+ # enemy clusters at (50,20) silently fail to place. Off-axis
181
  # neighbours (49/51 × 19/21) are verified-placeable.
182
+ - {type: e3, owner: enemy, position: [49, 19], stance: 2}
183
+ - {type: e3, owner: enemy, position: [49, 21], stance: 2}
184
+ - {type: e3, owner: enemy, position: [51, 21], stance: 2}
185
+ - {type: 4tnk, owner: enemy, position: [49, 20], stance: 2}
186
+ - {type: 4tnk, owner: enemy, position: [51, 19], stance: 2}
187
  # Persistent far enemy `fact` — keeps the episode alive past
188
  # the central cluster being cleaned out so the win/fail
189
  # evaluator sees the terminal frame (no DRAW collapse).
 
201
  max_turns: 50
202
 
203
  # ── MEDIUM ───────────────────────────────────────────────────────
204
+ # +1 axis vs easy: the e3 anti-tank rocket cluster grows from 3 to
205
+ # 4 (a wider, denser fire zone) while keeping the TWO 4tnk anchors.
206
+ # The fire zone is genuinely lethal to a 2tnk column crossing the
207
+ # centre. Both-charge-together LOSES (the centre brawl + rockets
208
+ # bust units_lost_lte:1); single-squad alone LOSES (lone column
209
+ # absorbs the centre fire AND half the force never reaches the win
210
+ # region). Only true bound-and-cover (route the squads through the
211
+ # far periphery, alternating cover/move roles) lands ≥4 tanks
212
+ # losing ≤1.
213
  medium:
214
  description: >
215
  Two squads (Squad A at (10,15), Squad B at (10,25), 3 medium
tests/test_coord_cover_and_move.py CHANGED
@@ -3,7 +3,7 @@
3
  The bar (binding):
4
  - stall LOSES on every level + every seed (after_ticks timeout);
5
  - both-charge-through-the-centre LOSES on every level + every seed
6
- (the heavy-tank brawl busts the attrition cap);
7
  - single-squad-only LOSES on every level + every seed (half the force
8
  idle → the n=4 region clause is unreachable);
9
  - the intended bound-and-cover (periphery route around the fire zone)
@@ -12,18 +12,20 @@ The bar (binding):
12
  clause (no DRAW degeneracy);
13
  - hard ships ≥2 `spawn_point` groups (seed-driven start variation).
14
 
15
- Recalibrated after the engine balance pass (armor-class weapon
16
- selection + stance + parallel-production + pbox-fires fixes): the
17
- original design assumed a dense anti-tank-infantry cluster could
18
- out-trade a 6-tank charge, but post-fix medium tanks decisively
19
- out-DPS infantry a 6-tank column charging even a 24-strong e3
20
- swarm one-shots the rockets and crosses with ZERO losses, collapsing
21
- the "mass-charge loses" discriminator. The fire zone is now ANCHORED
22
- BY 4tnk HEAVY TANKS (1 easy / 2 medium+hard): the heavy-tank brawl is
23
- what punishes a centre charge, while the e3/e1 infantry keep the
24
- anti-tank-infantry doctrine and the range geometry meaningful. All
25
- enemy units are stance:2 Defend (auto-fire in range, STATIONARY) so a
26
- staller is a clean timeout LOSS, not chased down by hunters.
 
 
27
 
28
  Validation is scripted (no model / network).
29
  """
@@ -187,36 +189,39 @@ def test_two_squads_six_tanks_each_level():
187
  )
188
 
189
 
190
- def test_fire_zone_uses_anti_tank_rocket_on_medium_and_hard():
191
- """The fire zone's lethality is the load-bearing property —
192
- medium / hard must use e3 (anti-tank rocket soldier) at the
193
- centre cluster. Easy may use e1 (forgiving)."""
 
 
194
  pack = load_pack(PACK_PATH)
195
- for lvl in ("medium", "hard"):
196
  c = compile_level(pack, lvl)
197
  types = [a.type for a in c.scenario.actors if a.owner == "enemy"]
198
- assert types.count("e3") >= 4, (
199
- f"{lvl}: need ≥4 e3 (anti-tank rocket) for fire zone; got {types}"
 
200
  )
201
  # Persistent far enemy marker (engine auto-done mitigation).
202
  assert "fact" in types, f"{lvl}: needs persistent enemy fact"
203
 
204
 
205
- def test_fire_zone_anchored_by_heavy_tank():
206
- """Post-engine-balance-pass invariant: medium tanks out-DPS
207
- infantry, so the centre fire zone is anchored by 4tnk heavy
208
- tank(s) the heavy-tank brawl is what punishes a centre charge.
209
- Every level must carry at least one enemy 4tnk; medium and hard
210
- carry two."""
211
  pack = load_pack(PACK_PATH)
212
- for lvl, want in (("easy", 1), ("medium", 2), ("hard", 2)):
213
  c = compile_level(pack, lvl)
214
  n4tnk = sum(
215
  1 for a in c.scenario.actors
216
  if a.owner == "enemy" and a.type == "4tnk"
217
  )
218
- assert n4tnk >= want, (
219
- f"{lvl}: fire zone must be anchored by ≥{want} enemy 4tnk "
220
  f"(heavy-tank brawl), got {n4tnk}"
221
  )
222
 
 
3
  The bar (binding):
4
  - stall LOSES on every level + every seed (after_ticks timeout);
5
  - both-charge-through-the-centre LOSES on every level + every seed
6
+ (the rocket fire + heavy-tank brawl bust the attrition cap);
7
  - single-squad-only LOSES on every level + every seed (half the force
8
  idle → the n=4 region clause is unreachable);
9
  - the intended bound-and-cover (periphery route around the fire zone)
 
12
  clause (no DRAW degeneracy);
13
  - hard ships ≥2 `spawn_point` groups (seed-driven start variation).
14
 
15
+ Recalibrated after the engine movement fixes ((A) attack_unit on
16
+ out-of-sight targets paths normally no teleport; (B) a moving unit
17
+ fires AND takes fire en route — no sprint-invincibility). A 6-tank
18
+ column that attack_moves through the centre now genuinely trades fire
19
+ while crossing. Probing showed the e1 rifle cluster on the old easy
20
+ tier could no longer punish the charge at all (0 losses): the
21
+ load-bearing punisher is e3 ANTI-TANK ROCKET soldiers (the real
22
+ anti-armour DPS) PLUS two 4tnk HEAVY-TANK anchors e3 rockets alone,
23
+ or a single 4tnk, only cost a charge ONE tank; four e3 plus two 4tnk
24
+ cost it TWO. Every level now fields an e3 rocket cluster anchored by
25
+ TWO 4tnk (easy a smaller e3 cluster, medium/hard a 4× e3 cluster,
26
+ hard adds e1 rifle screens). All enemy units are stance:2 Defend
27
+ (auto-fire in range, STATIONARY) so a staller is a clean timeout
28
+ LOSS, not chased down by hunters.
29
 
30
  Validation is scripted (no model / network).
31
  """
 
189
  )
190
 
191
 
192
+ def test_fire_zone_uses_anti_tank_rocket_every_level():
193
+ """The fire zone's lethality is the load-bearing property — post
194
+ engine movement fixes the e3 (anti-tank rocket soldier) cluster is
195
+ the real anti-armour DPS, so EVERY level uses e3 at the centre.
196
+ Easy fields a smaller cluster (≥3 e3, the forgiving tier);
197
+ medium / hard a denser one (≥4 e3)."""
198
  pack = load_pack(PACK_PATH)
199
+ for lvl, want_e3 in (("easy", 3), ("medium", 4), ("hard", 4)):
200
  c = compile_level(pack, lvl)
201
  types = [a.type for a in c.scenario.actors if a.owner == "enemy"]
202
+ assert types.count("e3") >= want_e3, (
203
+ f"{lvl}: need ≥{want_e3} e3 (anti-tank rocket) for fire "
204
+ f"zone; got {types}"
205
  )
206
  # Persistent far enemy marker (engine auto-done mitigation).
207
  assert "fact" in types, f"{lvl}: needs persistent enemy fact"
208
 
209
 
210
+ def test_fire_zone_anchored_by_two_heavy_tanks():
211
+ """Recalibration invariant: probing showed the e3 rockets alone or
212
+ a single 4tnk only cost a 6-tank charge ONE tank — it takes TWO
213
+ 4tnk anchors alongside the rocket cluster to reliably cost the
214
+ charge TWO. Every level therefore anchors the fire zone with two
215
+ enemy 4tnk heavy tanks."""
216
  pack = load_pack(PACK_PATH)
217
+ for lvl in ("easy", "medium", "hard"):
218
  c = compile_level(pack, lvl)
219
  n4tnk = sum(
220
  1 for a in c.scenario.actors
221
  if a.owner == "enemy" and a.type == "4tnk"
222
  )
223
+ assert n4tnk >= 2, (
224
+ f"{lvl}: fire zone must be anchored by ≥2 enemy 4tnk "
225
  f"(heavy-tank brawl), got {n4tnk}"
226
  )
227