yxc20098 commited on
Commit
0f11358
·
1 Parent(s): 4f4b205

feat(scenario): mcv-deploy-third-base — tri-region operational footprint via 3rd MCV (SC2LE 3-base anchor)

Browse files

Group A seed (Mid-Late: 3rd MCV → 3rd base). Three MCVs must EACH
deploy at a DISTINCT target region (NE/SE/S-CENTER) for the
tri-region operational footprint — bunching at one region or
abandoning an MCV leaves a region uncovered and LOSES. Uses the
Wave-2 MCV deploy fix.

Pack design:
- Map: arena 160x80 cordon 4 (wider arena for 3 well-separated
regions plus a west starter base).
- starting_cash 0 (the decision is move+deploy, not build).
- One pre-placed agent fact (west, (15,40)) + three MCVs at varied
agent positions + a persistent unarmed enemy fact marker
((150,40) easy/medium, (150,75) hard) for LOSS-not-DRAW.
- within_ticks 6300 + pre-fail at tick 4000 with <2 facts so
stalling LOSES well before the deadline.
- max_turns 75 across all tiers (reachable tick 6753 > 6300 ✓).
- Easy: MCVs sit INSIDE their target regions; decision is solely
'deploy all three'.
- Medium: MCVs cluster at west; must move to each region; light
patrol per region.
- Hard: TWO spawn_point groups rotate the whole MCV cluster
between WEST and EAST starts (per-MCV nearest-region flips per
seed); doubled patrols; objective_coords: relative.

Win = building_count_gte:{type:fact,n:3} AND building_in_region
(one per region: NE (130,15), SE (130,65), S-CENTER (75,70), all
radius 8) AND within_ticks:6300. Fail = after_ticks:6301 OR (after
tick 4000 with fewer than 2 facts).

Benchmark anchors: SC2LE 3-base macro, MicroRTS multi-front
expansion, distribution-network multi-warehouse planning, tri-region
operational footprint.

Scripted-policy validation (tests/test_mcv_deploy_third_base.py):
- stall → LOSS on all 3 levels × all 4 seeds.
- bunch-in-place → LOSS on medium+hard × all 4 seeds (facts land
outside every target region).
- deploy-only-two-regions → LOSS on all 3 levels × all 4 seeds.
- intended-3-regions → WIN on all 3 levels × all 4 seeds.
Plus tick/turn-alignment, spawn-variation, anchor, and arena
compile guard tests. 48 new tests pass; tests/test_hard_tier.py
appends 'mcv-deploy-third-base' to UPGRADED.

openra_bench/scenarios/packs/mcv-deploy-third-base.yaml ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MCV-DEPLOY-THIRD-BASE — Group A seed (Mid-Late: 3rd MCV → 3rd base).
2
+ # Tri-region operational footprint: the agent must commit ALL THREE
3
+ # MCVs to DISTINCT target regions (NE / SE / S-CENTER), not bunch them
4
+ # at one spot and not abandon any. Real-world anchor: a distribution
5
+ # network with three production/supply/distribution sites placed across
6
+ # distinct geographies for maximum coverage.
7
+ #
8
+ # Why this is harder than mcv-deploy-second-base:
9
+ # - 3 distinct deploy decisions (1 MCV → 1 region, x3) instead of 2,
10
+ # - 3 separate `building_in_region` checks (one per region) so the
11
+ # model cannot collapse coverage onto one or two regions,
12
+ # - longer horizon (within_ticks 6300 → max_turns ~75) so movement
13
+ # across a wider 160x80 arena is a real factor,
14
+ # - a light enemy patrol near each target on medium/hard creates
15
+ # incidental MCV risk en route (lose any MCV → cannot deploy it →
16
+ # missing region → LOSS).
17
+ #
18
+ # Wave-2 MCV deploy fix (commits bdedc55 OpenRA-Rust + b0cb710
19
+ # OpenRA-Bench, validated by tests/test_mcv_deploy.py) is the
20
+ # pre-requisite: `Command.deploy([mcv_id])` removes the MCV and
21
+ # creates an agent-owned `fact` at offset (mcv_x-1, mcv_y-1). The
22
+ # whole pack rests on that contract.
23
+ #
24
+ # Engine tick/turn budget (CLAUDE.md): ~90 ticks/turn, tick ≤
25
+ # 93 + 90·(max_turns − 1). For within_ticks 6300 we need max_turns ≥
26
+ # 70 (reachable tick at 70 turns = 93 + 90·69 = 6303). We use 75 for
27
+ # slack on hard's combat detours.
28
+ #
29
+ # Engine footguns applied:
30
+ # - `spawn_mcvs: false` — the engine default is true (auto-seeds an
31
+ # extra MCV at the map's mpspawn cells), polluting the unit count
32
+ # and the deploy decision. The pack declares every actor itself.
33
+ # - Persistent unarmed enemy `fact` MARKER at the east edge
34
+ # prevents engine auto-`done` via all-enemies-eliminated, so a
35
+ # non-winner emits a real LOSS on the deadline (never a DRAW).
36
+ # - The west existing agent `fact` counts toward `building_count_gte:
37
+ # {type: fact, n: 3}` (so the agent needs +2 deploys at minimum
38
+ # to satisfy that bar) — but the THREE region predicates EACH
39
+ # require a fact in their target region, and the west fact is in
40
+ # NONE of them, so the agent MUST deploy all three MCVs at the
41
+ # three target regions to win. (The `n: 3` bar plus the three
42
+ # region predicates together are the no-cheat seal: any laziest
43
+ # play that ignores 1+ MCVs fails the corresponding region.)
44
+ # - Hard tier: ≥2 agent spawn_point groups so the whole MCV CLUSTER
45
+ # rotates between WEST (group 0) and EAST (group 1) starts — the
46
+ # closest region for each MCV flips per seed, so a memorised
47
+ # assignment cannot generalise.
48
+ #
49
+ # Region geometry (all radius 8, well inside the playable 160×80
50
+ # arena cordon: x∈[4,156], y∈[4,76]):
51
+ # - NE : center (130, 15)
52
+ # - SE : center (130, 65)
53
+ # - S-CENTER : center ( 75, 70)
54
+ # Easy MCV positions land each MCV INSIDE a distinct region (the
55
+ # resulting fact cell at (mcv_x-1, mcv_y-1) is in the region). Medium
56
+ # and hard require movement: MCVs start clustered or in inappropriate
57
+ # pairings so the agent must move at least one MCV across a long
58
+ # stretch to populate every region.
59
+
60
+ meta:
61
+ id: mcv-deploy-third-base
62
+ title: 'MCV Deploy Third Base — Tri-Region Operational Footprint'
63
+ capability: reasoning
64
+ real_world_meaning: >
65
+ Tri-region facility footprint — each site at the RIGHT region for
66
+ coverage. The operator commits THREE indivisible mobile assets to
67
+ three DISTINCT geographies so the resulting network covers all
68
+ three regions. Bunching two assets in one region wastes a deploy
69
+ and leaves one region uncovered; abandoning an asset entirely
70
+ leaves another uncovered. The decision is committing each asset
71
+ to its designated region (which the start positions hint at on
72
+ easy and which the model must reason about on medium/hard).
73
+ robotics_analogue: >
74
+ Autonomous-fleet multi-FOB deployment for redundant coverage —
75
+ three mobile platforms each transition from mobile to fixed
76
+ (deploy is irreversible: the platform anchors and converts into a
77
+ forward operating base), one per designated coverage region, so
78
+ the resulting tri-FOB network maximises overall coverage and
79
+ redundancy across the operational theatre.
80
+ benchmark_anchor:
81
+ - "SC2LE 3-base macro"
82
+ - "MicroRTS multi-front expansion"
83
+ - "distribution-network multi-warehouse planning"
84
+ - "tri-region operational footprint"
85
+ author: "openra-bench"
86
+
87
+ # Wider 160×80 arena (cordon 4) — fits THREE well-separated regions
88
+ # (NE, SE, S-CENTER) plus a west base, the spread the tri-region
89
+ # footprint requires. The pack-level base_map is the rush-hour-arena
90
+ # sentinel (a known-supported string the loader can resolve before the
91
+ # per-level `overrides.base_map` generator spec replaces it). Per-level
92
+ # overrides materialise the actual arena .oramap.
93
+ base_map: rush-hour-arena
94
+
95
+ # starting_cash: 0 — the agent never needs to BUILD anything. Every
96
+ # objective is satisfied purely by deploying MCVs. The 0 cash is the
97
+ # explicit signal that build/place_building tools are not the path.
98
+ starting_cash: 0
99
+
100
+ base:
101
+ agent: {faction: allies}
102
+ enemy: {faction: soviet, cash: 0}
103
+ # Tools include build/place_building so the tool list isn't lying
104
+ # about what the engine accepts, but cash:0 makes them inert — the
105
+ # path is move_units + deploy only.
106
+ tools: [observe, deploy, move_units, build, place_building, stop]
107
+ spawn_mcvs: false
108
+ planning: true
109
+ termination: {max_ticks: 8500}
110
+ actors: []
111
+
112
+ levels:
113
+ # ── EASY ─────────────────────────────────────────────────────────
114
+ # Bare skill: each MCV starts INSIDE its designated region (the
115
+ # fact cell after deploy lands at (mcv_x-1, mcv_y-1), still inside
116
+ # radius 8 of the region center). The decision is purely "deploy
117
+ # ALL THREE, not just one or two" — no movement, no enemies.
118
+ # All target regions are inside agent vision (no fog scout needed).
119
+ # max_turns 75 → reachable tick 93 + 90·74 = 6753 > 6300 ✓
120
+ # (the deadline bites; a stall emits LOSS, not DRAW).
121
+ easy:
122
+ description: >
123
+ You have THREE Construction Vehicles (MCV), each pre-positioned
124
+ AT a distinct target region (NE, SE, and S-CENTER), plus one
125
+ existing Construction Yard (fact) at your starter base in the
126
+ west. Deploy EACH MCV in place — each deploy consumes the MCV
127
+ and creates a new fact one cell up-and-left of the MCV's
128
+ position, inside its designated region. Win = you own ≥3 facts
129
+ total AND there is a fact near (130,15) AND a fact near
130
+ (130,65) AND a fact near (75,70), before tick 6300. Deploying
131
+ only 1 or 2 MCVs leaves an uncovered region and LOSES. Losing
132
+ an MCV before deploying it also LOSES (its region stays
133
+ uncovered).
134
+ overrides:
135
+ base_map:
136
+ generator: arena
137
+ name: mcv-deploy-third-base-arena
138
+ width: 160
139
+ height: 80
140
+ cordon: 4
141
+ actors:
142
+ # Existing west agent fact (the starter base). Does NOT count
143
+ # toward any of the three target-region predicates (none of
144
+ # them are in the west), but DOES count toward the
145
+ # `building_count_gte: {type: fact, n: 3}` total bar.
146
+ - {type: fact, owner: agent, position: [15, 40]}
147
+ # Three MCVs already inside their designated regions. After
148
+ # deploy, fact lands at (mcv_x-1, mcv_y-1):
149
+ # (131,16) → fact (130,15) — exactly NE region center
150
+ # (131,66) → fact (130,65) — exactly SE region center
151
+ # ( 76,71) → fact ( 75,70) — exactly S-CENTER center
152
+ - {type: mcv, owner: agent, position: [131, 16]}
153
+ - {type: mcv, owner: agent, position: [131, 66]}
154
+ - {type: mcv, owner: agent, position: [76, 71]}
155
+ # Persistent unarmed enemy `fact` MARKER at the far east edge,
156
+ # outside all three target regions. Keeps the episode alive
157
+ # past any partial outcome (the all-enemies-eliminated
158
+ # auto-`done` is gated on this surviving) so a non-winner
159
+ # emits a real reachable LOSS on the deadline, never a DRAW.
160
+ - {type: fact, owner: enemy, position: [150, 40]}
161
+ win_condition:
162
+ all_of:
163
+ - building_count_gte: {type: fact, n: 3}
164
+ - building_in_region: {x: 130, y: 15, radius: 8, type: fact, count: 1}
165
+ - building_in_region: {x: 130, y: 65, radius: 8, type: fact, count: 1}
166
+ - building_in_region: {x: 75, y: 70, radius: 8, type: fact, count: 1}
167
+ - within_ticks: 6300
168
+ fail_condition:
169
+ any_of:
170
+ - after_ticks: 6301
171
+ # Pre-fail clause: if the agent has not produced ≥2 facts by
172
+ # tick 4000 the run is unwinnable (no time to move+deploy
173
+ # the remaining MCV). Catches a stall LONG before the main
174
+ # deadline, so playback shows the genuine LOSS reason.
175
+ - all_of:
176
+ - after_ticks: 4000
177
+ - not: {building_count_gte: {type: fact, n: 2}}
178
+ max_turns: 75
179
+
180
+ # ── MEDIUM ───────────────────────────────────────────────────────
181
+ # +1 axis: the MCVs no longer start inside their target regions —
182
+ # they're clustered around the west starter base. The agent must
183
+ # MOVE each MCV to a distinct region (the assignment is the
184
+ # decision) and then deploy. A LIGHT enemy patrol sits near each
185
+ # target so a slow / wandering MCV may be lost en route (no
186
+ # patrol is on the optimal direct line — they're offset).
187
+ # Same tick budget; target regions are in fog (the model must
188
+ # ground them from the coordinate disclosure).
189
+ # max_turns 75 → reachable tick 6753 > 6300 ✓
190
+ medium:
191
+ description: >
192
+ Same THREE Construction Vehicles, but this time clustered at
193
+ your starter base in the west — you must MOVE each one to its
194
+ target region BEFORE deploying. Three target regions (NE near
195
+ (130,15), SE near (130,65), S-CENTER near (75,70)) each need
196
+ ONE fact. A light Soviet patrol sits near each target — keep
197
+ to a direct path and the patrol will only graze you. Win = you
198
+ own ≥3 facts total AND there is a fact in each of the three
199
+ target regions, before tick 6300. Losing any MCV before its
200
+ deploy leaves a region uncovered and LOSES; bunching two MCVs
201
+ in one region also LOSES (a region is left uncovered).
202
+ overrides:
203
+ base_map:
204
+ generator: arena
205
+ name: mcv-deploy-third-base-arena
206
+ width: 160
207
+ height: 80
208
+ cordon: 4
209
+ enemy: {faction: soviet, cash: 0, bot_type: patrol}
210
+ actors:
211
+ # Same west starter fact as easy.
212
+ - {type: fact, owner: agent, position: [15, 40]}
213
+ # Three MCVs CLUSTERED at the starter base (varied positions
214
+ # so they're distinct cells, but all within a few cells of
215
+ # the west fact). The agent must reason about which MCV
216
+ # should head to which region.
217
+ - {type: mcv, owner: agent, position: [20, 35]}
218
+ - {type: mcv, owner: agent, position: [20, 40]}
219
+ - {type: mcv, owner: agent, position: [20, 45]}
220
+ # Light Soviet patrols near each target (stance 2 = return
221
+ # fire only — the patrol bot oscillates around spawn cell;
222
+ # placed OFFSET from each region's center so the direct
223
+ # approach is unthreatened, a meandering MCV gets grazed).
224
+ - {type: e1, owner: enemy, position: [115, 12], stance: 2}
225
+ - {type: e1, owner: enemy, position: [115, 68], stance: 2}
226
+ - {type: e1, owner: enemy, position: [60, 68], stance: 2}
227
+ # Persistent far enemy marker (LOSS-not-DRAW guarantee).
228
+ - {type: fact, owner: enemy, position: [150, 40]}
229
+ win_condition:
230
+ all_of:
231
+ - building_count_gte: {type: fact, n: 3}
232
+ - building_in_region: {x: 130, y: 15, radius: 8, type: fact, count: 1}
233
+ - building_in_region: {x: 130, y: 65, radius: 8, type: fact, count: 1}
234
+ - building_in_region: {x: 75, y: 70, radius: 8, type: fact, count: 1}
235
+ - within_ticks: 6300
236
+ fail_condition:
237
+ any_of:
238
+ - after_ticks: 6301
239
+ - all_of:
240
+ - after_ticks: 4500 # half-way through, must have ≥2 facts
241
+ - not: {building_count_gte: {type: fact, n: 2}}
242
+ max_turns: 75
243
+
244
+ # ── HARD ─────────────────────────────────────────────────────────
245
+ # +1 axis: TWO agent spawn_point groups rotate the whole MCV
246
+ # CLUSTER between WEST and EAST starts. The same three target
247
+ # regions stay fixed (enemy actors don't honour spawn_point per
248
+ # CLAUDE.md), but which corner an MCV starts in determines which
249
+ # target is CLOSEST — a memorised "MCV-i goes to region-j"
250
+ # assignment cannot generalise across seeds.
251
+ # max_turns 75 → reachable tick 6753 > 6300 ✓
252
+ hard:
253
+ description: >
254
+ Same tri-region objective (NE, SE, S-CENTER each need a fact)
255
+ under tighter pressure. Your three Construction Vehicles cluster
256
+ at either the WEST or the EAST edge of the map depending on the
257
+ run — read your MCVs' positions and assign each to the region
258
+ it can reach fastest. A doubled Soviet patrol sits near each
259
+ target. Win = you own ≥3 facts total AND there is a fact in
260
+ each of the three target regions, before tick 6300. Losing any
261
+ MCV before its deploy LOSES (the region stays uncovered);
262
+ bunching two MCVs in one region LOSES (a region uncovered).
263
+ objective_coords: relative
264
+ overrides:
265
+ base_map:
266
+ generator: arena
267
+ name: mcv-deploy-third-base-arena
268
+ width: 160
269
+ height: 80
270
+ cordon: 4
271
+ enemy: {faction: soviet, cash: 0, bot_type: patrol}
272
+ actors:
273
+ # West starter fact (duplicated across both spawn groups so
274
+ # whichever group is selected, the starter base is present —
275
+ # see CLAUDE.md note on the spawn_point filter dropping every
276
+ # un-tagged agent actor when ANY agent actor is tagged).
277
+ - {type: fact, owner: agent, position: [15, 40], spawn_point: 0}
278
+ - {type: fact, owner: agent, position: [15, 40], spawn_point: 1}
279
+ # WEST MCV cluster (group 0) — closest region is S-CENTER
280
+ # (75,70) at ~70 cells, NE (130,15) at ~115, SE (130,65) at
281
+ # ~115. The agent must commit one MCV to the far NE and one
282
+ # to the far SE despite the western starter — the natural
283
+ # west-side "do nothing" laziest play leaves both east
284
+ # regions uncovered.
285
+ - {type: mcv, owner: agent, position: [20, 35], spawn_point: 0}
286
+ - {type: mcv, owner: agent, position: [20, 40], spawn_point: 0}
287
+ - {type: mcv, owner: agent, position: [20, 45], spawn_point: 0}
288
+ # EAST MCV cluster (group 1) — closest regions are NE and SE;
289
+ # S-CENTER (75,70) is the far target. Inversion of group 0:
290
+ # an agent that memorised "always head south-east first"
291
+ # on group 0 will leave S-CENTER uncovered on group 1.
292
+ - {type: mcv, owner: agent, position: [145, 35], spawn_point: 1}
293
+ - {type: mcv, owner: agent, position: [145, 40], spawn_point: 1}
294
+ - {type: mcv, owner: agent, position: [145, 45], spawn_point: 1}
295
+ # Doubled patrols (one extra unit per region vs medium) —
296
+ # attrition risk is real, but each patrol's spawn cell is
297
+ # offset from the region center so the optimal direct
298
+ # approach is still unthreatened.
299
+ - {type: e1, owner: enemy, position: [115, 12], stance: 2}
300
+ - {type: e1, owner: enemy, position: [117, 14], stance: 2}
301
+ - {type: e1, owner: enemy, position: [115, 68], stance: 2}
302
+ - {type: e1, owner: enemy, position: [117, 66], stance: 2}
303
+ - {type: e1, owner: enemy, position: [60, 68], stance: 2}
304
+ - {type: e1, owner: enemy, position: [62, 66], stance: 2}
305
+ # Persistent far enemy marker (LOSS-not-DRAW guarantee). NOT
306
+ # tagged with spawn_point — enemy actors place regardless
307
+ # (CLAUDE.md: spawn_point filter applies only to AGENT
308
+ # actors).
309
+ - {type: fact, owner: enemy, position: [150, 75]}
310
+ win_condition:
311
+ all_of:
312
+ - building_count_gte: {type: fact, n: 3}
313
+ - building_in_region: {x: 130, y: 15, radius: 8, type: fact, count: 1}
314
+ - building_in_region: {x: 130, y: 65, radius: 8, type: fact, count: 1}
315
+ - building_in_region: {x: 75, y: 70, radius: 8, type: fact, count: 1}
316
+ - within_ticks: 6300
317
+ fail_condition:
318
+ any_of:
319
+ - after_ticks: 6301
320
+ - all_of:
321
+ - after_ticks: 4500
322
+ - not: {building_count_gte: {type: fact, n: 2}}
323
+ max_turns: 75
tests/test_hard_tier.py CHANGED
@@ -62,14 +62,15 @@ UPGRADED = [
62
  # column against three raider vectors (north/central/south).
63
  "mid-economy-under-fire",
64
  # Other mcv-deploy-* siblings (defensible-site, near-resource,
65
- # second-base, third-base, relocate-under-pressure) register
66
- # themselves via their own Wave-3 commits as they land.
67
  # Group A seed: hard tier stages the spare MCV NE-leaning vs
68
  # SE-leaning by seed; the win predicate accepts a deploy at
69
  # either candidate region (NE or SE) so the model must pick the
70
  # NEAREST one — cross-map diagonal blows the tick budget.
71
  "mcv-deploy-second-base",
72
  # Group A seed (this commit): hard tier seed-varies the FRESH
 
73
  # MCV's start column (40,20 vs 60,20) with the matching safe
74
  # shoulders, so a single memorised relocation cell cannot
75
  # generalise across seeds.
@@ -78,6 +79,11 @@ UPGRADED = [
78
  # harvest income); hard tier defines 2 symmetric spawn_point groups
79
  # (north/south base) so each seed gets its own near-patch geometry.
80
  "mcv-deploy-near-resource",
 
 
 
 
 
81
  ]
82
 
83
  # Consciously NOT spawn-varied, with the reason (keeps the curation
 
62
  # column against three raider vectors (north/central/south).
63
  "mid-economy-under-fire",
64
  # Other mcv-deploy-* siblings (defensible-site, near-resource,
65
+ # relocate-under-pressure) register themselves via their own
66
+ # Wave-3 commits as they land.
67
  # Group A seed: hard tier stages the spare MCV NE-leaning vs
68
  # SE-leaning by seed; the win predicate accepts a deploy at
69
  # either candidate region (NE or SE) so the model must pick the
70
  # NEAREST one — cross-map diagonal blows the tick budget.
71
  "mcv-deploy-second-base",
72
  # Group A seed (this commit): hard tier seed-varies the FRESH
73
+ # Group A seed (sibling commit): hard tier seed-varies the FRESH
74
  # MCV's start column (40,20 vs 60,20) with the matching safe
75
  # shoulders, so a single memorised relocation cell cannot
76
  # generalise across seeds.
 
79
  # harvest income); hard tier defines 2 symmetric spawn_point groups
80
  # (north/south base) so each seed gets its own near-patch geometry.
81
  "mcv-deploy-near-resource",
82
+ # Wave-2 MCV deploy + tri-region operational footprint: 3 MCVs
83
+ # must each deploy at a DISTINCT target region (NE/SE/S-CENTER);
84
+ # hard tier rotates the WHOLE MCV cluster between WEST and EAST
85
+ # starts so the per-MCV nearest-region assignment flips per seed.
86
+ "mcv-deploy-third-base",
87
  ]
88
 
89
  # Consciously NOT spawn-varied, with the reason (keeps the curation
tests/test_mcv_deploy_third_base.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """mcv-deploy-third-base — Group A seed (Mid-Late: 3rd MCV → 3rd base).
2
+
3
+ Tri-region operational footprint scripted-policy validation. The pack
4
+ declares THREE MCVs and a single existing west `fact`; the win
5
+ predicate requires ≥3 facts AND a fact INSIDE each of three distinct
6
+ target regions (NE / SE / S-CENTER) before tick 6300. The discriminator
7
+ is committing each MCV to a DISTINCT region — bunching all three at
8
+ the base satisfies `building_count_gte` but fails all three region
9
+ predicates; deploying only 1-2 leaves at least one region uncovered;
10
+ stalling produces no new facts at all.
11
+
12
+ This file proves the no-defect / no-cheat bar at scripted level (no
13
+ model, no network) across seeds 1–4 and across easy / medium / hard:
14
+
15
+ - stall (only Command.observe()) → LOSS
16
+ - bunch-all-at-base (deploy in place; on medium/hard MCVs
17
+ start away from the target regions, so the resulting facts
18
+ land in the west cluster, NOT in any target region) → LOSS
19
+ - deploy-only-two-regions (cover NE+SE, abandon S-CENTER) → LOSS
20
+ - intended-3-regions (move each MCV to a distinct region
21
+ and deploy) → WIN
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import pytest
27
+
28
+ pytest.importorskip("openra_train", reason="Rust env wheel not installed")
29
+
30
+ from openra_bench.eval_core import run_level
31
+ from openra_bench.scenarios import load_pack
32
+ from openra_bench.scenarios.loader import PACKS_DIR, compile_level
33
+
34
+ PACK = PACKS_DIR / "mcv-deploy-third-base.yaml"
35
+
36
+ # Three target region centers (see pack YAML). Deploy converts an MCV
37
+ # at (mx, my) into a fact at (mx-1, my-1), so to land a fact exactly
38
+ # at (cx, cy) the MCV must be at (cx+1, cy+1).
39
+ REGIONS = [(130, 15), (130, 65), (75, 70)]
40
+ MCV_TARGETS = [(cx + 1, cy + 1) for (cx, cy) in REGIONS]
41
+
42
+ LEVELS = ("easy", "medium", "hard")
43
+ HARD_SEEDS = (1, 2, 3, 4)
44
+
45
+
46
+ # ── scripted policies ─────────────────────────────────────────────────
47
+
48
+
49
+ def stall(rs, Command):
50
+ """Do nothing — the deadline (and the early pre-fail clause at
51
+ tick 4000 with <2 facts) emits a real LOSS."""
52
+ return [Command.observe()]
53
+
54
+
55
+ def bunch_in_place(rs, Command):
56
+ """Deploy every MCV at its current cell. On EASY the MCVs already
57
+ sit inside their target regions (so this also wins easy — easy's
58
+ decision is solely "deploy all three, not zero"). On MEDIUM and
59
+ HARD the MCVs cluster at the west (or east) starter base and the
60
+ resulting facts land OUTSIDE every target region, so the pack's
61
+ three `building_in_region` predicates all fail → LOSS."""
62
+ cmds = []
63
+ for u in rs.get("units_summary") or []:
64
+ if str(u.get("type", "")).lower() == "mcv":
65
+ cmds.append(Command.deploy([str(u["id"])]))
66
+ return cmds or [Command.observe()]
67
+
68
+
69
+ def make_deploy_only_two():
70
+ """Send MCVs to the first TWO regions, never the third. The
71
+ third region's `building_in_region` predicate is never satisfied
72
+ → LOSS on the deadline. Works on all tiers."""
73
+ state = {"assigned": {}, "deployed": set()}
74
+ # Only two of the three targets are used.
75
+ targets_pool = [MCV_TARGETS[0], MCV_TARGETS[1]]
76
+
77
+ def fn(rs, Command):
78
+ units = rs.get("units_summary") or []
79
+ mcvs = sorted(
80
+ [u for u in units if str(u.get("type", "")).lower() == "mcv"],
81
+ key=lambda u: u["id"],
82
+ )
83
+ # Stable id→target assignment, nearest-unused-target.
84
+ for u in mcvs:
85
+ if u["id"] not in state["assigned"] and len(state["assigned"]) < len(
86
+ targets_pool
87
+ ):
88
+ used = set(state["assigned"].values())
89
+ cands = [t for t in targets_pool if t not in used]
90
+ if not cands:
91
+ continue
92
+ t = min(
93
+ cands,
94
+ key=lambda t: (t[0] - u["cell_x"]) ** 2
95
+ + (t[1] - u["cell_y"]) ** 2,
96
+ )
97
+ state["assigned"][u["id"]] = t
98
+ cmds = []
99
+ for u in mcvs:
100
+ tgt = state["assigned"].get(u["id"])
101
+ if tgt is None:
102
+ continue
103
+ tx, ty = tgt
104
+ if abs(u["cell_x"] - tx) <= 1 and abs(u["cell_y"] - ty) <= 1:
105
+ if u["id"] not in state["deployed"]:
106
+ cmds.append(Command.deploy([str(u["id"])]))
107
+ state["deployed"].add(u["id"])
108
+ else:
109
+ cmds.append(Command.move_units([str(u["id"])], tx, ty))
110
+ return cmds or [Command.observe()]
111
+
112
+ return fn
113
+
114
+
115
+ def make_intended_three_regions():
116
+ """The intended policy: assign each MCV to a DISTINCT target
117
+ region (nearest-unused), move it there, deploy. After all three
118
+ MCVs deploy, the agent owns ≥3 facts (existing west + 3 new)
119
+ AND each target region holds one fact → WIN."""
120
+ state = {"assigned": {}, "deployed": set()}
121
+
122
+ def fn(rs, Command):
123
+ units = rs.get("units_summary") or []
124
+ mcvs = sorted(
125
+ [u for u in units if str(u.get("type", "")).lower() == "mcv"],
126
+ key=lambda u: u["id"],
127
+ )
128
+ for u in mcvs:
129
+ if u["id"] in state["assigned"]:
130
+ continue
131
+ used = set(state["assigned"].values())
132
+ cands = [t for t in MCV_TARGETS if t not in used]
133
+ if not cands:
134
+ continue
135
+ t = min(
136
+ cands,
137
+ key=lambda t: (t[0] - u["cell_x"]) ** 2
138
+ + (t[1] - u["cell_y"]) ** 2,
139
+ )
140
+ state["assigned"][u["id"]] = t
141
+ cmds = []
142
+ for u in mcvs:
143
+ tgt = state["assigned"].get(u["id"])
144
+ if tgt is None:
145
+ continue
146
+ tx, ty = tgt
147
+ if abs(u["cell_x"] - tx) <= 1 and abs(u["cell_y"] - ty) <= 1:
148
+ if u["id"] not in state["deployed"]:
149
+ cmds.append(Command.deploy([str(u["id"])]))
150
+ state["deployed"].add(u["id"])
151
+ else:
152
+ cmds.append(Command.move_units([str(u["id"])], tx, ty))
153
+ return cmds or [Command.observe()]
154
+
155
+ return fn
156
+
157
+
158
+ # ── tests ─────────────────────────────────────────────────────────────
159
+
160
+
161
+ @pytest.mark.parametrize("level", LEVELS)
162
+ @pytest.mark.parametrize("seed", HARD_SEEDS)
163
+ def test_intended_three_regions_wins_every_level_every_seed(level, seed):
164
+ """The intended commit-each-MCV-to-its-own-region policy must WIN
165
+ on every level and every hard seed (1–4) — the no-defect bar."""
166
+ pack = load_pack(PACK)
167
+ c = compile_level(pack, level)
168
+ res = run_level(c, make_intended_three_regions(), seed=seed)
169
+ assert res.outcome == "win", (
170
+ f"{level} seed={seed}: intended-3-regions must WIN; got "
171
+ f"{res.outcome} (tick={res.signals.game_tick} "
172
+ f"buildings={res.signals.own_buildings})"
173
+ )
174
+
175
+
176
+ @pytest.mark.parametrize("level", LEVELS)
177
+ @pytest.mark.parametrize("seed", HARD_SEEDS)
178
+ def test_stall_loses_every_level_every_seed(level, seed):
179
+ """The do-nothing policy: no MCV ever deploys → no new facts →
180
+ the early pre-fail (`after_ticks: 4000` AND <2 facts) bites well
181
+ before the deadline. Real reachable LOSS, never a DRAW."""
182
+ pack = load_pack(PACK)
183
+ c = compile_level(pack, level)
184
+ res = run_level(c, stall, seed=seed)
185
+ assert res.outcome == "loss", (
186
+ f"{level} seed={seed}: stall must LOSE; got {res.outcome} "
187
+ f"(tick={res.signals.game_tick})"
188
+ )
189
+
190
+
191
+ @pytest.mark.parametrize("level", ("medium", "hard"))
192
+ @pytest.mark.parametrize("seed", HARD_SEEDS)
193
+ def test_bunch_at_base_loses_on_medium_and_hard(level, seed):
194
+ """On medium and hard the MCVs cluster at the west (or east, on
195
+ hard's seed-rotated group) starter base — deploying in place
196
+ lands every resulting fact OUTSIDE all three target regions, so
197
+ every `building_in_region` predicate fails and the run LOSES on
198
+ the deadline. (Easy MCVs already sit in their regions by design,
199
+ so bunch-in-place wins easy — the easy decision is solely "do
200
+ you deploy at all"; only medium/hard test the distribution
201
+ capability.)"""
202
+ pack = load_pack(PACK)
203
+ c = compile_level(pack, level)
204
+ res = run_level(c, bunch_in_place, seed=seed)
205
+ assert res.outcome == "loss", (
206
+ f"{level} seed={seed}: bunch-in-place must LOSE; got "
207
+ f"{res.outcome} (buildings={res.signals.own_buildings})"
208
+ )
209
+
210
+
211
+ @pytest.mark.parametrize("level", LEVELS)
212
+ @pytest.mark.parametrize("seed", HARD_SEEDS)
213
+ def test_deploy_only_two_regions_loses(level, seed):
214
+ """Partial coverage policy: only the FIRST TWO target regions get
215
+ a fact; the THIRD is abandoned. The third region's
216
+ `building_in_region` predicate is never satisfied → LOSS, on
217
+ every tier and every seed."""
218
+ pack = load_pack(PACK)
219
+ c = compile_level(pack, level)
220
+ res = run_level(c, make_deploy_only_two(), seed=seed)
221
+ assert res.outcome == "loss", (
222
+ f"{level} seed={seed}: deploy-only-two-regions must LOSE; "
223
+ f"got {res.outcome} (buildings={res.signals.own_buildings})"
224
+ )
225
+
226
+
227
+ def test_within_ticks_is_reachable_per_tier():
228
+ """Tick/turn alignment: the within_ticks deadline must be inside
229
+ max_turns (tick ≤ 93 + 90·(max_turns − 1)) — otherwise a staller
230
+ DRAWS instead of LOSING. Re-derived per tier."""
231
+ pack = load_pack(PACK)
232
+ for lvl in LEVELS:
233
+ c = compile_level(pack, lvl)
234
+ max_tick = 93 + 90 * (c.max_turns - 1)
235
+ assert 6300 <= max_tick, (
236
+ f"{lvl}: within_ticks=6300 > reachable max_tick="
237
+ f"{max_tick} (would draw instead of losing)"
238
+ )
239
+
240
+
241
+ def test_hard_has_two_spawn_groups_with_distinct_starts():
242
+ """Hard-tier curation contract: ≥2 agent spawn_point groups, and
243
+ the seeds (1, 2, 3, 4) must actually produce ≥2 distinct starts
244
+ on the live engine (the whole MCV CLUSTER rotates between WEST
245
+ and EAST). Mirrors `tests/test_hard_tier.py` for early signal."""
246
+ from pathlib import Path
247
+
248
+ from openra_bench.eval_core import _scenario_to_tmp_yaml, RustEnvPool
249
+ from openra_bench.rust_adapter import RustObsAdapter
250
+
251
+ pack = load_pack(PACK)
252
+ c = compile_level(pack, "hard")
253
+ sp = {
254
+ (a.spawn_point if a.spawn_point is not None else 0)
255
+ for a in c.scenario.actors
256
+ if a.owner == "agent"
257
+ }
258
+ assert len(sp) >= 2, (
259
+ f"hard must define ≥2 agent spawn_point groups; got {sorted(sp)}"
260
+ )
261
+
262
+ starts = set()
263
+ tmp = _scenario_to_tmp_yaml(c)
264
+ pool = RustEnvPool(size=1, scenario_path=tmp)
265
+ env = pool.acquire()
266
+ try:
267
+ for seed in HARD_SEEDS:
268
+ ad = RustObsAdapter()
269
+ ad.observe(env.reset(seed=seed))
270
+ u = ad.render_state().get("units_summary", []) or []
271
+ mcvs = sorted(
272
+ (x["cell_x"], x["cell_y"])
273
+ for x in u
274
+ if str(x.get("type", "")).lower() == "mcv"
275
+ )
276
+ starts.add(tuple(mcvs))
277
+ finally:
278
+ pool.release(env)
279
+ pool.shutdown()
280
+ Path(tmp).unlink(missing_ok=True)
281
+ assert len(starts) >= 2, (
282
+ f"hard seeds {HARD_SEEDS} produced identical MCV starts "
283
+ f"{starts}; spawn_point round-robin not taking effect"
284
+ )
285
+
286
+
287
+ def test_meta_benchmark_anchor_carries_required_strings():
288
+ """The pack must declare the four benchmark / real-world anchors
289
+ the brief required (SC2LE / MicroRTS / distribution-network /
290
+ tri-region operational footprint)."""
291
+ pack = load_pack(PACK)
292
+ anchors = set(pack.meta.benchmark_anchor)
293
+ required = {
294
+ "SC2LE 3-base macro",
295
+ "MicroRTS multi-front expansion",
296
+ "distribution-network multi-warehouse planning",
297
+ "tri-region operational footprint",
298
+ }
299
+ assert required.issubset(anchors), (
300
+ f"missing benchmark anchors: {required - anchors}"
301
+ )
302
+
303
+
304
+ def test_pack_compiles_with_arena_base_map():
305
+ """The pack-level base_map is a known-supported sentinel; the
306
+ real generator-spec arena is materialised per-level via
307
+ `overrides.base_map` (the documented idiom — a Level-level
308
+ `base_map` outside `overrides:` is silently ignored). All three
309
+ levels must compile onto the generated arena."""
310
+ pack = load_pack(PACK)
311
+ for lvl in LEVELS:
312
+ c = compile_level(pack, lvl)
313
+ assert c.scenario.base_map == "mcv-deploy-third-base-arena", (
314
+ f"{lvl}: expected mcv-deploy-third-base-arena, got "
315
+ f"{c.scenario.base_map}"
316
+ )
317
+ assert c.map_supported, (
318
+ f"{lvl}: mcv-deploy-third-base-arena must resolve to a "
319
+ "real .oramap (mapgen materialise should have written it)"
320
+ )