Spaces:
Running
feat(scenario): proc-instruction-following-edge-case — conditional SOP (IFBench / BFCL V4 anchor)
Browse filesWave-6 IFBench / BFCL V4 negative-instruction adherence pack:
selective-action discipline. The brief carries an explicit
conditional rule — 'Move the JEEP units to (90,20). Do NOT move the
TANK units.' — and the bench measures whether the agent honours both
halves.
Win predicate uses the type-filtered region predicate
`units_of_type_in_region_gte` (Wave-2 / Group-N) to check BOTH:
1. ≥3 jeeps in goal region around (90,20).
2. ≥3 tanks STILL in start region around (8,20).
Fail mirrors clause 2 as a `not` clause so a single tank dragged
out of the start cluster trips IMMEDIATE fail — the IFBench
instruction-precision teeth.
Tiers (one new variable per step):
easy = bare positive instruction only (no negative teeth — calibration).
medium = +negative teeth (must-not-move tanks anchored at (8,20)).
hard = +2 agent spawn_point groups (jeeps NW vs SW), tanks
duplicated at (8..10, 20) across BOTH groups so the
must-not-move region is identical across seeds.
Scripted validation (4 levels × 4 seeds each):
stall → LOSS (clock; after_ticks 4501 within max_turns 55).
move-all → WIN on easy (no teeth); LOSS turn 1 on medium/hard
(negative-instruction teeth fire when tank leaves cluster).
jeeps-only → WIN (intended capability play).
Registered in tests/test_hard_tier.py::UPGRADED (≥2 jeep spawn
groups, tanks anchored at common region across seeds).
Real-world anchor: IFBench conditional/edge-case instructions; BFCL
V4 negative-instruction compliance; conditional SOP; selective-action
discipline (mixed fleet, act on subset, leave the rest).
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# proc-instruction-following-edge-case — IFBench conditional / negative-
|
| 2 |
+
# instruction adherence (selective-action discipline).
|
| 3 |
+
#
|
| 4 |
+
# The mission brief carries an EXPLICIT CONDITIONAL RULE — "do X, but
|
| 5 |
+
# NOT Y" — and the bench measures whether the agent honours the
|
| 6 |
+
# carve-out. The intended capability is *selective action*: identify
|
| 7 |
+
# the units the instruction targets, act on those, and LEAVE THE OTHER
|
| 8 |
+
# UNITS WHERE THEY STARTED. Touching the prohibited group at all
|
| 9 |
+
# (even moving them a single cell) is a procedural violation and
|
| 10 |
+
# fails the mission outright, regardless of what the jeeps achieve.
|
| 11 |
+
#
|
| 12 |
+
# Concretely: the agent owns a mixed column — JEEPS and MEDIUM TANKS
|
| 13 |
+
# (2tnk) — at the west edge (x=8, y∈[18..22]). The brief says:
|
| 14 |
+
# "Move the JEEP units to (90, 20). Do NOT move the TANK units."
|
| 15 |
+
# The win predicate uses the type-filtered region predicate
|
| 16 |
+
# `units_of_type_in_region_gte` to check BOTH halves of the
|
| 17 |
+
# instruction simultaneously:
|
| 18 |
+
#
|
| 19 |
+
# 1. ≥3 JEEPS in region around (90, 20) — the positive instruction.
|
| 20 |
+
# 2. ≥3 TANKS still in region around (8, 20) — the negative
|
| 21 |
+
# instruction (the tanks must remain at their start).
|
| 22 |
+
#
|
| 23 |
+
# Both clauses must hold at the same observation tick — a "move tanks
|
| 24 |
+
# then later return them" play is structurally hard within the budget
|
| 25 |
+
# and is the wrong skill anyway (the brief forbade the move). The
|
| 26 |
+
# failure side mirrors clause 2 as a `not units_of_type_in_region_gte`
|
| 27 |
+
# clause so a single tank dragged out of the start cluster trips fail
|
| 28 |
+
# IMMEDIATELY (no need to wait for the deadline). This is the
|
| 29 |
+
# IFBench-style "instruction-precision" teeth: violation is detected
|
| 30 |
+
# the moment the agent acts on the prohibited group.
|
| 31 |
+
#
|
| 32 |
+
# Failure modes (all real, reachable LOSSES; no draw degeneracy):
|
| 33 |
+
# • STALL (only `observe`): jeeps never reach (90,20); the
|
| 34 |
+
# within_ticks deadline fires → after_ticks fail.
|
| 35 |
+
# • MOVE-ALL (the model treats the order as "move everything east"
|
| 36 |
+
# and ignores the carve-out): the tanks leave the start cluster
|
| 37 |
+
# within a few turns → `not units_of_type_in_region_gte 2tnk` fail
|
| 38 |
+
# fires as soon as <3 tanks remain near (8,20). Even if the jeeps
|
| 39 |
+
# reach (90,20), the run is forfeit — the brief said "do NOT move
|
| 40 |
+
# the tanks" and the agent moved them.
|
| 41 |
+
# • TANKS-ONLY (the model fixates on the wrong half — only
|
| 42 |
+
# tanks moved): the tanks leave the start AND the jeeps never
|
| 43 |
+
# reach the goal → both fail clauses fire.
|
| 44 |
+
# • INTENDED (move JEEPS east to (90,20), issue NO order to the
|
| 45 |
+
# tanks): jeeps arrive on time, tanks remain at start → WIN.
|
| 46 |
+
#
|
| 47 |
+
# Real-world anchor: IFBench / instruction-precision benchmarks
|
| 48 |
+
# ("output only X, do not include Y"); BFCL V4 negative-instruction
|
| 49 |
+
# compliance ("call this tool but DO NOT call that one"); conditional
|
| 50 |
+
# SOP ("perform action A on units of class C, leave class D
|
| 51 |
+
# untouched"); robotic mixed-asset selective-task execution.
|
| 52 |
+
#
|
| 53 |
+
# Distinct from:
|
| 54 |
+
# • proc-no-attack-passive-only (B0): forbids a tool entirely
|
| 55 |
+
# (attack_unit / attack_move) — the carve-out is on TOOLS, not on
|
| 56 |
+
# a subset of UNITS. Here the agent IS allowed `move_units`; the
|
| 57 |
+
# constraint is the SET OF UNITS that may receive that command.
|
| 58 |
+
# • proc-strict-toolban-under-pressure: also a tool-level ban.
|
| 59 |
+
# • coord-squad-handoff: positive sequenced relay (both squads must
|
| 60 |
+
# move). Here exactly one squad must move and the other must NOT.
|
| 61 |
+
#
|
| 62 |
+
# Tick budget note: ~90 ticks/turn (tick ≈ 93 + 90·(turn-1)). With
|
| 63 |
+
# max_turns=55 the reachable max tick is 4953, so the fail
|
| 64 |
+
# `after_ticks: 4501` bites well within the budget (no draw). The
|
| 65 |
+
# intended jeep convoy travels ~82 cells; jeep speed is ~12 cells/turn
|
| 66 |
+
# so it arrives in ~7-10 turns (~tick 700-1000), well under the
|
| 67 |
+
# deadline.
|
| 68 |
+
#
|
| 69 |
+
# Hard-tier curation: hard defines 2 agent spawn_point groups for the
|
| 70 |
+
# JEEPS (NW vs SW staging — y=8 vs y=32), round-robined by seed; the
|
| 71 |
+
# TANKS are duplicated at the SAME start cell (8,20) across BOTH
|
| 72 |
+
# spawn groups (the must-not-move group is anchored to ONE region so
|
| 73 |
+
# the negative-instruction predicate is well-defined). This pack is
|
| 74 |
+
# in `tests/test_hard_tier.py::UPGRADED`.
|
| 75 |
+
|
| 76 |
+
meta:
|
| 77 |
+
id: proc-instruction-following-edge-case
|
| 78 |
+
title: 'Selective Action — Move the Jeeps, Do NOT Move the Tanks'
|
| 79 |
+
capability: action
|
| 80 |
+
real_world_meaning: >
|
| 81 |
+
Conditional standard-operating-procedure: the order has both a
|
| 82 |
+
positive and a negative clause ("do X to units of class C; do NOT
|
| 83 |
+
do anything to units of class D"). The operator's skill is to
|
| 84 |
+
identify the targeted subset, act on it, and leave the rest
|
| 85 |
+
untouched. Spraying the same action over the whole fleet is the
|
| 86 |
+
classic failure mode — and is what the IFBench / BFCL V4
|
| 87 |
+
negative-instruction families measure.
|
| 88 |
+
robotics_analogue: >
|
| 89 |
+
Mixed-asset fleet with a per-class action allowlist for this
|
| 90 |
+
mission: forklifts may move pallets; the autonomous floor
|
| 91 |
+
scrubber must stay docked. The fleet controller must NOT issue
|
| 92 |
+
a move command to the scrubber even if the scene is messy and
|
| 93 |
+
the scrubber would naively be the "obvious next agent" to act.
|
| 94 |
+
benchmark_anchor:
|
| 95 |
+
- "IFBench conditional / edge-case instruction"
|
| 96 |
+
- "BFCL V4 negative instruction compliance"
|
| 97 |
+
- "conditional SOP compliance"
|
| 98 |
+
- "selective-action discipline"
|
| 99 |
+
author: "openra-bench"
|
| 100 |
+
|
| 101 |
+
base_map: rush-hour-arena
|
| 102 |
+
|
| 103 |
+
# Shared engine fields. The agent's tools are the bare action set —
|
| 104 |
+
# move/attack/observe/stop — and the instruction's teeth are in the
|
| 105 |
+
# WIN PREDICATE (must keep the tanks at start), not a forbidden-tools
|
| 106 |
+
# list. This is deliberate: the IFBench / BFCL V4 anchor is
|
| 107 |
+
# "everything is technically *allowed*, but the brief forbids it" —
|
| 108 |
+
# the bench measures whether the agent self-restrains.
|
| 109 |
+
base:
|
| 110 |
+
agent: {faction: allies, cash: 0}
|
| 111 |
+
enemy: {faction: soviet, cash: 0}
|
| 112 |
+
tools: [move_units, attack_unit, observe, stop]
|
| 113 |
+
planning: true
|
| 114 |
+
termination: {max_ticks: 6000}
|
| 115 |
+
actors: [] # every level supplies its own actor list via overrides.
|
| 116 |
+
|
| 117 |
+
levels:
|
| 118 |
+
# ── EASY ─────────────────────────────────────────────────────────
|
| 119 |
+
# The bare selective-action skill: ONLY the positive clause is
|
| 120 |
+
# tested. 3 jeeps + 3 tanks at the west edge; jeeps must reach
|
| 121 |
+
# (90,20); no negative-clause teeth. A naive "move everything"
|
| 122 |
+
# policy CAN win on easy — easy is the calibration tier (the
|
| 123 |
+
# capability is the difficulty step on medium).
|
| 124 |
+
easy:
|
| 125 |
+
description: >
|
| 126 |
+
Move ALL THREE JEEP units to the region around (90, 20) within
|
| 127 |
+
radius 6, before tick 4500. The TANK units may be ignored
|
| 128 |
+
(easy tier: no negative-instruction teeth). Stalling and
|
| 129 |
+
timeout LOSE.
|
| 130 |
+
overrides:
|
| 131 |
+
actors:
|
| 132 |
+
# 3 jeeps + 3 medium tanks at the west edge.
|
| 133 |
+
- {type: jeep, owner: agent, position: [8, 18]}
|
| 134 |
+
- {type: jeep, owner: agent, position: [8, 19]}
|
| 135 |
+
- {type: jeep, owner: agent, position: [8, 21]}
|
| 136 |
+
- {type: 2tnk, owner: agent, position: [8, 20]}
|
| 137 |
+
- {type: 2tnk, owner: agent, position: [9, 20]}
|
| 138 |
+
- {type: 2tnk, owner: agent, position: [10, 20]}
|
| 139 |
+
win_condition:
|
| 140 |
+
all_of:
|
| 141 |
+
- {units_of_type_in_region_gte:
|
| 142 |
+
{type: jeep, x: 90, y: 20, radius: 6, n: 3}}
|
| 143 |
+
- {within_ticks: 4500}
|
| 144 |
+
fail_condition:
|
| 145 |
+
any_of:
|
| 146 |
+
- {after_ticks: 4501}
|
| 147 |
+
- {not: {own_units_gte: 1}}
|
| 148 |
+
max_turns: 55
|
| 149 |
+
|
| 150 |
+
# ── MEDIUM ───────────────────────────────────────────────────────
|
| 151 |
+
# +1 controlled variable: the NEGATIVE INSTRUCTION teeth come in.
|
| 152 |
+
# The brief now explicitly forbids moving the tanks. The win
|
| 153 |
+
# condition adds a second clause ("≥3 tanks still at start") and
|
| 154 |
+
# the fail condition adds the symmetric `not` clause so a single
|
| 155 |
+
# tank dragged out of the start cluster trips an IMMEDIATE fail
|
| 156 |
+
# (no need to wait for the deadline). This is the IFBench /
|
| 157 |
+
# BFCL V4 anchor cell — measuring whether the model honours the
|
| 158 |
+
# carve-out under no other pressure than the brief itself.
|
| 159 |
+
medium:
|
| 160 |
+
description: >
|
| 161 |
+
Move ALL THREE JEEP units to the region around (90, 20) within
|
| 162 |
+
radius 6, before tick 4500. Do NOT move the TANK units — all
|
| 163 |
+
three TANK (2tnk) units must remain in the start region around
|
| 164 |
+
(8, 20) within radius 6. Issuing ANY move order to a tank
|
| 165 |
+
breaks the tank-at-start clause as soon as a tank leaves the
|
| 166 |
+
cluster and FAILS the mission immediately. Stalling, moving a
|
| 167 |
+
tank, force-loss, and timeout all LOSE.
|
| 168 |
+
overrides:
|
| 169 |
+
actors:
|
| 170 |
+
- {type: jeep, owner: agent, position: [8, 18]}
|
| 171 |
+
- {type: jeep, owner: agent, position: [8, 19]}
|
| 172 |
+
- {type: jeep, owner: agent, position: [8, 21]}
|
| 173 |
+
- {type: 2tnk, owner: agent, position: [8, 20]}
|
| 174 |
+
- {type: 2tnk, owner: agent, position: [9, 20]}
|
| 175 |
+
- {type: 2tnk, owner: agent, position: [10, 20]}
|
| 176 |
+
win_condition:
|
| 177 |
+
all_of:
|
| 178 |
+
- {units_of_type_in_region_gte:
|
| 179 |
+
{type: jeep, x: 90, y: 20, radius: 6, n: 3}}
|
| 180 |
+
- {units_of_type_in_region_gte:
|
| 181 |
+
{type: 2tnk, x: 8, y: 20, radius: 6, n: 3}}
|
| 182 |
+
- {within_ticks: 4500}
|
| 183 |
+
fail_condition:
|
| 184 |
+
any_of:
|
| 185 |
+
- {after_ticks: 4501}
|
| 186 |
+
- {not: {own_units_gte: 1}}
|
| 187 |
+
- {not: {units_of_type_in_region_gte:
|
| 188 |
+
{type: 2tnk, x: 8, y: 20, radius: 6, n: 3}}}
|
| 189 |
+
max_turns: 55
|
| 190 |
+
|
| 191 |
+
# ── HARD ─────────────────────────────────────────────────────────
|
| 192 |
+
# +1 controlled variable vs medium: the JEEP starting corner is
|
| 193 |
+
# seed-randomised (NW staging y=8 vs SW staging y=32 — 2 agent
|
| 194 |
+
# spawn_point groups, round-robined by seed). The TANKS always
|
| 195 |
+
# start clustered at (8..10, 20) — the must-not-move group is
|
| 196 |
+
# ANCHORED to one region so the negative-instruction predicate is
|
| 197 |
+
# well-defined and identical across seeds. The selective-action
|
| 198 |
+
# skill is unchanged; the spawn variation prevents memorised
|
| 199 |
+
# opening generalisation (test_hard_tier.py::UPGRADED contract).
|
| 200 |
+
hard:
|
| 201 |
+
description: >
|
| 202 |
+
Move ALL THREE JEEP units to the region around (90, 20) within
|
| 203 |
+
radius 6, before tick 4500 — your jeeps may start at the
|
| 204 |
+
NORTHWEST (y≈8) OR SOUTHWEST (y≈32) corner depending on the
|
| 205 |
+
seed; route accordingly. Do NOT move the TANK units — all
|
| 206 |
+
three TANK (2tnk) units must remain in the start region around
|
| 207 |
+
(8, 20) within radius 6. Issuing any move order to a tank
|
| 208 |
+
breaks the tank-at-start clause as soon as a tank leaves the
|
| 209 |
+
cluster and FAILS the mission immediately. Stalling, moving a
|
| 210 |
+
tank, force-loss, and timeout all LOSE.
|
| 211 |
+
overrides:
|
| 212 |
+
actors:
|
| 213 |
+
# spawn_point 0 — NW jeep staging (y≈8).
|
| 214 |
+
- {type: jeep, owner: agent, position: [8, 7], spawn_point: 0}
|
| 215 |
+
- {type: jeep, owner: agent, position: [8, 8], spawn_point: 0}
|
| 216 |
+
- {type: jeep, owner: agent, position: [8, 9], spawn_point: 0}
|
| 217 |
+
# spawn_point 1 — SW jeep staging (y≈32).
|
| 218 |
+
- {type: jeep, owner: agent, position: [8, 31], spawn_point: 1}
|
| 219 |
+
- {type: jeep, owner: agent, position: [8, 32], spawn_point: 1}
|
| 220 |
+
- {type: jeep, owner: agent, position: [8, 33], spawn_point: 1}
|
| 221 |
+
# TANKS are anchored at (8..10, 20) in BOTH spawn groups
|
| 222 |
+
# (the must-not-move region is identical across seeds).
|
| 223 |
+
# CLAUDE.md: if ANY agent actor declares spawn_point, every
|
| 224 |
+
# agent actor WITHOUT spawn_point is filtered OUT — so the
|
| 225 |
+
# tanks are duplicated across both groups at identical coords.
|
| 226 |
+
- {type: 2tnk, owner: agent, position: [8, 20], spawn_point: 0}
|
| 227 |
+
- {type: 2tnk, owner: agent, position: [9, 20], spawn_point: 0}
|
| 228 |
+
- {type: 2tnk, owner: agent, position: [10, 20], spawn_point: 0}
|
| 229 |
+
- {type: 2tnk, owner: agent, position: [8, 20], spawn_point: 1}
|
| 230 |
+
- {type: 2tnk, owner: agent, position: [9, 20], spawn_point: 1}
|
| 231 |
+
- {type: 2tnk, owner: agent, position: [10, 20], spawn_point: 1}
|
| 232 |
+
win_condition:
|
| 233 |
+
all_of:
|
| 234 |
+
- {units_of_type_in_region_gte:
|
| 235 |
+
{type: jeep, x: 90, y: 20, radius: 6, n: 3}}
|
| 236 |
+
- {units_of_type_in_region_gte:
|
| 237 |
+
{type: 2tnk, x: 8, y: 20, radius: 6, n: 3}}
|
| 238 |
+
- {within_ticks: 4500}
|
| 239 |
+
fail_condition:
|
| 240 |
+
any_of:
|
| 241 |
+
- {after_ticks: 4501}
|
| 242 |
+
- {not: {own_units_gte: 1}}
|
| 243 |
+
- {not: {units_of_type_in_region_gte:
|
| 244 |
+
{type: 2tnk, x: 8, y: 20, radius: 6, n: 3}}}
|
| 245 |
+
max_turns: 55
|
|
@@ -359,6 +359,18 @@ UPGRADED = [
|
|
| 359 |
# sit on the spawn-matched row so throughput is symmetric per
|
| 360 |
# spawn, but a memorised opening cannot generalise across seeds.
|
| 361 |
"econ-cash-reserve-management",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
]
|
| 363 |
|
| 364 |
# Consciously NOT spawn-varied, with the reason (keeps the curation
|
|
|
|
| 359 |
# sit on the spawn-matched row so throughput is symmetric per
|
| 360 |
# spawn, but a memorised opening cannot generalise across seeds.
|
| 361 |
"econ-cash-reserve-management",
|
| 362 |
+
# Wave-6 IFBench / BFCL V4 negative-instruction seed — conditional
|
| 363 |
+
# SOP / selective-action discipline anchor. The brief carries an
|
| 364 |
+
# explicit carve-out ("move the JEEPS to (90,20); do NOT move the
|
| 365 |
+
# TANKS"); win checks BOTH halves via type-filtered region
|
| 366 |
+
# (`units_of_type_in_region_gte`). Hard tier defines 2 agent
|
| 367 |
+
# spawn_point groups (NW jeep staging y≈8 / SW jeep staging y≈32)
|
| 368 |
+
# round-robined by seed; the TANKS are duplicated at (8..10, 20)
|
| 369 |
+
# across BOTH groups so the must-not-move region is identical and
|
| 370 |
+
# well-defined across seeds. The spawn variation prevents memorised
|
| 371 |
+
# opening generalisation without diluting the selective-action
|
| 372 |
+
# signal.
|
| 373 |
+
"proc-instruction-following-edge-case",
|
| 374 |
]
|
| 375 |
|
| 376 |
# Consciously NOT spawn-varied, with the reason (keeps the curation
|
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""proc-instruction-following-edge-case: IFBench / BFCL V4 negative
|
| 2 |
+
instruction adherence (selective-action discipline).
|
| 3 |
+
|
| 4 |
+
The brief carries an EXPLICIT CONDITIONAL RULE — "move the JEEP units
|
| 5 |
+
to (90,20); do NOT move the TANK units." The bench measures whether
|
| 6 |
+
the agent honours BOTH halves of the instruction. The win predicate
|
| 7 |
+
checks:
|
| 8 |
+
|
| 9 |
+
1. ≥3 jeeps in the goal region around (90,20).
|
| 10 |
+
2. ≥3 tanks STILL in the start region around (8,20).
|
| 11 |
+
|
| 12 |
+
The fail predicate mirrors clause 2 as a `not` clause so a single tank
|
| 13 |
+
dragged out of the start cluster trips an IMMEDIATE loss (no need to
|
| 14 |
+
wait for the deadline) — that is the IFBench instruction-precision
|
| 15 |
+
teeth.
|
| 16 |
+
|
| 17 |
+
The bar (per CLAUDE.md) must hold on every level × every hard seed:
|
| 18 |
+
|
| 19 |
+
- STALL -> LOSS (clock)
|
| 20 |
+
- MOVE-ALL -> WIN on easy (no negative teeth), LOSS on
|
| 21 |
+
medium/hard (touching the tanks fails fast)
|
| 22 |
+
- JEEPS-ONLY -> WIN (the intended capability play)
|
| 23 |
+
"""
|
| 24 |
+
from __future__ import annotations
|
| 25 |
+
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
|
| 28 |
+
import pytest
|
| 29 |
+
|
| 30 |
+
pytest.importorskip("openra_rl_training", reason="Rust env wheel not installed")
|
| 31 |
+
|
| 32 |
+
from openra_bench.scenarios import load_pack
|
| 33 |
+
from openra_bench.scenarios.loader import compile_level
|
| 34 |
+
from openra_bench.scenarios.win_conditions import WinContext, evaluate
|
| 35 |
+
|
| 36 |
+
PACK = (
|
| 37 |
+
Path(__file__).parent.parent
|
| 38 |
+
/ "openra_bench"
|
| 39 |
+
/ "scenarios"
|
| 40 |
+
/ "packs"
|
| 41 |
+
/ "proc-instruction-following-edge-case.yaml"
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _win_clauses(c):
|
| 46 |
+
return dict(c.win_condition.__pydantic_extra__ or {})["all_of"]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _fail_clauses(c):
|
| 50 |
+
return dict(c.fail_condition.__pydantic_extra__ or {})["any_of"]
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# ── A. STRUCTURAL: predicate / clauses / deadlines wired correctly ───
|
| 54 |
+
|
| 55 |
+
def test_easy_has_positive_clause_only():
|
| 56 |
+
"""Easy is the calibration tier — only the positive instruction
|
| 57 |
+
(jeeps reach (90,20)) is in the win predicate. Moving the tanks
|
| 58 |
+
is harmless on easy (the negative teeth come in on medium)."""
|
| 59 |
+
c = compile_level(load_pack(PACK), "easy")
|
| 60 |
+
win = _win_clauses(c)
|
| 61 |
+
type_clauses = [cl for cl in win if "units_of_type_in_region_gte" in cl]
|
| 62 |
+
types = [cl["units_of_type_in_region_gte"]["type"] for cl in type_clauses]
|
| 63 |
+
assert types == ["jeep"], f"easy: expected only the jeep clause, got {types}"
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
@pytest.mark.parametrize("level", ["medium", "hard"])
|
| 67 |
+
def test_medium_and_hard_enforce_both_halves_of_the_instruction(level):
|
| 68 |
+
"""Medium and hard MUST encode BOTH:
|
| 69 |
+
- positive: ≥3 jeeps at goal (90,20) and
|
| 70 |
+
- negative: ≥3 tanks still at start (8,20).
|
| 71 |
+
Anything less and the bench fails to enforce the carve-out."""
|
| 72 |
+
c = compile_level(load_pack(PACK), level)
|
| 73 |
+
win = _win_clauses(c)
|
| 74 |
+
type_clauses = [cl for cl in win if "units_of_type_in_region_gte" in cl]
|
| 75 |
+
by_type = {cl["units_of_type_in_region_gte"]["type"]: cl["units_of_type_in_region_gte"]
|
| 76 |
+
for cl in type_clauses}
|
| 77 |
+
assert "jeep" in by_type, f"{level}: missing positive jeep clause"
|
| 78 |
+
assert by_type["jeep"]["x"] == 90 and by_type["jeep"]["y"] == 20, (
|
| 79 |
+
f"{level}: jeep goal must be (90,20), got "
|
| 80 |
+
f"({by_type['jeep']['x']},{by_type['jeep']['y']})"
|
| 81 |
+
)
|
| 82 |
+
assert by_type["jeep"]["n"] >= 3, f"{level}: jeep clause needs n>=3"
|
| 83 |
+
assert "2tnk" in by_type, f"{level}: missing negative tanks-at-start clause"
|
| 84 |
+
assert by_type["2tnk"]["x"] == 8 and by_type["2tnk"]["y"] == 20, (
|
| 85 |
+
f"{level}: tanks-at-start must be (8,20), got "
|
| 86 |
+
f"({by_type['2tnk']['x']},{by_type['2tnk']['y']})"
|
| 87 |
+
)
|
| 88 |
+
assert by_type["2tnk"]["n"] >= 3, f"{level}: tanks clause needs n>=3"
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
@pytest.mark.parametrize("level", ["medium", "hard"])
|
| 92 |
+
def test_medium_and_hard_have_immediate_violation_fail_clause(level):
|
| 93 |
+
"""The fail predicate must include a `not units_of_type_in_region
|
| 94 |
+
_gte 2tnk` clause so moving a tank trips fail IMMEDIATELY (no
|
| 95 |
+
wait for deadline)."""
|
| 96 |
+
c = compile_level(load_pack(PACK), level)
|
| 97 |
+
fails = _fail_clauses(c)
|
| 98 |
+
not_clauses = [cl["not"] for cl in fails if "not" in cl]
|
| 99 |
+
has_immediate = any(
|
| 100 |
+
"units_of_type_in_region_gte" in nc
|
| 101 |
+
and nc["units_of_type_in_region_gte"]["type"] == "2tnk"
|
| 102 |
+
for nc in not_clauses
|
| 103 |
+
)
|
| 104 |
+
assert has_immediate, (
|
| 105 |
+
f"{level}: fail_condition must include `not units_of_type_in"
|
| 106 |
+
f"_region_gte 2tnk` so moving a tank trips IMMEDIATE fail"
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
|
| 111 |
+
def test_level_has_binding_deadline_and_reachable_loss(level):
|
| 112 |
+
c = compile_level(load_pack(PACK), level)
|
| 113 |
+
win = _win_clauses(c)
|
| 114 |
+
wt = [cl["within_ticks"] for cl in win if "within_ticks" in cl]
|
| 115 |
+
assert wt, f"{level}: missing within_ticks deadline"
|
| 116 |
+
# The deadline must bite within max_turns (engine ~90 ticks/turn).
|
| 117 |
+
assert wt[0] < 93 + 90 * (c.max_turns - 1), (
|
| 118 |
+
f"{level}: within_ticks {wt[0]} unreachable inside max_turns "
|
| 119 |
+
f"{c.max_turns} (would draw on timeout)"
|
| 120 |
+
)
|
| 121 |
+
fail = _fail_clauses(c)
|
| 122 |
+
at = [cl["after_ticks"] for cl in fail if "after_ticks" in cl]
|
| 123 |
+
assert at, f"{level}: missing after_ticks in fail_condition"
|
| 124 |
+
assert at[0] <= 93 + 90 * (c.max_turns - 1), (
|
| 125 |
+
f"{level}: after_ticks {at[0]} unreachable inside max_turns "
|
| 126 |
+
f"{c.max_turns} (would draw on timeout)"
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def test_hard_has_two_spawn_groups_for_jeeps():
|
| 131 |
+
"""Hard tier must have ≥2 jeep spawn_point groups (the
|
| 132 |
+
test_hard_tier.py::UPGRADED contract). Tanks are anchored at
|
| 133 |
+
(8..10, 20) across BOTH groups so the negative-instruction
|
| 134 |
+
predicate is identical and well-defined across seeds."""
|
| 135 |
+
c = compile_level(load_pack(PACK), "hard")
|
| 136 |
+
agent_actors = [a for a in c.scenario.actors if a.owner == "agent"]
|
| 137 |
+
sp = {(a.spawn_point if a.spawn_point is not None else 0) for a in agent_actors}
|
| 138 |
+
assert len(sp) >= 2, (
|
| 139 |
+
f"hard: must define ≥2 agent spawn_point groups; got {sorted(sp)}"
|
| 140 |
+
)
|
| 141 |
+
# Tanks must be duplicated in EVERY spawn group at the same coords
|
| 142 |
+
# so the negative-instruction predicate is identical across seeds.
|
| 143 |
+
tank_cells_per_group = {}
|
| 144 |
+
for a in agent_actors:
|
| 145 |
+
if a.type == "2tnk":
|
| 146 |
+
g = a.spawn_point if a.spawn_point is not None else 0
|
| 147 |
+
tank_cells_per_group.setdefault(g, set()).add(tuple(a.position))
|
| 148 |
+
assert len(tank_cells_per_group) >= 2, (
|
| 149 |
+
"hard: tanks must be present in BOTH spawn groups"
|
| 150 |
+
)
|
| 151 |
+
cell_sets = list(tank_cells_per_group.values())
|
| 152 |
+
assert all(s == cell_sets[0] for s in cell_sets), (
|
| 153 |
+
f"hard: tanks must occupy the SAME cells across spawn groups; "
|
| 154 |
+
f"got {tank_cells_per_group}"
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
# ── B. PREDICATE UNIT TEST: tank-at-start negative clause semantics ──
|
| 159 |
+
|
| 160 |
+
class _Sig:
|
| 161 |
+
def __init__(self):
|
| 162 |
+
self.game_tick = 100
|
| 163 |
+
self.then_progress: dict = {}
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def _ctx(units):
|
| 167 |
+
return WinContext(signals=_Sig(), render_state={"units_summary": units})
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def test_tanks_still_at_start_is_satisfied_by_unmoved_tanks():
|
| 171 |
+
spec = {"units_of_type_in_region_gte":
|
| 172 |
+
{"type": "2tnk", "x": 8, "y": 20, "radius": 6, "n": 3}}
|
| 173 |
+
tanks = [
|
| 174 |
+
{"type": "2tnk", "cell_x": 8, "cell_y": 20, "id": "1"},
|
| 175 |
+
{"type": "2tnk", "cell_x": 9, "cell_y": 20, "id": "2"},
|
| 176 |
+
{"type": "2tnk", "cell_x": 10, "cell_y": 20, "id": "3"},
|
| 177 |
+
]
|
| 178 |
+
assert evaluate(spec, _ctx(tanks)) is True
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def test_tanks_still_at_start_breaks_the_moment_a_tank_leaves_cluster():
|
| 182 |
+
"""The negative-instruction teeth: as soon as <3 tanks remain in
|
| 183 |
+
the start region, the clause becomes False — so the fail-side
|
| 184 |
+
`not` clause fires immediately."""
|
| 185 |
+
spec = {"units_of_type_in_region_gte":
|
| 186 |
+
{"type": "2tnk", "x": 8, "y": 20, "radius": 6, "n": 3}}
|
| 187 |
+
# Two tanks still at start, one dragged east — predicate is False.
|
| 188 |
+
moved = [
|
| 189 |
+
{"type": "2tnk", "cell_x": 8, "cell_y": 20, "id": "1"},
|
| 190 |
+
{"type": "2tnk", "cell_x": 9, "cell_y": 20, "id": "2"},
|
| 191 |
+
{"type": "2tnk", "cell_x": 40, "cell_y": 20, "id": "3"},
|
| 192 |
+
]
|
| 193 |
+
assert evaluate(spec, _ctx(moved)) is False
|
| 194 |
+
# And the corresponding `not` (which IS the fail clause) is True.
|
| 195 |
+
not_spec = {"not": spec}
|
| 196 |
+
assert evaluate(not_spec, _ctx(moved)) is True
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def test_jeeps_do_not_satisfy_the_tanks_at_start_clause():
|
| 200 |
+
"""Type-filter teeth: a jeep parked at the tanks' start does NOT
|
| 201 |
+
count toward the 2tnk-at-start clause (the carve-out is on UNIT
|
| 202 |
+
TYPE, not on cell occupancy)."""
|
| 203 |
+
spec = {"units_of_type_in_region_gte":
|
| 204 |
+
{"type": "2tnk", "x": 8, "y": 20, "radius": 6, "n": 3}}
|
| 205 |
+
jeeps = [
|
| 206 |
+
{"type": "jeep", "cell_x": 8, "cell_y": 20, "id": "j1"},
|
| 207 |
+
{"type": "jeep", "cell_x": 9, "cell_y": 20, "id": "j2"},
|
| 208 |
+
{"type": "jeep", "cell_x": 10, "cell_y": 20, "id": "j3"},
|
| 209 |
+
]
|
| 210 |
+
assert evaluate(spec, _ctx(jeeps)) is False
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
# ── C. SOLVENCY / NO-CHEAT: scripted policies on the live engine ─────
|
| 214 |
+
|
| 215 |
+
pytest.importorskip("openra_train", reason="Rust env wheel not installed")
|
| 216 |
+
from openra_bench.eval_core import run_level # noqa: E402
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def _split(rs):
|
| 220 |
+
us = rs.get("units_summary", []) or []
|
| 221 |
+
jeeps = [str(u["id"]) for u in us if u.get("type") == "jeep"]
|
| 222 |
+
tanks = [str(u["id"]) for u in us if u.get("type") == "2tnk"]
|
| 223 |
+
return jeeps, tanks
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def _stall(rs, C):
|
| 227 |
+
return [C.observe()]
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def _move_all(rs, C):
|
| 231 |
+
"""The classic IFBench failure mode: model treats the brief as
|
| 232 |
+
'move everything east' and ignores the carve-out. On medium and
|
| 233 |
+
hard the negative-instruction teeth fire the moment a tank
|
| 234 |
+
leaves the start cluster."""
|
| 235 |
+
jeeps, tanks = _split(rs)
|
| 236 |
+
cmds = []
|
| 237 |
+
if jeeps:
|
| 238 |
+
cmds.append(C.move_units(jeeps, 90, 20))
|
| 239 |
+
if tanks:
|
| 240 |
+
cmds.append(C.move_units(tanks, 90, 20))
|
| 241 |
+
return cmds or [C.observe()]
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def _jeeps_only(rs, C):
|
| 245 |
+
"""The intended capability play: move ONLY the jeeps to the goal,
|
| 246 |
+
issue NO order to the tanks. The tanks stay at their start and
|
| 247 |
+
both halves of the instruction are honoured."""
|
| 248 |
+
jeeps, _ = _split(rs)
|
| 249 |
+
if not jeeps:
|
| 250 |
+
return [C.observe()]
|
| 251 |
+
return [C.move_units(jeeps, 90, 20)]
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
# Seeds 1..4 = held-out seed contract from CLAUDE.md.
|
| 255 |
+
|
| 256 |
+
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
|
| 257 |
+
@pytest.mark.parametrize("seed", [1, 2, 3, 4])
|
| 258 |
+
def test_stall_loses(level, seed):
|
| 259 |
+
c = compile_level(load_pack(PACK), level)
|
| 260 |
+
res = run_level(c, _stall, seed=seed)
|
| 261 |
+
assert res.outcome == "loss", (
|
| 262 |
+
f"{level} seed{seed}: stall must LOSE on timeout, got {res.outcome}"
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
@pytest.mark.parametrize("level", ["medium", "hard"])
|
| 267 |
+
@pytest.mark.parametrize("seed", [1, 2, 3, 4])
|
| 268 |
+
def test_move_all_loses_when_negative_teeth_present(level, seed):
|
| 269 |
+
"""On medium/hard the brief forbids moving the tanks; spraying
|
| 270 |
+
move-east across both squads must LOSE the moment a tank leaves
|
| 271 |
+
the start cluster."""
|
| 272 |
+
c = compile_level(load_pack(PACK), level)
|
| 273 |
+
res = run_level(c, _move_all, seed=seed)
|
| 274 |
+
assert res.outcome == "loss", (
|
| 275 |
+
f"{level} seed{seed}: move-all (touched the tanks) must LOSE, "
|
| 276 |
+
f"got {res.outcome}"
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
|
| 281 |
+
@pytest.mark.parametrize("seed", [1, 2, 3, 4])
|
| 282 |
+
def test_jeeps_only_wins(level, seed):
|
| 283 |
+
"""The intended selective-action policy must WIN on every level
|
| 284 |
+
× every seed."""
|
| 285 |
+
c = compile_level(load_pack(PACK), level)
|
| 286 |
+
res = run_level(c, _jeeps_only, seed=seed)
|
| 287 |
+
assert res.outcome == "win", (
|
| 288 |
+
f"{level} seed{seed}: jeeps-only (intended play) must WIN, "
|
| 289 |
+
f"got {res.outcome}"
|
| 290 |
+
)
|