Spaces:
Running
combat-bait-counter-attack: medium/hard maps reinforce north-flank strike
Browse filesMedium 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.
|
@@ -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`
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:**
|
| 61 |
-
|
| 62 |
-
(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
Binary file (982 Bytes). View file
|
|
|
|
Binary file (980 Bytes). View file
|
|
|
|
@@ -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
|
| 18 |
Empirically measured under interrupt mode: **~64 ticks/turn**, not 90.
|
| 19 |
|
| 20 |
-
**
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
LATER than intended (silent draw-degeneracy) or NOT AT ALL inside
|
| 23 |
-
`max_turns`. The
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
-
|
| 28 |
-
|
| 29 |
-
|
| 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 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 159 |
- (b) Add a runtime warning when a generator emits multiple mpspawns
|
| 160 |
-
AND the scenario has `position:` actors
|
|
|
|
| 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 |
---
|
|
@@ -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
|
|
@@ -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
|