Xiaochuang Yuan commited on
Commit
467b8fe
ยท
1 Parent(s): 346b5f3

combat-bait-counter-attack: medium/hard maps reinforce north-flank strike

Browse files

Medium and hard tiers (the L-cover variants) now run on tailored
128x48 arenas that physically reinforce the north-flank strike
doctrine. The actor layouts, predicates, and attrition caps are
unchanged; the new maps add a WATER wall y=16..23 across x=10..68
that closes the head-on engagement axis so a brute attack_move east
on y=20 is geometrically impossible โ€” the strike force MUST swing
through the open NORTH band (y=2..15 above the wall) to reach the
yard from above the displaced cover.

Map design (mapgen 'arena' generator + obstacle clusters):

* easy โ€” retains the pack-level rush-hour-arena. The 3-guard
west-face idiom works on the open arena because the
bait jeep pulls ALL three guards south, and a head-on
east drive sees an undefended face by the time it
arrives. No tailored map is load-bearing here.

* medium - combat-bait-counter-attack-medium 128x48 cordon 2
Mid-axis wall y=16..23 across x=10..68 closes the head-
on engagement band. Strike stages at (6,12) on the north
flank (above wall); bait jeep at (6,28) on the south
flank (below wall). The west connector x=2..9 is always
clear so the strike route runs east on y=12 above the
wall, descends past x=68 to the yard at (80,20). The
south arc of guards (y=24) sits east of the wall, in the
open band, where the bait's deep-kite vector pulls them
off post.

* hard - combat-bait-counter-attack-hard 128x48 cordon 2
Same wall layout as medium plus the existing spawn-
distance round-robin: group 0 stages far-west (x=6),
group 1 stages forward (x=18); both staging latitudes
(y=12 / y=28) sit in the open NORTH/SOUTH bands.

Smoke check: every level compiles with map_supported=True; all
actors in-bounds (x in [0..127], y in [0..47]); guards at x=76+
and yard at x=80 sit east of the wall extent (x=10..68) so no
actor is placed on a WATER cell.

CLAUDE.md CHANGED
@@ -17,9 +17,17 @@ A scenario is defective if any of the following hold:
17
  1. The win predicate is satisfiable by a play that ignores the
18
  advertised capability (the "laziest play wins" inversion).
19
  2. `within_ticks` or `after_ticks` is set above the tick reachable
20
- within `max_turns` (engine advances ~90 ticks per decision turn โ‡’
21
- `tick โ‰ค 93 + 90ยท(max_turns โˆ’ 1)`); the deadline never bites โ‡’ the
22
- episode times out as a **DRAW**, not a LOSS.
 
 
 
 
 
 
 
 
23
  3. There is no `fail_condition`, or it only triggers on full
24
  force-wipe; a stall / preserve / partial outcome silently draws.
25
  4. The intended capability is not solvable inside the declared budget
@@ -57,9 +65,15 @@ A scenario is defective if any of the following hold:
57
 
58
  ## Engine facts you must internalise
59
 
60
- - **Ticks/turn:** ~90. Max tick at `max_turns` โ‰ˆ `93 + 90ยท(max_turns
61
- โˆ’ 1)`. Any `within_ticks` / `after_ticks` above this is **inert**
62
- (won't bite) โ‡’ draw degeneracy.
 
 
 
 
 
 
63
  - **Engine auto-done:** the engine sets `done=True` when all enemy
64
  actors are eliminated, or sometimes when an agent unit reaches an
65
  enemy-key location. Without a persistent enemy actor a win-by-reach
 
17
  1. The win predicate is satisfiable by a play that ignores the
18
  advertised capability (the "laziest play wins" inversion).
19
  2. `within_ticks` or `after_ticks` is set above the tick reachable
20
+ within `max_turns`; the deadline never bites โ‡’ the episode times
21
+ out as a **DRAW**, not a LOSS. The engine constant is
22
+ `DEFAULT_TICKS_PER_STEP = 30` (`openra-train/src/env.rs:33`), so a
23
+ non-interrupt-mode pack reaches `tick โ‰ˆ 30ยทmax_turns`. Interrupt-
24
+ mode runs (any pack with a non-empty `interrupts:` block) advance
25
+ 1โ€“`max_ticks` ticks per turn (`max_ticks` defaults to 5 in the
26
+ bench call site, `openra_bench/eval_core.py`), so per-turn tick
27
+ advance is variable โ€” read the actual value from
28
+ `info["ticks_advanced"]` instead of assuming it. The historical
29
+ "engine advances ~90 ticks per decision turn" estimate is wrong;
30
+ triaged in `docs/ENGINE_FOLLOWUPS_TRIAGE.md` finding #1.
31
  3. There is no `fail_condition`, or it only triggers on full
32
  force-wipe; a stall / preserve / partial outcome silently draws.
33
  4. The intended capability is not solvable inside the declared budget
 
65
 
66
  ## Engine facts you must internalise
67
 
68
+ - **Ticks/turn:** non-interrupt mode advances exactly
69
+ `DEFAULT_TICKS_PER_STEP = 30` ticks per `env.step()`
70
+ (`openra-train/src/env.rs:33`). Max tick at `max_turns` โ‰ˆ
71
+ `30ยทmax_turns`. Interrupt mode (`step_until_event`, used whenever
72
+ `interrupts:` is non-empty) advances 1โ€“`max_ticks` ticks per turn
73
+ (variable; default `max_ticks = 5`) and breaks on the first signal
74
+ โ€” read `info["ticks_advanced"]` rather than computing
75
+ arithmetically. Any `within_ticks` / `after_ticks` above the
76
+ reachable tick is **inert** (won't bite) โ‡’ draw degeneracy.
77
  - **Engine auto-done:** the engine sets `done=True` when all enemy
78
  actors are eliminated, or sometimes when an agent unit reaches an
79
  enemy-key location. Without a persistent enemy actor a win-by-reach
data/maps/combat-bait-counter-attack-hard.oramap ADDED
Binary file (982 Bytes). View file
 
data/maps/combat-bait-counter-attack-medium.oramap ADDED
Binary file (980 Bytes). View file
 
docs/ENGINE_FOLLOWUPS.md CHANGED
@@ -10,26 +10,33 @@ Filed 2026-05-23 from Wave 12 + Wave 13 agent reports.
10
 
11
  ---
12
 
13
- ## 1. Tick-rate discrepancy (medium severity)
14
 
15
  **Finding** (def-position-revealed-direction agent, Wave 13)
16
 
17
- CLAUDE.md says "engine advances ~90 ticks per decision turn".
18
  Empirically measured under interrupt mode: **~64 ticks/turn**, not 90.
19
 
20
- **Impact**: per-pack `within_ticks` / `after_ticks` deadlines tuned via
21
- the CLAUDE.md formula `tick โ‰ค 93 + 90ยท(max_turns โˆ’ 1)` may either bite
 
 
 
 
 
 
 
 
 
 
22
  LATER than intended (silent draw-degeneracy) or NOT AT ALL inside
23
- `max_turns`. The agent had to tighten `within_ticks: 5400 โ†’ 3600` to
24
- make the deadline actually bite.
25
-
26
- **Action options**
27
- - (a) Re-measure tick-rate under all bench code paths (interrupt vs
28
- non-interrupt; varying `parallel_cells`) and update CLAUDE.md with
29
- the empirical number.
30
- - (b) Audit every pack's `within_ticks` against the empirical rate;
31
- flag packs whose deadline is now unreachable.
32
- - (c) Stabilize the engine's tick-per-turn to a documented constant.
33
 
34
  ---
35
 
@@ -139,25 +146,35 @@ capability from the bench.
139
 
140
  ## 6. Arena generator's default 4-corner mpspawns rotate per-seed (medium severity)
141
 
142
- **Finding** (perception-target-vs-fog agent, Wave 13)
143
-
144
- `openra_bench/mapgen.py::_arena` emits 4 default corner mpspawns.
145
- The engine picks one mpspawn per seed and **OFFSETS pre-placed actor
146
- coords relative to it** โ€” authored `position: [6, 5]` actually appears
147
- at y=29-33 on some seeds.
148
-
149
- **Impact**: per-scenario maps using the `arena` generator with
150
- default spawns get unexpectedly seed-rotated, breaking position
151
- invariants. Several Wave 12 packs may be affected silently.
 
 
 
 
 
 
 
 
152
 
153
  **Workaround**: declare exactly one mpspawn per generator spec:
154
- `spawns: [[6, 5]]`. Documented inline in the affected pack.
 
155
 
156
  **Action options**
157
- - (a) Default `arena` generator to a SINGLE centred mpspawn so
158
- position invariants hold by default.
159
  - (b) Add a runtime warning when a generator emits multiple mpspawns
160
- AND the scenario has `position:` actors that could be offset.
 
161
  - (c) Add `mapgen.py` docstring warning.
162
 
163
  ---
 
10
 
11
  ---
12
 
13
+ ## 1. Tick-rate discrepancy (medium severity) โ€” RESOLVED (doc fix)
14
 
15
  **Finding** (def-position-revealed-direction agent, Wave 13)
16
 
17
+ CLAUDE.md said "engine advances ~90 ticks per decision turn".
18
  Empirically measured under interrupt mode: **~64 ticks/turn**, not 90.
19
 
20
+ **Resolution (2026-05-23, post-merge):** The engine constant is
21
+ `DEFAULT_TICKS_PER_STEP = 30` (`OpenRA-Rust/openra-train/src/env.rs:33`).
22
+ Non-interrupt mode advances exactly 30 ticks per `env.step()`;
23
+ interrupt mode (`step_until_event`) advances 1โ€“`max_ticks` ticks per
24
+ turn, variable per call. `OpenRA-Bench/CLAUDE.md` updated in two
25
+ places โ€” the "~90 ticks/turn" estimate is gone; replaced with the
26
+ actual constant + a note to read `info["ticks_advanced"]` instead of
27
+ calculating arithmetically. See `docs/ENGINE_FOLLOWUPS_TRIAGE.md`
28
+ finding #1.
29
+
30
+ **Impact (resolved)**: per-pack `within_ticks` / `after_ticks` deadlines
31
+ tuned via the bogus `tick โ‰ค 93 + 90ยท(max_turns โˆ’ 1)` formula may bite
32
  LATER than intended (silent draw-degeneracy) or NOT AT ALL inside
33
+ `max_turns`. The bench has manual tightening evidence in
34
+ `combat-attack-from-behind-fog.yaml:105`,
35
+ `econ-protect-harvester-route.yaml:159`,
36
+ `combat-protect-vip-escort.yaml:144`, `tp-survive-n-turns.yaml:151`,
37
+ `rob-deadline-shortened-midway.yaml:107` already manually compensating
38
+ โ€” these are correct (the deadlines were tightened to bite under 30
39
+ ticks/step) and stay.
 
 
 
40
 
41
  ---
42
 
 
146
 
147
  ## 6. Arena generator's default 4-corner mpspawns rotate per-seed (medium severity)
148
 
149
+ **Finding** (perception-target-vs-fog agent, Wave 13). **CORRECTED
150
+ 2026-05-23 โ€” original wording was misleading; see triage doc finding
151
+ #6 for the post-merge re-read.**
152
+
153
+ `openra_bench/mapgen.py::_arena` emits 4 default corner mpspawns. The
154
+ engine does NOT offset pre-placed actor coordinates โ€” `position:
155
+ [6, 5]` lands at exactly (6, 5) every seed (`build_scenario_actor`,
156
+ `openra-train/src/env.rs:2338+`, places actors at literal `sa.position`).
157
+ What DOES vary per seed is the **auto-spawned MCV**:
158
+ `assign_spawn_points` (`world.rs:4684+`) picks one mpspawn cell per
159
+ playable slot, deterministic on `seed`. So if a pack relies on the
160
+ auto-MCV being at a specific corner (rather than its absolute
161
+ position), the corner DOES rotate across seeds.
162
+
163
+ **Impact**: per-scenario maps using the `arena` generator with default
164
+ spawns and depending on a fixed agent-base location see the auto-MCV
165
+ land at a different corner per seed โ€” breaking position invariants
166
+ that assume "the agent's MCV is always at (6,5)".
167
 
168
  **Workaround**: declare exactly one mpspawn per generator spec:
169
+ `spawns: [[6, 5]]`. Documented inline in the affected pack
170
+ (`perception-target-vs-fog.yaml:62-68, 157, 200, 252`).
171
 
172
  **Action options**
173
+ - (a) Default `arena` generator to a SINGLE centred mpspawn so the
174
+ auto-MCV's corner is fixed by default.
175
  - (b) Add a runtime warning when a generator emits multiple mpspawns
176
+ AND the scenario has `position:`-anchored agent actors AND no
177
+ `spawn_point:` axis is declared.
178
  - (c) Add `mapgen.py` docstring warning.
179
 
180
  ---
openra_bench/scenarios/packs/combat-bait-counter-attack.yaml CHANGED
@@ -32,6 +32,23 @@
32
  # not pass through.
33
  #
34
  # Engine notes (per CLAUDE.md):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  # - `fact` is `MustBeDestroyed`: razing it would auto-`done` the
36
  # engine. A SENTINEL enemy `fact` is placed in the far north so
37
  # the episode persists past the objective `fact` destruction
@@ -203,6 +220,30 @@ levels:
203
  and south faces of the enemy yard at (80,20). Destroy the yard,
204
  losing at most two units, within about 62 turns.
205
  overrides:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  actors:
207
  # Strike force: 5 medium tanks staged on the NORTH flank
208
  # (y=12) so that, once the bait pulls the SOUTH arc off post,
@@ -270,6 +311,21 @@ levels:
270
  distance (far west or well forward). Destroy the enemy yard,
271
  losing at most two units, within about 62 turns.
272
  overrides:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  actors:
274
  # spawn_point 0 โ€” FAR-WEST staging (x=6): the strike has the
275
  # full run-in to the yard; the bait has time to pull the
 
32
  # not pass through.
33
  #
34
  # Engine notes (per CLAUDE.md):
35
+ # - Map (medium / hard only): custom 128x48 arena (cordon 2 โ‡’
36
+ # playable y in [2..45]) generated via mapgen's `arena` generator
37
+ # with WATER walls that physically reinforce the north-flank-
38
+ # strike doctrine of the L-cover variant. The strike force stages
39
+ # on the NORTH flank (y=12) and the bait jeep on the SOUTH flank
40
+ # (y=28); a wide head-on engagement band y=16..23 across
41
+ # x=10..68 is walled so a brute-frontal drive on y=20 is
42
+ # geometrically impossible โ€” the strike force MUST detour to the
43
+ # NORTH lane (y=2..15 left of x=10 and y=2..15 above the wall).
44
+ # The SOUTH lane (y=29..45 plus y>=24 east of x=68) carries the
45
+ # bait's deep-kite vector that pulls the south arc of guards off
46
+ # post; the fact at (80,20) sits in the open band east of the
47
+ # wall (x>=69, y=15..27). Easy retains the pack-level
48
+ # rush-hour-arena (no head-on wall โ€” the easy 3-guard west-face
49
+ # idiom works on the open arena because the bait pulls ALL three
50
+ # guards south, and the strike's head-on east drive sees an
51
+ # undefended face by the time it arrives).
52
  # - `fact` is `MustBeDestroyed`: razing it would auto-`done` the
53
  # engine. A SENTINEL enemy `fact` is placed in the far north so
54
  # the episode persists past the objective `fact` destruction
 
220
  and south faces of the enemy yard at (80,20). Destroy the yard,
221
  losing at most two units, within about 62 turns.
222
  overrides:
223
+ # MEDIUM MAP โ€” 128x48 arena (cordon 2 -> playable y in [2..45]).
224
+ # Head-on engagement band y=16..23 walled across x=10..68 so a
225
+ # brute attack_move east on y=20 is geometrically impossible โ€”
226
+ # the strike force MUST swing through the NORTH lane (y=2..15
227
+ # at x>=10 stays clear above the wall; y=12 staging is in the
228
+ # open band). The SOUTH lane (y=29..45 left of x=10 plus the
229
+ # open east column y>=24 east of x=68) carries the bait jeep's
230
+ # deep-kite vector. The yard at (80,20) sits in the open
231
+ # corridor east of the wall (x>=69).
232
+ base_map:
233
+ generator: arena
234
+ name: combat-bait-counter-attack-medium
235
+ width: 128
236
+ height: 48
237
+ cordon: 2
238
+ title: 'Bait + Counter-Attack โ€” Medium (North-Flank Strike)'
239
+ obstacles:
240
+ # Mid-axis wall โ€” closes y=16..23 across x=10..68. North
241
+ # staging (y=12) and south staging (y=28) stay open; the
242
+ # west connector strip x=2..9 is always clear so the
243
+ # north-flank strike route runs north from y=12 to y=2..15
244
+ # (open above wall) east to x=68 then south to the yard
245
+ # at (80,20) once the bait has pulled the south arc.
246
+ - {x: 10, y: 16, w: 59, h: 8}
247
  actors:
248
  # Strike force: 5 medium tanks staged on the NORTH flank
249
  # (y=12) so that, once the bait pulls the SOUTH arc off post,
 
311
  distance (far west or well forward). Destroy the enemy yard,
312
  losing at most two units, within about 62 turns.
313
  overrides:
314
+ # HARD MAP โ€” same head-on wall layout as medium plus the
315
+ # existing spawn-distance round-robin (spawn 0 at far-west
316
+ # x=6, spawn 1 at forward x=18). The wall y=16..23 at
317
+ # x=10..68 still blocks the head-on engagement axis from
318
+ # both spawns; spawn 1's staging cells (18,12) and (18,28)
319
+ # sit in the open NORTH/SOUTH bands above and below the wall.
320
+ base_map:
321
+ generator: arena
322
+ name: combat-bait-counter-attack-hard
323
+ width: 128
324
+ height: 48
325
+ cordon: 2
326
+ title: 'Bait + Counter-Attack โ€” Hard (Spawn-Varied Distance)'
327
+ obstacles:
328
+ - {x: 10, y: 16, w: 59, h: 8}
329
  actors:
330
  # spawn_point 0 โ€” FAR-WEST staging (x=6): the strike has the
331
  # full run-in to the yard; the bait has time to pull the
openra_bench/scenarios/schema.py CHANGED
@@ -212,6 +212,20 @@ class CompiledLevel(BaseModel):
212
  # world-build time. ScenarioDefinition (training) doesn't know
213
  # about this field so it's preserved on the CompiledLevel and
214
  # re-attached at YAML-write time, mirroring `scheduled_events`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  ore_patches: list[dict[str, Any]] = Field(default_factory=list)
216
  # Naval-MVP overlay: explicit `water_cells:` (list of `[x, y]`) and
217
  # `water_rect:` (a single `[x, y, w, h]`) blocks declare WATER
 
212
  # world-build time. ScenarioDefinition (training) doesn't know
213
  # about this field so it's preserved on the CompiledLevel and
214
  # re-attached at YAML-write time, mirroring `scheduled_events`.
215
+ #
216
+ # Engine clamp footgun (`openra-sim/src/resource.rs`,
217
+ # `seed_ore_patch`): per-cell density is
218
+ # `clamp(ceil(amount / passable_cells), 1, 12)`. The cap of 12
219
+ # ore/cell means a small patch with a big `amount` is wasted โ€”
220
+ # `amount: 2000, radius: 2` (~13 cells) yields only ~156 total ore
221
+ # = ~3900 cash ceiling. To get a useful econ patch, choose
222
+ # `amount` so that `amount >= passable_cells * 12` (i.e. fill the
223
+ # disc to the cap), then GROW the disc via `radius` rather than
224
+ # the per-cell `amount`. Recommended ratio: `amount =~ 12 * pi *
225
+ # radius^2 * 1.2` (the 1.2 covers cells on the disc that turn out
226
+ # to be impassable). At 50 cr/ore the cash ceiling of a patch is
227
+ # `12 * passable_cells * 50 = 600 * passable_cells`. Triaged in
228
+ # ENGINE_FOLLOWUPS_TRIAGE.md finding #2.
229
  ore_patches: list[dict[str, Any]] = Field(default_factory=list)
230
  # Naval-MVP overlay: explicit `water_cells:` (list of `[x, y]`) and
231
  # `water_rect:` (a single `[x, y, w, h]`) blocks declare WATER