yxc20098 commited on
Commit
58991b9
·
1 Parent(s): 9970161

feat(scenario): rob-multiple-simultaneous-pressures — compound-incident triage (SC2 multi-front / ops triage anchor)

Browse files

Wave-6 REASONING pack: agent must hold THREE pressures simultaneously
— defence (hunt charge on the fact), economy (raider tanks on the
harv patch), and tech (war factory by hard deadline). No single-axis
play wins.

Design highlights
- bot_type: hunt (each enemy unit attacks its OWN nearest agent
target). Two threat axes emerge from geometry — centre-line
tank raiders hunt the nearest harv, flanking infantry hunt the
base buildings — without needing two bot types.
- starting_cash 2000 = exactly the weap cost. The intended play
queues weap on turn 1 and tops up cash from the harvest loop;
pulling harvs home strands the EV bar (economy_value_gte: 1000)
which makes the harvest loop load-bearing through the raid.
- Tools: full set. Pre-placed: fact + proc + powr + 2 harv +
2 mines + 4 1tnk defenders (powr added so the proc/weap chain
has power per gamerules.rs prereqs).
- Hard tier: 2 spawn_point groups (NORTH y=14 / SOUTH y=26)
round-robined by seed; centre-line raiders + hunt squad face
the active spawn with equal pressure on either latitude.
- Anti-DRAW: unarmed enemy fact marker far east per CLAUDE.md
auto-done footgun.

Discrimination (scripted, all seeds 1..4 on every tier)
- stall (only observe) → LOSS (clock + harv-loss)
- focus-defence-skip-tech → LOSS (weap never built)
- focus-tech-lose-econ (no harvest) → LOSS (EV bar never met)
- intended multi-handle → WIN

Registered in tests/test_hard_tier.py::UPGRADED.

Smoke (Qwen/Qwen3.6-Plus, medium s=1): loss composite=0.1894 —
valid model discrimination on the simultaneous-pressure axis.

openra_bench/scenarios/packs/rob-multiple-simultaneous-pressures.yaml ADDED
@@ -0,0 +1,396 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # rob-multiple-simultaneous-pressures — compound-incident triage
2
+ #
3
+ # REASONING capability — Wave-6 "multi-front simultaneous pressure"
4
+ # pack. The agent must hold THREE pressures simultaneously, none of
5
+ # which can be sequenced or paused:
6
+ #
7
+ # 1. DEFENCE — the agent fact is under a `hunt` charge that always
8
+ # picks the nearest agent target. Even if the harvesters die, the
9
+ # hunters keep coming for the fact / proc.
10
+ # 2. ECONOMY — pre-placed harvesters (`harv`) work the patch east of
11
+ # the proc and are the closest agent target to the raider tanks
12
+ # lined up at the lane mouth. Stop harvesting and income stops;
13
+ # lose a harv and the win predicate breaks (harv≥1).
14
+ # 3. TECH — a war factory (`weap`, cost 2000, prereq `proc`) must be
15
+ # built and standing by tick 5400. starting_cash 2000 covers the
16
+ # base weap exactly; the chain only closes if the harvesters keep
17
+ # running through the raid (so cash is in the tin when defence
18
+ # tightens). Skipping the tech to focus on combat fails the win.
19
+ #
20
+ # Real-world anchor: ops triage during a compound incident — multiple
21
+ # system failures at once. SC2 multi-front simultaneous pressure. DR
22
+ # multi-system failure response. The discriminating policies:
23
+ #
24
+ # • stall (only observe): LOSS — no weap built, harvs don't deliver
25
+ # (`harvest` order needed each turn), eventually clock expires.
26
+ # • focus-defence-skip-tech (queue defenders, hold the line, never
27
+ # start weap): LOSS — weap never appears ⇒ the has_building:weap
28
+ # clause never latches ⇒ timeout LOSS via after_ticks.
29
+ # • focus-tech-lose-econ (queue weap immediately, pull defenders
30
+ # back to base, abandon the harvs / never issue harvest orders):
31
+ # LOSS — the post-weap EV bar (economy_value_gte: 1000) never
32
+ # latches because starting_cash 2000 is consumed by weap and
33
+ # the idle harvs add no income ⇒ timeout LOSS via after_ticks.
34
+ # (Lose-the-harv mode is also possible if the raiders reach
35
+ # the unprotected harvs, but the EV teeth bite first.)
36
+ # • intended multi-handle (keep harv ring on the patch, defenders
37
+ # intercept raiders, queue weap from turn 1, fund it with the
38
+ # harv income that keeps flowing): WIN.
39
+ #
40
+ # DESIGN
41
+ # ──────
42
+ # - bot_type: `hunt`. Each enemy unit attacks its OWN nearest agent
43
+ # target (Infantry / Vehicle / MCV / Building per scripted_bot.rs
44
+ # `foes()`). The TANK raiders are lined up directly east of the
45
+ # harv lane (their nearest agent target is the harv at the patch
46
+ # ⇒ economy harassment); the INFANTRY hunters are staged on
47
+ # FLANKING latitudes where the nearest agent target is the fact /
48
+ # proc / powr (⇒ base-hunt pressure). Same bot, two threat axes
49
+ # driven by geometry — the "raider for econ + hunt for base" of
50
+ # the spec is realised by positioning, not by two bot types.
51
+ # - starting_cash: 2000 — exactly the weap cost. The intended play
52
+ # queues weap immediately and tops up cash from the running harv
53
+ # loop while the defenders intercept the raiders. Stalling lets
54
+ # the cash sit unused; spending it on defenders pre-empts weap.
55
+ # - Tools: full set (observe + build + place_building + harvest +
56
+ # move_units + attack_unit + attack_move + stop). The pack tests
57
+ # simultaneous management, not tool restriction.
58
+ # - Pre-placed: fact + proc + powr (power for the queue) + 2 harv +
59
+ # 2 mines + 4 1tnk defenders. Powr is included because weap is
60
+ # queued from the Building list — without power the build is
61
+ # silently slow / blocked (CLAUDE.md underpowered-queue footgun).
62
+ # The spec lists "fact + proc + 2 harv + 2 mines + 4 defenders" as
63
+ # the core seed; powr is treated as core infrastructure (proc
64
+ # itself needs powr per gamerules.rs).
65
+ # - Anti-DRAW: unarmed enemy `fact` marker far east — the engine
66
+ # auto-`done`s on enemy-MustBeDestroyed wipe before the win/fail
67
+ # check otherwise (CLAUDE.md auto-done footgun).
68
+ #
69
+ # Engine facts used (CLAUDE.md / gamerules.rs):
70
+ # - weap cost 2000, prereq `proc` (pre-placed ⇒ available T1).
71
+ # - proc prereq is `powr` (pre-placed ⇒ proc functions).
72
+ # - hunt bot's foes() includes Building, so flanking infantry that
73
+ # have the fact as their nearest target attack the base.
74
+ # - `harv` Command.harvest(uid, mx, my) per turn keeps the harvest
75
+ # loop running; idle harvs ⇒ no income.
76
+ # - place_building has no adjacency requirement; weap can be
77
+ # placed anywhere in-bounds (we pre-pick a safe spot via the
78
+ # description's coordinate hint).
79
+ # - hard tier: ≥2 agent spawn_point groups (UPGRADED contract,
80
+ # tests/test_hard_tier.py) — north-flank vs south-flank base.
81
+ #
82
+ # Validate (no model / no network):
83
+ # python3 -m pytest tests/test_rob_multiple_simultaneous_pressures.py -q
84
+
85
+ meta:
86
+ id: rob-multiple-simultaneous-pressures
87
+ title: 'Compound Incident — Multi-Front Simultaneous Pressure (Defence + Economy + Tech)'
88
+ capability: reasoning
89
+ real_world_meaning: >
90
+ Ops triage during a compound incident — three independent failures
91
+ must be handled simultaneously, none of which can be sequenced or
92
+ paused. Your base is under a hunt charge (defence), the harvesters
93
+ feeding income are being raided (economy), and a tech objective
94
+ (war factory) must be completed before a hard deadline. Focusing
95
+ on any single pressure forfeits the others; the intended play
96
+ keeps all three loops running in parallel, funding the tech with
97
+ the harvest income that the defenders are simultaneously
98
+ protecting.
99
+ robotics_analogue: >
100
+ Disaster-recovery multi-system failure — a control room handles
101
+ a security event (perimeter under attack), a throughput event
102
+ (data pipeline workers under harassment), and a planned upgrade
103
+ (capability rollout) at the same time. Pausing the upgrade to
104
+ fight the security incident misses the rollout SLA; abandoning
105
+ the pipeline to harden the perimeter starves the upgrade of
106
+ resources. The capability under test is parallel-task triage
107
+ under genuine simultaneous pressure.
108
+ benchmark_anchor:
109
+ - 'SC2 multi-front simultaneous pressure'
110
+ - 'ops triage during compound incident'
111
+ - 'DR multi-system failure'
112
+ - 'military theater-wide simultaneous threats'
113
+ author: openra-bench
114
+
115
+ base_map: rush-hour-arena
116
+ # Exactly enough for the weap (cost 2000). The intended play funds
117
+ # any extra defender / structure spend from the harvest loop income
118
+ # that runs THROUGH the raid. Anything more than 2000 lets a
119
+ # focus-defence policy buy out the threat without ever touching tech.
120
+ starting_cash: 2000
121
+
122
+ base:
123
+ agent:
124
+ faction: allies
125
+ # `hunt`: each enemy unit attacks its OWN nearest agent target
126
+ # (Infantry/Vehicle/MCV/Building). The geometry of the placed
127
+ # enemy units splits the pressure into two axes — tanks at the
128
+ # patch lane hunt the harvs (economy raid), flanking infantry
129
+ # hunt the fact/proc (base hunt).
130
+ enemy:
131
+ faction: soviet
132
+ bot_type: hunt
133
+ tools:
134
+ - observe
135
+ - build
136
+ - place_building
137
+ - harvest
138
+ - move_units
139
+ - attack_unit
140
+ - attack_move
141
+ - stop
142
+ planning: true
143
+ # No interrupts: fixed-cadence steps give the predictable
144
+ # ~90 ticks/turn we need to align the after_ticks 5401 fail
145
+ # clause with max_turns 61 (93+90·60=5493 ≥ 5401 ✓). Interrupt
146
+ # mode shortens the per-step advance and would make a stall
147
+ # episode draw before the deadline bites.
148
+ termination:
149
+ max_ticks: 12000
150
+ # Per-level actors via overrides — keeps the audit explicit for
151
+ # each tier's threat composition.
152
+ actors: []
153
+
154
+ levels:
155
+ # ── EASY ─────────────────────────────────────────────────────────
156
+ # Bare three-pressure skill: ONE flanking hunt group (only the base
157
+ # pressure axis), pre-placed defenders blunt the raid mouth, weap
158
+ # is the sole tech clause. Stall / focus-defence-skip-tech /
159
+ # focus-tech-lose-econ all LOSE; the intended multi-handle WINS.
160
+ # max_turns 61 → reachable tick 93 + 90·60 = 5493 > 5401 ✓.
161
+ easy:
162
+ description: >
163
+ Compound incident. You hold a base on the west — Construction
164
+ Yard (fact at 10,18), refinery (proc at 8,18), power plant
165
+ (powr at 8,20), and 2 harvesters working an ore patch east of
166
+ the proc (mines at 24,16 and 24,22). Four light tank defenders
167
+ ring the patch at x=18. You start with $2000 — exactly enough
168
+ for a war factory. THREE pressures are live AT THE SAME TIME:
169
+ (1) DEFEND — a single hunt squad of 3 rifle infantry is staged
170
+ NORTH of the base; they charge whatever agent target is
171
+ nearest, including the fact.
172
+ (2) ECONOMY — one enemy tank (raider) is at the lane mouth
173
+ east of the patch; under the hunt bot it targets the
174
+ harvester nearest to it. If the harvs die the win breaks.
175
+ (3) TECH — you must have a war factory (weap, cost 2000, prereq
176
+ proc) standing by tick 5400.
177
+ Win = `weap` AND `proc` AND ≥1 harvester AND `fact` still
178
+ standing, before tick 5400. Stalling, focusing on defence and
179
+ skipping tech, and focusing on tech while abandoning the
180
+ harvesters all LOSE.
181
+ overrides:
182
+ actors:
183
+ # Agent base on the west — fact + proc + powr + 2 harv +
184
+ # 2 mines + 4 defenders per the pack core.
185
+ - {type: fact, owner: agent, position: [10, 18]}
186
+ - {type: proc, owner: agent, position: [8, 18]}
187
+ - {type: powr, owner: agent, position: [8, 20]}
188
+ - {type: harv, owner: agent, position: [14, 16]}
189
+ - {type: harv, owner: agent, position: [14, 22]}
190
+ - {type: mine, owner: neutral, position: [24, 16]}
191
+ - {type: mine, owner: neutral, position: [24, 22]}
192
+ # Four 1tnk defenders ringing the patch lane at x=18
193
+ # (between the harvs and the raider mouth). Tank-on-tank
194
+ # is required to actually kill a raider 1tnk (rifle e1 dies
195
+ # before it can do it).
196
+ - {type: '1tnk', owner: agent, position: [18, 16]}
197
+ - {type: '1tnk', owner: agent, position: [18, 22]}
198
+ - {type: '1tnk', owner: agent, position: [22, 18]}
199
+ - {type: '1tnk', owner: agent, position: [22, 20]}
200
+ # ECONOMY-RAID PRESSURE — 1 raider tank at the lane mouth.
201
+ # Its nearest agent target is the harv at (14,16) ⇒ raids the
202
+ # economy under hunt semantics.
203
+ - {type: '1tnk', owner: enemy, position: [60, 18]}
204
+ # BASE-HUNT PRESSURE — 3 e1 infantry staged on the NORTHERN
205
+ # flank. Their nearest agent target is fact/powr (no harv
206
+ # between them and the base from y=4), so under hunt the
207
+ # group charges the base buildings directly.
208
+ - {type: e1, owner: enemy, position: [40, 4], stance: 3, count: 3}
209
+ # Anti-DRAW: persistent unarmed enemy `fact` marker far east.
210
+ - {type: fact, owner: enemy, position: [120, 4]}
211
+ win_condition:
212
+ all_of:
213
+ - {has_building: weap}
214
+ - {has_building: proc}
215
+ - {unit_type_count_gte: {type: harv, n: 1}}
216
+ - {has_building: fact}
217
+ # ECONOMY teeth: post-weap residual EV bar. starting_cash 2000
218
+ # exactly funds the weap (cost 2000) ⇒ to clear this 1000-EV
219
+ # bar the harvesters MUST be in `harvest` mode through the
220
+ # raid so cash refills. A focus-tech policy that queues weap
221
+ # and pulls the harvs home (no harvest order) drains cash to
222
+ # 0 and never re-fills ⇒ this clause never latches ⇒ LOSS by
223
+ # the after_ticks fail clause.
224
+ - {economy_value_gte: 1000}
225
+ - {within_ticks: 5400}
226
+ # 5401 ≤ 93 + 90·60 = 5493 ⇒ deadline bites inside max_turns.
227
+ # Loss is reachable: clock OR fact razed OR harvs eradicated.
228
+ fail_condition:
229
+ any_of:
230
+ - {after_ticks: 5401}
231
+ - {not: {has_building: fact}}
232
+ - {not: {unit_type_count_gte: {type: harv, n: 1}}}
233
+ max_turns: 61
234
+
235
+ # ── MEDIUM ───────────────────────────────────────────────────────
236
+ # +1 controlled variable vs easy: the raid axis now has TWO raider
237
+ # tanks (one at each patch latitude — the harv ring can't ignore
238
+ # either) AND the hunt squad is heavier (4× e1 + 1× e3). Same
239
+ # clock; the budget is still 2000 ⇒ no slack on the cash side.
240
+ medium:
241
+ description: >
242
+ The same compound incident, scaled up on both pressure axes.
243
+ Base layout unchanged (fact+proc+powr at the west, 2 harvs on
244
+ the patch east of proc, 4 light tank defenders on the ring).
245
+ Pressures:
246
+ (1) DEFEND — a heavier hunt squad of 4 rifle infantry + 1
247
+ rocket soldier (e3) on the northern flank charges the base.
248
+ (2) ECONOMY — TWO raider tanks are staged at the patch lane
249
+ mouth, one on each harv latitude (y=16 and y=22) — the
250
+ harv ring can't ignore either.
251
+ (3) TECH — war factory (`weap`) by tick 5400.
252
+ Win = `weap` AND `proc` AND ≥1 harvester AND `fact` standing
253
+ before tick 5400. Stall, focus-defence-skip-tech,
254
+ focus-tech-lose-econ all LOSE.
255
+ overrides:
256
+ actors:
257
+ - {type: fact, owner: agent, position: [10, 18]}
258
+ - {type: proc, owner: agent, position: [8, 18]}
259
+ - {type: powr, owner: agent, position: [8, 20]}
260
+ - {type: harv, owner: agent, position: [14, 16]}
261
+ - {type: harv, owner: agent, position: [14, 22]}
262
+ - {type: mine, owner: neutral, position: [24, 16]}
263
+ - {type: mine, owner: neutral, position: [24, 22]}
264
+ - {type: '1tnk', owner: agent, position: [18, 16]}
265
+ - {type: '1tnk', owner: agent, position: [18, 22]}
266
+ - {type: '1tnk', owner: agent, position: [22, 18]}
267
+ - {type: '1tnk', owner: agent, position: [22, 20]}
268
+ # ECONOMY-RAID — two raiders, one per harv latitude.
269
+ - {type: '1tnk', owner: enemy, position: [60, 16]}
270
+ - {type: '1tnk', owner: enemy, position: [60, 22]}
271
+ # BASE-HUNT — 4 e1 + 1 e3 on the northern flank.
272
+ - {type: e1, owner: enemy, position: [40, 4], stance: 3, count: 4}
273
+ - {type: e3, owner: enemy, position: [42, 4], stance: 3, count: 1}
274
+ # Anti-DRAW marker.
275
+ - {type: fact, owner: enemy, position: [120, 4]}
276
+ win_condition:
277
+ all_of:
278
+ - {has_building: weap}
279
+ - {has_building: proc}
280
+ - {unit_type_count_gte: {type: harv, n: 1}}
281
+ - {has_building: fact}
282
+ # See easy: post-weap EV bar makes the harvest loop load-
283
+ # bearing so focus-tech-lose-econ cannot satisfy the win.
284
+ - {economy_value_gte: 1000}
285
+ - {within_ticks: 5400}
286
+ fail_condition:
287
+ any_of:
288
+ - {after_ticks: 5401}
289
+ - {not: {has_building: fact}}
290
+ - {not: {unit_type_count_gte: {type: harv, n: 1}}}
291
+ max_turns: 61
292
+
293
+ # ── HARD ─────────────────────────────────────────────────────────
294
+ # +2 controlled variables vs medium (per the spec "hunt + raider +
295
+ # tighter clock + 2 spawn groups"):
296
+ # 1. TIGHTER clock (within_ticks 4500; max_turns 51 → reachable
297
+ # tick 93+90·50 = 4593 ≥ 4501 ✓). The tech window shrinks so a
298
+ # sequenced "defend then tech" attempt runs out of clock.
299
+ # 2. TWO spawn_point groups (north y=14 base vs south y=26 base)
300
+ # — the BASE LATITUDE flips by seed, so a single memorised
301
+ # ring / weap placement cannot generalise. Both spawns face
302
+ # the SAME enemy composition (enemy actors are not spawn-
303
+ # filtered, per oramap.rs): the medium pack's 2 raider tanks
304
+ # and one heavy hunt squad, plus a mirrored off-axis hunt
305
+ # squad so the seed that lands on the off-axis flank still
306
+ # gets a same-latitude charge (otherwise the off-axis flank
307
+ # would have noticeably lighter pressure than the on-axis
308
+ # one). Same composition as medium per axis — the discriminator
309
+ # is clock-tightness + spawn variation, not enemy-count
310
+ # escalation.
311
+ hard:
312
+ description: >
313
+ The compound incident on a tighter clock and a seed-varied
314
+ start. Your base stages from a seed-chosen latitude (NORTH
315
+ y=14 or SOUTH y=26) — both formations cover the same patch
316
+ column (mines at x=24, two per latitude). Pressures:
317
+ (1) DEFEND — a hunt squad of 4 rifle infantry + 1 rocket
318
+ soldier charges from the centre-line; whichever base
319
+ latitude you got is the one it heads for.
320
+ (2) ECONOMY — TWO raider tanks at the patch lane mouth on
321
+ the centre-line; they hunt the nearest harv (which is
322
+ always the active spawn's, since the off-axis spawn
323
+ isn't placed).
324
+ (3) TECH — war factory (`weap`) by tick 4500 (clock down from
325
+ 5400).
326
+ Win = `weap` AND `proc` AND ≥1 harvester AND `fact` standing
327
+ before tick 4500. Stall, focus-defence-skip-tech,
328
+ focus-tech-lose-econ, and slow-multi-handle (over-budget on
329
+ clock) all LOSE.
330
+ overrides:
331
+ actors:
332
+ # Spawn 0 — NORTH base (y centered on 14). Four 1tnk
333
+ # defenders ring the two harv latitudes (12 and 16).
334
+ - {type: fact, owner: agent, position: [10, 14], spawn_point: 0}
335
+ - {type: proc, owner: agent, position: [8, 14], spawn_point: 0}
336
+ - {type: powr, owner: agent, position: [8, 16], spawn_point: 0}
337
+ - {type: harv, owner: agent, position: [14, 12], spawn_point: 0}
338
+ - {type: harv, owner: agent, position: [14, 16], spawn_point: 0}
339
+ - {type: '1tnk', owner: agent, position: [18, 12], spawn_point: 0}
340
+ - {type: '1tnk', owner: agent, position: [18, 16], spawn_point: 0}
341
+ - {type: '1tnk', owner: agent, position: [22, 13], spawn_point: 0}
342
+ - {type: '1tnk', owner: agent, position: [22, 15], spawn_point: 0}
343
+ # Spawn 1 — SOUTH base (y centered on 26). Mirror of spawn 0
344
+ # at y=24/28.
345
+ - {type: fact, owner: agent, position: [10, 26], spawn_point: 1}
346
+ - {type: proc, owner: agent, position: [8, 26], spawn_point: 1}
347
+ - {type: powr, owner: agent, position: [8, 28], spawn_point: 1}
348
+ - {type: harv, owner: agent, position: [14, 24], spawn_point: 1}
349
+ - {type: harv, owner: agent, position: [14, 28], spawn_point: 1}
350
+ - {type: '1tnk', owner: agent, position: [18, 24], spawn_point: 1}
351
+ - {type: '1tnk', owner: agent, position: [18, 28], spawn_point: 1}
352
+ - {type: '1tnk', owner: agent, position: [22, 25], spawn_point: 1}
353
+ - {type: '1tnk', owner: agent, position: [22, 27], spawn_point: 1}
354
+ # Mines — neutrals are NOT spawn-filtered (per CLAUDE.md);
355
+ # one set per latitude so each spawn has a workable patch
356
+ # on each of its two harv rows.
357
+ - {type: mine, owner: neutral, position: [24, 12]}
358
+ - {type: mine, owner: neutral, position: [24, 16]}
359
+ - {type: mine, owner: neutral, position: [24, 24]}
360
+ - {type: mine, owner: neutral, position: [24, 28]}
361
+ # ECONOMY-RAID — TWO raider tanks (matching medium) at the
362
+ # patch lane mouth. Placed at the centre-line latitudes
363
+ # (y=18, y=22) so they sit equidistant from the two
364
+ # possible base latitudes (y=14 north / y=26 south) and the
365
+ # hunt bot resolves each raider to whichever harv is
366
+ # currently nearest. (Enemy actors are not spawn-filtered.)
367
+ - {type: '1tnk', owner: enemy, position: [60, 18]}
368
+ - {type: '1tnk', owner: enemy, position: [60, 22]}
369
+ # BASE-HUNT — one heavy hunt squad (matching medium: 4 e1 +
370
+ # 1 e3) at the centre-line so it charges the nearest base
371
+ # buildings whichever latitude the agent is on (the e1 at
372
+ # the centre is ≤12 cells from either spawn's fact, and the
373
+ # rotation between spawns is the seed-driven variation).
374
+ - {type: e1, owner: enemy, position: [40, 20], stance: 3, count: 4}
375
+ - {type: e3, owner: enemy, position: [42, 20], stance: 3, count: 1}
376
+ # Anti-DRAW marker.
377
+ - {type: fact, owner: enemy, position: [120, 20]}
378
+ win_condition:
379
+ all_of:
380
+ - {has_building: weap}
381
+ - {has_building: proc}
382
+ - {unit_type_count_gte: {type: harv, n: 1}}
383
+ - {has_building: fact}
384
+ # See easy: post-weap EV bar makes the harvest loop load-
385
+ # bearing so focus-tech-lose-econ cannot satisfy the win.
386
+ # Bar held at 1000 (same as easy/medium) — the discriminator
387
+ # on hard is the tighter clock + spawn variation, not a
388
+ # harder EV target.
389
+ - {economy_value_gte: 1000}
390
+ - {within_ticks: 4500}
391
+ fail_condition:
392
+ any_of:
393
+ - {after_ticks: 4501}
394
+ - {not: {has_building: fact}}
395
+ - {not: {unit_type_count_gte: {type: harv, n: 1}}}
396
+ max_turns: 51
tests/test_hard_tier.py CHANGED
@@ -486,6 +486,12 @@ UPGRADED = [
486
  # assignment flips per seed and a memorised "which jeep to which
487
  # corner" plan cannot generalise.
488
  "scout-map-reveal-percent-target",
 
 
 
 
 
 
489
  ]
490
 
491
  # Consciously NOT spawn-varied, with the reason (keeps the curation
 
486
  # assignment flips per seed and a memorised "which jeep to which
487
  # corner" plan cannot generalise.
488
  "scout-map-reveal-percent-target",
489
+ # Wave-6 compound-incident triage pack (defence + economy + tech
490
+ # under simultaneous pressure). Hard tier defines two agent
491
+ # spawn_point groups (NORTH y=14 / SOUTH y=26) round-robined by
492
+ # seed; the centre-line raider tanks and centre-line hunt squad
493
+ # face the seed-chosen latitude with equal pressure.
494
+ "rob-multiple-simultaneous-pressures",
495
  ]
496
 
497
  # Consciously NOT spawn-varied, with the reason (keeps the curation
tests/test_rob_multiple_simultaneous_pressures.py ADDED
@@ -0,0 +1,437 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """rob-multiple-simultaneous-pressures — compound-incident triage.
2
+
3
+ Wave-6 REASONING pack: three live pressures must be handled at once —
4
+ DEFENCE (hunt charge on the fact), ECONOMY (raider tanks on the harv
5
+ patch), TECH (`weap` by deadline). Single-axis policies cannot win;
6
+ the intended multi-handle keeps all three loops running in parallel.
7
+
8
+ Bar (per CLAUDE.md): the intended multi-handle policy must WIN on
9
+ every (level, seed); stall / focus-defence-skip-tech / focus-tech-
10
+ lose-econ must LOSE on every (level, seed). No draws.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import types
16
+ from pathlib import Path
17
+
18
+ import pytest
19
+
20
+ pytest.importorskip("openra_rl_training", reason="Rust env wheel not installed")
21
+ from openra_bench.scenarios import load_pack
22
+ from openra_bench.scenarios.loader import compile_level
23
+ from openra_bench.scenarios.win_conditions import WinContext, evaluate
24
+
25
+ PACKS = Path(__file__).parent.parent / "openra_bench" / "scenarios" / "packs"
26
+ PACK_PATH = PACKS / "rob-multiple-simultaneous-pressures.yaml"
27
+ LEVELS = ("easy", "medium", "hard")
28
+ SEEDS = (1, 2, 3, 4)
29
+
30
+
31
+ # ── Pack-shape tests (cheap; do not run the engine) ─────────────────
32
+
33
+
34
+ def test_pack_compiles_with_three_levels():
35
+ pack = load_pack(PACK_PATH)
36
+ assert pack.meta.id == "rob-multiple-simultaneous-pressures"
37
+ assert pack.meta.capability == "reasoning"
38
+ assert set(pack.levels) == {"easy", "medium", "hard"}
39
+
40
+
41
+ def test_meta_benchmark_anchor_includes_required_anchors():
42
+ pack = load_pack(PACK_PATH)
43
+ anchors = pack.meta.benchmark_anchor or []
44
+ joined = " ".join(anchors).lower()
45
+ for needle in (
46
+ "sc2 multi-front",
47
+ "ops triage",
48
+ "dr multi-system",
49
+ "military",
50
+ ):
51
+ assert needle in joined, f"missing anchor keyword: {needle!r}"
52
+
53
+
54
+ def test_uses_hunt_bot():
55
+ """A `hunt` bot drives both pressure axes — the raiders at the
56
+ patch hunt the harvesters; the flanking infantry hunt the base
57
+ buildings. The pack must declare `hunt`."""
58
+ pack = load_pack(PACK_PATH)
59
+ enemy = pack.base.get("enemy") if isinstance(pack.base, dict) else None
60
+ assert enemy is not None
61
+ bot = enemy.get("bot_type") or enemy.get("bot")
62
+ assert bot == "hunt", f"expected hunt bot, got {bot!r}"
63
+
64
+
65
+ def test_starting_cash_is_2000_exact():
66
+ """starting_cash must equal weap cost (2000) so cash slack cannot
67
+ be used to bypass either the tech or the income-from-harvest loop."""
68
+ pack = load_pack(PACK_PATH)
69
+ assert pack.starting_cash == 2000, (
70
+ f"expected starting_cash 2000 (weap cost), got {pack.starting_cash}"
71
+ )
72
+
73
+
74
+ def test_every_level_has_fail_condition():
75
+ pack = load_pack(PACK_PATH)
76
+ for lvl in LEVELS:
77
+ c = compile_level(pack, lvl)
78
+ assert c.fail_condition is not None, f"{lvl} missing fail_condition"
79
+
80
+
81
+ def test_hard_has_two_spawn_point_groups():
82
+ """Hard-tier curation contract: ≥2 distinct agent spawn_point
83
+ groups so the seed round-robins the base latitude."""
84
+ c = compile_level(load_pack(PACK_PATH), "hard")
85
+ sp = {
86
+ (a.spawn_point if a.spawn_point is not None else 0)
87
+ for a in c.scenario.actors
88
+ if a.owner == "agent"
89
+ }
90
+ assert len(sp) >= 2, f"hard needs ≥2 spawn_point groups, got {sp}"
91
+
92
+
93
+ def test_tick_budget_aligned_with_max_turns():
94
+ """within_ticks and after_ticks must be reachable inside max_turns
95
+ (~90 ticks/turn): otherwise the deadline never bites and a stall
96
+ draws instead of losing."""
97
+ pack = load_pack(PACK_PATH)
98
+ for lvl in LEVELS:
99
+ level_def = pack.levels[lvl]
100
+ max_turns = level_def.max_turns
101
+ reachable = 93 + 90 * (max_turns - 1)
102
+ win = compile_level(pack, lvl).win_condition.model_dump(exclude_none=True)
103
+
104
+ def _collect(node, key, out):
105
+ if isinstance(node, dict):
106
+ if key in node:
107
+ out.append(node[key])
108
+ for v in node.values():
109
+ _collect(v, key, out)
110
+ elif isinstance(node, list):
111
+ for v in node:
112
+ _collect(v, key, out)
113
+ wts: list = []
114
+ _collect(win, "within_ticks", wts)
115
+ assert wts, f"{lvl} has no within_ticks clock teeth"
116
+ for wt in wts:
117
+ assert wt <= reachable, (
118
+ f"{lvl} within_ticks={wt} > reachable={reachable}"
119
+ )
120
+ fail = compile_level(pack, lvl).fail_condition.model_dump(exclude_none=True)
121
+ ats: list = []
122
+ _collect(fail, "after_ticks", ats)
123
+ assert ats, f"{lvl} has no after_ticks fail teeth"
124
+ for at in ats:
125
+ assert at <= reachable, (
126
+ f"{lvl} after_ticks={at} > reachable={reachable}"
127
+ )
128
+
129
+
130
+ # ── Predicate-level checks (synthesised WinContext, no engine) ──────
131
+
132
+
133
+ def _ctx(*, own_buildings=(), units=(), tick=1000, cash=2000, resources=0):
134
+ sig = types.SimpleNamespace(
135
+ game_tick=tick,
136
+ units_killed=0,
137
+ units_lost=0,
138
+ own_buildings=list(own_buildings),
139
+ own_building_types={t for (t, _, _) in own_buildings},
140
+ enemies_seen_ids=set(),
141
+ enemy_buildings_seen_ids=set(),
142
+ cash=cash,
143
+ resources=resources,
144
+ then_progress={},
145
+ )
146
+ return WinContext(
147
+ signals=sig,
148
+ render_state={"units_summary": list(units)},
149
+ )
150
+
151
+
152
+ def test_predicates_easy():
153
+ c = compile_level(load_pack(PACK_PATH), "easy")
154
+ full = [
155
+ ("fact", 10, 18), ("proc", 8, 18), ("powr", 8, 20),
156
+ ("weap", 14, 18),
157
+ ]
158
+ harvs = [
159
+ {"cell_x": 14, "cell_y": 16, "type": "harv"},
160
+ {"cell_x": 14, "cell_y": 22, "type": "harv"},
161
+ ]
162
+ # WIN: weap + proc + ≥1 harv + fact + EV ≥ 1000, in time.
163
+ assert evaluate(
164
+ c.win_condition,
165
+ _ctx(own_buildings=full, units=harvs, tick=3000, cash=1200),
166
+ )
167
+ # No weap → not a win.
168
+ no_weap = [b for b in full if b[0] != "weap"]
169
+ assert not evaluate(
170
+ c.win_condition,
171
+ _ctx(own_buildings=no_weap, units=harvs, tick=3000, cash=1200),
172
+ )
173
+ # EV below 1000 → not a win (focus-tech-lose-econ failure mode).
174
+ assert not evaluate(
175
+ c.win_condition,
176
+ _ctx(own_buildings=full, units=harvs, tick=3000, cash=0),
177
+ )
178
+ # No harv → fail.
179
+ assert evaluate(
180
+ c.fail_condition,
181
+ _ctx(own_buildings=full, units=[], tick=3000, cash=1200),
182
+ )
183
+ # No fact (base razed) → fail.
184
+ no_fact = [b for b in full if b[0] != "fact"]
185
+ assert evaluate(
186
+ c.fail_condition,
187
+ _ctx(own_buildings=no_fact, units=harvs, tick=3000, cash=1200),
188
+ )
189
+ # Past deadline → fail.
190
+ assert evaluate(
191
+ c.fail_condition,
192
+ _ctx(own_buildings=full, units=harvs, tick=5402, cash=1200),
193
+ )
194
+
195
+
196
+ def test_predicates_hard_tighter_clock():
197
+ """Hard's within_ticks is 4500 (tighter than easy/medium's 5400)."""
198
+ c = compile_level(load_pack(PACK_PATH), "hard")
199
+ full = [
200
+ ("fact", 10, 14), ("proc", 8, 14), ("powr", 8, 16),
201
+ ("weap", 14, 14),
202
+ ]
203
+ harvs = [{"cell_x": 14, "cell_y": 12, "type": "harv"}]
204
+ assert evaluate(
205
+ c.win_condition,
206
+ _ctx(own_buildings=full, units=harvs, tick=4000, cash=1200),
207
+ )
208
+ assert evaluate(
209
+ c.fail_condition,
210
+ _ctx(own_buildings=full, units=harvs, tick=4501, cash=1200),
211
+ )
212
+
213
+
214
+ # ── engine-driven scripted policies ─────────────────────────────────
215
+
216
+
217
+ def _find_units(rs, type_name):
218
+ return [
219
+ u for u in (rs.get("units_summary", []) or [])
220
+ if str(u.get("type", "")).lower() == type_name
221
+ ]
222
+
223
+
224
+ def _base_xy(rs):
225
+ facts = [
226
+ b for b in (rs.get("own_buildings", []) or [])
227
+ if str(b.get("type", "")).lower() == "fact"
228
+ ]
229
+ if not facts:
230
+ return None
231
+ return min(facts, key=lambda b: b["cell_x"])
232
+
233
+
234
+ def _stall(rs, Command):
235
+ """Idle every turn — no harvest, no build, no defence orders.
236
+ Defenders auto-engage but harvs never harvest (no income) and
237
+ weap is never built ⇒ has_building:weap never latches ⇒ timeout
238
+ LOSS via after_ticks."""
239
+ return [Command.observe()]
240
+
241
+
242
+ def _focus_defence_skip_tech(rs, Command):
243
+ """Hold every defender at the lane mouth; KICK harvs on the patch
244
+ (income flows); but NEVER queue weap. Weap is required by the win
245
+ predicate, so this LOSES on the clock even though the base may
246
+ survive."""
247
+ cmds = []
248
+ base = _base_xy(rs)
249
+ bx = base["cell_x"] if base else 10
250
+ by = base["cell_y"] if base else 18
251
+ harvs = _find_units(rs, "harv")
252
+ patch_xs = [24, 24, 24, 24]
253
+ patch_ys = [by - 2, by + 2, by - 4, by + 4]
254
+ for i, h in enumerate(harvs):
255
+ mx = patch_xs[i % len(patch_xs)]
256
+ my = patch_ys[i % len(patch_ys)]
257
+ cmds.append(Command.harvest([str(h["id"])], mx, my))
258
+ for u in (rs.get("units_summary", []) or []):
259
+ if str(u.get("type", "")).lower() == "1tnk":
260
+ cmds.append(
261
+ Command.move_units([str(u["id"])], target_x=bx + 4, target_y=by)
262
+ )
263
+ if not cmds:
264
+ cmds.append(Command.observe())
265
+ return cmds
266
+
267
+
268
+ def _focus_tech_lose_econ_policy():
269
+ """Queue weap from turn 1 (using the starting cash) BUT pull every
270
+ defender AND harvester to the base — the patch is abandoned, so
271
+ raiders eat the harvs (unit_type_count_gte:{harv,1} ⇒ fail). Even
272
+ if weap finishes, the harv-count fail clause kills the run."""
273
+ state = {"weap_queued": False, "weap_attempts": 0}
274
+
275
+ def pol(rs, Command):
276
+ ob = rs.get("own_buildings", []) or []
277
+ own_b = {b["type"] for b in ob}
278
+ prod = rs.get("production", []) or []
279
+ cash = int(rs.get("cash", 0) or 0)
280
+ base = _base_xy(rs)
281
+ bx = base["cell_x"] if base else 10
282
+ by = base["cell_y"] if base else 18
283
+ cmds = []
284
+ if (
285
+ "weap" not in own_b
286
+ and "weap" not in prod
287
+ and cash >= 2000
288
+ ):
289
+ cmds.append(Command.build("weap"))
290
+ state["weap_queued"] = True
291
+ if "weap" not in own_b:
292
+ i = state["weap_attempts"]
293
+ cmds.append(
294
+ Command.place_building("weap", bx + 6 + (i % 4), by - 2 + (i // 4))
295
+ )
296
+ state["weap_attempts"] += 1
297
+ for u in (rs.get("units_summary", []) or []):
298
+ cmds.append(
299
+ Command.move_units([str(u["id"])], target_x=bx, target_y=by)
300
+ )
301
+ if not cmds:
302
+ cmds.append(Command.observe())
303
+ return cmds
304
+
305
+ return pol
306
+
307
+
308
+ def _intended_multi_handle_policy():
309
+ """The intended REASONING capability play: handle all three
310
+ pressures in parallel from turn 1.
311
+
312
+ PRESSURE A (economy/raid): keep both harvesters in `harvest`
313
+ mode every turn — they cycle between proc and the local
314
+ mine; income accrues to top up the tech buy. The pre-placed
315
+ tank ring is on Defend stance and auto-fires on incoming
316
+ raiders at the patch lane mouth (no explicit move orders
317
+ needed — moving the defenders OFF the ring would abandon
318
+ the patch and lose).
319
+ PRESSURE B (defence/hunt): the ring tanks ALSO catch any
320
+ flanking hunter that crosses the patch column on its way to
321
+ the fact (Defend stance auto-engage). For close-in hunters
322
+ the harv proximity is enough to make them targets too.
323
+ PRESSURE C (tech): queue `weap` on turn 1 with the starting
324
+ cash, place it immediately in a safe slot west of the base
325
+ (between fact and proc, away from the hunt approach).
326
+ """
327
+ state = {"weap_queued": False, "weap_attempts": 0}
328
+
329
+ def pol(rs, Command):
330
+ ob = rs.get("own_buildings", []) or []
331
+ own_b = {b["type"] for b in ob}
332
+ prod = rs.get("production", []) or []
333
+ cash = int(rs.get("cash", 0) or 0)
334
+ base = _base_xy(rs)
335
+ bx = base["cell_x"] if base else 10
336
+ by = base["cell_y"] if base else 18
337
+ cmds = []
338
+
339
+ if (
340
+ "weap" not in own_b
341
+ and "weap" not in prod
342
+ and cash >= 2000
343
+ ):
344
+ cmds.append(Command.build("weap"))
345
+ state["weap_queued"] = True
346
+ if "weap" not in own_b:
347
+ i = state["weap_attempts"]
348
+ cmds.append(
349
+ Command.place_building(
350
+ "weap", bx + 4 + (i % 4), by + (i // 4)
351
+ )
352
+ )
353
+ state["weap_attempts"] += 1
354
+
355
+ harvs = _find_units(rs, "harv")
356
+ patch_xs = [24, 24, 24, 24]
357
+ patch_ys = [by - 2, by + 2, by - 4, by + 4]
358
+ for i, h in enumerate(harvs):
359
+ mx = patch_xs[i % len(patch_xs)]
360
+ my = patch_ys[i % len(patch_ys)]
361
+ cmds.append(Command.harvest([str(h["id"])], mx, my))
362
+
363
+ if not cmds:
364
+ cmds.append(Command.observe())
365
+ return cmds
366
+
367
+ return pol
368
+
369
+
370
+ # ── Engine-bound tests (parameterised over seeds 1..4) ───────────────
371
+
372
+
373
+ @pytest.mark.parametrize("seed", SEEDS)
374
+ @pytest.mark.parametrize("level", LEVELS)
375
+ def test_intended_multi_handle_wins(level, seed):
376
+ """Intended multi-handle must WIN on every (level, seed)."""
377
+ pytest.importorskip("openra_train")
378
+ from openra_bench.eval_core import run_level
379
+
380
+ c = compile_level(load_pack(PACK_PATH), level)
381
+ res = run_level(c, _intended_multi_handle_policy(), seed=seed)
382
+ own_b = res.signals.own_building_types
383
+ assert res.outcome == "win", (
384
+ f"intended multi-handle must WIN on {level} s={seed}; got "
385
+ f"{res.outcome} turns={res.turns} own_buildings={own_b} "
386
+ f"units_lost={res.signals.units_lost} cash={res.signals.cash}"
387
+ )
388
+
389
+
390
+ @pytest.mark.parametrize("seed", SEEDS)
391
+ @pytest.mark.parametrize("level", LEVELS)
392
+ def test_stall_loses(level, seed):
393
+ """Stall must be a real timeout LOSS on every (level, seed)."""
394
+ pytest.importorskip("openra_train")
395
+ from openra_bench.eval_core import run_level
396
+
397
+ c = compile_level(load_pack(PACK_PATH), level)
398
+ res = run_level(c, _stall, seed=seed)
399
+ assert res.outcome == "loss", (
400
+ f"stall must LOSS on {level} s={seed}; got {res.outcome} "
401
+ f"own_buildings={res.signals.own_building_types}"
402
+ )
403
+
404
+
405
+ @pytest.mark.parametrize("seed", SEEDS)
406
+ @pytest.mark.parametrize("level", LEVELS)
407
+ def test_focus_defence_skip_tech_loses(level, seed):
408
+ """Focus on defence and never queue weap ⇒ has_building:weap
409
+ never latches ⇒ timeout LOSS on every (level, seed)."""
410
+ pytest.importorskip("openra_train")
411
+ from openra_bench.eval_core import run_level
412
+
413
+ c = compile_level(load_pack(PACK_PATH), level)
414
+ res = run_level(c, _focus_defence_skip_tech, seed=seed)
415
+ own_b = res.signals.own_building_types
416
+ assert res.outcome == "loss", (
417
+ f"focus-defence-skip-tech must LOSE on {level} s={seed}; got "
418
+ f"{res.outcome} own_buildings={own_b}"
419
+ )
420
+
421
+
422
+ @pytest.mark.parametrize("seed", SEEDS)
423
+ @pytest.mark.parametrize("level", LEVELS)
424
+ def test_focus_tech_lose_econ_loses(level, seed):
425
+ """Focus on tech (queue weap, pull everything home) ⇒ harvs
426
+ abandoned ⇒ fail by harv-count (or by clock). Must LOSE."""
427
+ pytest.importorskip("openra_train")
428
+ from openra_bench.eval_core import run_level
429
+
430
+ c = compile_level(load_pack(PACK_PATH), level)
431
+ res = run_level(c, _focus_tech_lose_econ_policy(), seed=seed)
432
+ own_b = res.signals.own_building_types
433
+ assert res.outcome == "loss", (
434
+ f"focus-tech-lose-econ must LOSE on {level} s={seed}; got "
435
+ f"{res.outcome} own_buildings={own_b} "
436
+ f"units_lost={res.signals.units_lost}"
437
+ )