Spaces:
Running
feat(scenario): proc-tool-use-multi-distractor — multi-tool clutter resistance (τ²-bench / BFCL V4 anchor)
Browse filesWave-6 multi-distractor tool-relevance discipline pack. The agent is
handed a CLUTTERED 9-tool API surface (move_units, attack_unit,
attack_move, build, place_building, harvest, set_stance,
set_rally_point, observe) where only TWO tools are on the critical
path (move_units + observe). The other seven are MAXIMALLY SALIENT
distractors: a visible off-path weak-enemy garrison invites
attack_unit / attack_move, cash + pre-placed base invites build /
place_building, no-producer / no-harvester tools (set_rally_point,
harvest, set_stance) look generally useful but do nothing for THIS
goal.
Discriminator is OUTCOME-graded (reach + zero loss + within_ticks),
NOT tool-name graded — no forbidden_tools. The off-path enemies
are stance:0 (HoldFire) so they never pursue a clean column; the
cost of invoking a distractor tool is the TURNS BURNED ordering it
(each command also replaces the units' active move order so the
column never makes monotonic progress) while the within_ticks clock
keeps ticking. This is the τ²-bench multi-distractor / BFCL V4
cluttered-API setting: select the relevant subset of a large API
on your own, with no instruction telling you which to skip.
Distinct from sibling strict-toolban-fidelity-under-pressure (B3),
which uses a single forbidden_tools binary signal. This pack uses
NO forbidden_tools — pure outcome-graded relevance discipline.
Tiering:
easy : fewer distractors (1 off-path rifle), generous budget
(within_ticks 2000 / max_turns 25). Intended direct
policy wins in ~9 turns ≈ tick 813.
medium : full distractor palette (3-stack off-path garrison),
tighter budget (within_ticks 1500 / max_turns 22).
hard : + time-pressure central sentry stack at (60,18..22)
denying the direct y=20 lane, + TWO seed-driven
spawn_point groups (NORTH lane y=4..6 / SOUTH lane
y=34..36) round-robined by seed. Listed UPGRADED.
Validation (scripted, no network): intended movement-only policy
WINS every level/seed (1..4); stall (only observe) and use-
multiple-distractors (set_stance + attack_move + build + harvest
+ set_rally_point every turn) both LOSE every level/seed via the
within_ticks deadline.
meta.benchmark_anchor:
- τ²-bench multi-distractor handling
- BFCL V4 cluttered-API relevance
- IFBench distraction resistance
- junior-operator discipline test
Smoke (together / Qwen3.6-Plus, medium seed 1): loss, composite
0.2659 (model failed to ignore distractors — valid discrimination).
|
@@ -0,0 +1,375 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# proc-tool-use-multi-distractor — Wave-6 multi-distractor tool-relevance
|
| 2 |
+
# discipline (τ²-bench multi-distractor / BFCL V4 cluttered-API anchor).
|
| 3 |
+
#
|
| 4 |
+
# CAPABILITY (action): the agent is handed a CLUTTERED tool surface — 9
|
| 5 |
+
# tools, including several that are MAXIMALLY SALIENT (a visible weak
|
| 6 |
+
# enemy that invites attack_unit; cash on hand and a pre-placed base
|
| 7 |
+
# that invite build/place_building; harvesters absent but `harvest` is
|
| 8 |
+
# present; no producers but `set_rally_point` is present; `set_stance`
|
| 9 |
+
# / `attack_move` likewise tempt). Only TWO tools are actually needed
|
| 10 |
+
# to win: `move_units` (drive the column east) and `observe` (read
|
| 11 |
+
# state). The discipline test is whether the model can IGNORE the
|
| 12 |
+
# multiple irrelevant tools and use only the relevant ones.
|
| 13 |
+
#
|
| 14 |
+
# Distinct from sibling `strict-toolban-fidelity-under-pressure` (B3):
|
| 15 |
+
# B3: ONE forbidden tool (attack), procedural BINARY signal —
|
| 16 |
+
# tool_violations_gte:1 instantly fails.
|
| 17 |
+
# This: NO forbidden tools at all. Every tool is "allowed" in the
|
| 18 |
+
# allowlist sense — but invoking the irrelevant ones either
|
| 19 |
+
# (a) interrupts the units' move order so they stop progressing,
|
| 20 |
+
# (b) lures units off-course into a fight that costs casualties,
|
| 21 |
+
# or (c) burns the model's turn budget on no-ops while the
|
| 22 |
+
# within_ticks clock keeps ticking. The bench grades OUTCOME
|
| 23 |
+
# (reach + no losses + on time), not tool-name compliance —
|
| 24 |
+
# which IS the τ²-bench multi-distractor / BFCL V4 cluttered-
|
| 25 |
+
# API setting (the model must select the RELEVANT subset of a
|
| 26 |
+
# large API on its own; no instruction tells it which to skip).
|
| 27 |
+
#
|
| 28 |
+
# Why this cell exists in the Wave-6 taxonomy:
|
| 29 |
+
# - Real-world: τ²-bench's multi-distractor handling (a function-
|
| 30 |
+
# calling agent given a large toolset where most options are
|
| 31 |
+
# irrelevant to the current task); BFCL V4's cluttered-API
|
| 32 |
+
# relevance test (cherry-pick the right tool from a long list).
|
| 33 |
+
# - Robotics: a teleop operator panel exposing dozens of buttons;
|
| 34 |
+
# only two are on the critical path for THIS mission. A junior
|
| 35 |
+
# operator who reaches for every salient button wastes the
|
| 36 |
+
# window or causes collateral.
|
| 37 |
+
#
|
| 38 |
+
# Win : reach_region(110,20,r=6) AND units_lost_lte:0 AND within_ticks
|
| 39 |
+
# Fail : after_ticks (deadline) OR units_lost (any casualty)
|
| 40 |
+
#
|
| 41 |
+
# Failure modes (all real, reachable LOSSES; no draw degeneracy):
|
| 42 |
+
# • Stall (only observe): never moves; deadline LOSS via after_ticks.
|
| 43 |
+
# • Use-multiple-distractors (issue build / set_stance / attack_unit /
|
| 44 |
+
# attack_move / set_rally_point / harvest commands TO the agent's
|
| 45 |
+
# units every turn): each non-move command REPLACES the units'
|
| 46 |
+
# active move order so the column oscillates / stops, AND the
|
| 47 |
+
# attack_unit / attack_move calls lure the column into the off-
|
| 48 |
+
# path weak-enemy garrison where the garrison's return fire
|
| 49 |
+
# ticks a unit down → units_lost LOSS (or deadline LOSS).
|
| 50 |
+
# • Intended-direct (movement-only east to (110,20), ignore every
|
| 51 |
+
# other tool): the column arrives before the deadline with all
|
| 52 |
+
# units intact → WIN.
|
| 53 |
+
#
|
| 54 |
+
# Engine-footgun notes (re-asserted on every level):
|
| 55 |
+
# • ~90 ticks per decision turn ⇒ tick ≤ 93 + 90·(max_turns-1).
|
| 56 |
+
# Every `within_ticks` / `after_ticks` below is strictly under
|
| 57 |
+
# that threshold so the deadline genuinely BITES.
|
| 58 |
+
# • A persistent unarmed enemy `fact` at (118,20) keeps the
|
| 59 |
+
# episode alive past any enemy-elimination engine auto-`done`
|
| 60 |
+
# (CLAUDE.md engine fact #5) — so the win/fail evaluator reads
|
| 61 |
+
# reach_region + within_ticks instead of degenerating to DRAW.
|
| 62 |
+
# • Agent jeeps are stance:0 (HoldFire) so `move_units` en-route
|
| 63 |
+
# auto-fire is suppressed (CLAUDE.md engine fact). The off-path
|
| 64 |
+
# "weak enemy" garrison only fires when the agent CHOOSES to
|
| 65 |
+
# send units into its range (attack_unit / attack_move).
|
| 66 |
+
# • This pack does NOT use `forbidden_tools` — the test is
|
| 67 |
+
# outcome-graded, not tool-name graded. Every tool the model
|
| 68 |
+
# might pick is in `tools:`. The bench's tool_violations counter
|
| 69 |
+
# stays at 0 for every policy; the failure signal is reach /
|
| 70 |
+
# casualty / clock.
|
| 71 |
+
#
|
| 72 |
+
# Hard-tier curation: this pack IS in
|
| 73 |
+
# tests/test_hard_tier.py::UPGRADED — hard adds two agent spawn_point
|
| 74 |
+
# groups (NORTH lane y=10..12 / SOUTH lane y=28..30) so the route
|
| 75 |
+
# geometry differs per seed and a memorised "drive east on y=20"
|
| 76 |
+
# opening cannot generalise. Hard additionally adds time-pressure
|
| 77 |
+
# (a `patrol` enemy bot oscillating across the central corridor) —
|
| 78 |
+
# the patrol is itself a DISTRACTOR (the model is tempted to
|
| 79 |
+
# attack_unit it) AND a real threat to a y=20 column, reinforcing
|
| 80 |
+
# the spawn-vary controlled axis.
|
| 81 |
+
|
| 82 |
+
meta:
|
| 83 |
+
id: proc-tool-use-multi-distractor
|
| 84 |
+
title: 'Cluttered Tool Surface — Ignore the Distractor Tools and Just Move'
|
| 85 |
+
capability: action
|
| 86 |
+
real_world_meaning: >
|
| 87 |
+
The agent receives a CLUTTERED toolset (9 tools) where only TWO
|
| 88 |
+
are on the critical path for the current task (move + observe).
|
| 89 |
+
The other seven are MAXIMALLY SALIENT distractors: a visible
|
| 90 |
+
weak enemy invites `attack_unit`; cash on hand and a pre-placed
|
| 91 |
+
base invite `build` / `place_building`; `harvest` / `set_rally_
|
| 92 |
+
point` / `set_stance` / `attack_move` look generally useful but
|
| 93 |
+
do nothing for THIS goal. The discipline is to recognise which
|
| 94 |
+
subset of a large API is actually relevant and ignore the rest —
|
| 95 |
+
every off-path tool call either interrupts the unit's move
|
| 96 |
+
order, lures units into a costly engagement, or burns the
|
| 97 |
+
model's turn budget while the within_ticks clock keeps ticking.
|
| 98 |
+
robotics_analogue: >
|
| 99 |
+
A teleoperation panel with dozens of buttons (manipulator,
|
| 100 |
+
nav, gripper, vision, comms, logging…) but only two are on
|
| 101 |
+
the critical path for THIS mission. A disciplined operator
|
| 102 |
+
runs the two-button procedure; a junior operator who reaches
|
| 103 |
+
for every salient button wastes the window or causes
|
| 104 |
+
collateral damage. The skill is API/relevance discrimination
|
| 105 |
+
under no explicit "don't touch X" instruction.
|
| 106 |
+
benchmark_anchor:
|
| 107 |
+
- "τ²-bench multi-distractor handling"
|
| 108 |
+
- "BFCL V4 cluttered-API relevance"
|
| 109 |
+
- "IFBench distraction resistance"
|
| 110 |
+
- "junior-operator discipline test"
|
| 111 |
+
author: "openra-bench"
|
| 112 |
+
|
| 113 |
+
base_map: rush-hour-arena
|
| 114 |
+
|
| 115 |
+
# Shared engine fields. The tools allowlist is the FULL 9-tool
|
| 116 |
+
# cluttered API — no forbidden_tools. The agent must self-select
|
| 117 |
+
# the relevant subset (move_units + observe). Generous starting
|
| 118 |
+
# cash and a pre-placed agent base make `build` / `place_building`
|
| 119 |
+
# fully usable distractors (not gated out by tech-tree errors).
|
| 120 |
+
base:
|
| 121 |
+
agent: {faction: allies, cash: 0}
|
| 122 |
+
enemy: {faction: soviet, cash: 0}
|
| 123 |
+
# Full 9-tool API surface. Every tool the spec lists is present in
|
| 124 |
+
# `tools:`; none is in `forbidden_tools`. The discipline test is
|
| 125 |
+
# outcome-graded — irrelevant tool use shows up as missed deadline
|
| 126 |
+
# or unit loss, NOT as a tool_violations counter increment.
|
| 127 |
+
tools:
|
| 128 |
+
- move_units
|
| 129 |
+
- attack_unit
|
| 130 |
+
- attack_move
|
| 131 |
+
- build
|
| 132 |
+
- place_building
|
| 133 |
+
- harvest
|
| 134 |
+
- set_stance
|
| 135 |
+
- set_rally_point
|
| 136 |
+
- observe
|
| 137 |
+
planning: true
|
| 138 |
+
termination: {max_ticks: 6000}
|
| 139 |
+
actors:
|
| 140 |
+
# 4 jeep scouts at the west edge, tight column on y=20 (overridden
|
| 141 |
+
# per level / spawn group below). Jeeps are stance:0 (HoldFire)
|
| 142 |
+
# so the move_units en-route auto-fire engine quirk doesn't
|
| 143 |
+
# accidentally engage the off-path garrison. The garrison only
|
| 144 |
+
# fires if the agent chooses to send units into its range.
|
| 145 |
+
- {type: jeep, owner: agent, position: [8, 19], stance: 0}
|
| 146 |
+
- {type: jeep, owner: agent, position: [8, 20], stance: 0}
|
| 147 |
+
- {type: jeep, owner: agent, position: [8, 21], stance: 0}
|
| 148 |
+
- {type: jeep, owner: agent, position: [9, 20], stance: 0}
|
| 149 |
+
# Pre-placed agent base on the mid latitude (y=20) — set
|
| 150 |
+
# away from both hard-spawn lanes so it doesn't collide with
|
| 151 |
+
# the jeep column on any seed. Makes `build` / `place_building`
|
| 152 |
+
# fully usable distractor tools (no tech-tree failure to
|
| 153 |
+
# short-circuit the distraction). Cash is added per-level so
|
| 154 |
+
# `build` actually queues something.
|
| 155 |
+
- {type: fact, owner: agent, position: [14, 20]}
|
| 156 |
+
- {type: tent, owner: agent, position: [18, 20]}
|
| 157 |
+
- {type: powr, owner: agent, position: [14, 24]}
|
| 158 |
+
# Off-path "weak enemy" garrison (3 e1 rifles) on the NORTH
|
| 159 |
+
# shoulder at (45,8). VISIBLE early via the explored area around
|
| 160 |
+
# the agent base — they show up on the minimap as a salient
|
| 161 |
+
# attackable target. They sit on y=8, well off the y=20 direct
|
| 162 |
+
# corridor — going to attack them costs ~5 turns of route
|
| 163 |
+
# diversion AND the rifles fire back (jeeps that engage at
|
| 164 |
+
# close range eat hits from a 3-stack of e1). This is the
|
| 165 |
+
# `attack_unit` / `attack_move` distractor bait.
|
| 166 |
+
- {type: e1, owner: enemy, position: [45, 8], stance: 0}
|
| 167 |
+
- {type: e1, owner: enemy, position: [45, 9], stance: 0}
|
| 168 |
+
- {type: e1, owner: enemy, position: [46, 8], stance: 0}
|
| 169 |
+
# Persistent unarmed enemy marker at the far east — kept alive
|
| 170 |
+
# past any enemy auto-done so the win/fail evaluator reads
|
| 171 |
+
# reach_region + within_ticks instead of degenerating to DRAW
|
| 172 |
+
# (CLAUDE.md engine fact #5). Unarmed `fact` (cost 0) cannot
|
| 173 |
+
# damage the column.
|
| 174 |
+
- {type: fact, owner: enemy, position: [118, 20]}
|
| 175 |
+
|
| 176 |
+
levels:
|
| 177 |
+
# ── EASY ─────────────────────────────────────────────────────────
|
| 178 |
+
# Bare skill, FEWER distractors. The weak-enemy garrison is reduced
|
| 179 |
+
# to ONE rifleman (still visible, still a salient attack_unit
|
| 180 |
+
# target) and there is NO patrol on the corridor. Tick budget is
|
| 181 |
+
# GENEROUS so the discrimination is purely "did you use the
|
| 182 |
+
# relevant tools or not?" — a model that move_units east straight
|
| 183 |
+
# wins comfortably; a model that wastes turns on distractors runs
|
| 184 |
+
# out the clock.
|
| 185 |
+
# max_turns 25 ⇒ reachable tick 93+90·24 = 2253; within_ticks 2000
|
| 186 |
+
# fits (deadline reachable as LOSS). Direct policy: jeeps cover
|
| 187 |
+
# ~12 cells/turn ⇒ 102 cells in ~9 turns ≈ tick 813, well under
|
| 188 |
+
# 2000. Distractor-spam policy that re-orders units every turn
|
| 189 |
+
# never makes monotonic progress ⇒ deadline LOSS.
|
| 190 |
+
easy:
|
| 191 |
+
description: >
|
| 192 |
+
Get a jeep to the observation point at (110,20) within radius
|
| 193 |
+
6 BEFORE tick 2000, losing none of your 4 jeeps. You have a
|
| 194 |
+
9-tool API at your disposal — most of it is IRRELEVANT to
|
| 195 |
+
this task. The only tools you need are `move_units` and
|
| 196 |
+
`observe`. There is a single weak enemy rifleman dug in off-
|
| 197 |
+
path to the north at ~(45,8); attacking it is a distraction
|
| 198 |
+
that will pull your column off course and may cost units.
|
| 199 |
+
You also have a base and cash — building anything is a
|
| 200 |
+
distraction. Stalling (no orders) and force-loss both LOSE.
|
| 201 |
+
starting_cash: 2000
|
| 202 |
+
overrides:
|
| 203 |
+
actors:
|
| 204 |
+
- {type: jeep, owner: agent, position: [8, 19], stance: 0}
|
| 205 |
+
- {type: jeep, owner: agent, position: [8, 20], stance: 0}
|
| 206 |
+
- {type: jeep, owner: agent, position: [8, 21], stance: 0}
|
| 207 |
+
- {type: jeep, owner: agent, position: [9, 20], stance: 0}
|
| 208 |
+
- {type: fact, owner: agent, position: [14, 20]}
|
| 209 |
+
- {type: tent, owner: agent, position: [18, 20]}
|
| 210 |
+
- {type: powr, owner: agent, position: [14, 24]}
|
| 211 |
+
# EASY: one off-path rifle as the attack_unit distractor.
|
| 212 |
+
# stance:0 (HoldFire) so it sits put and never pursues even
|
| 213 |
+
# if the agent's `move_units` auto-fire (CLAUDE.md engine
|
| 214 |
+
# quirk: agents always auto-fire on the move regardless of
|
| 215 |
+
# the AGENT's stance) tags it; a clean "intended" column
|
| 216 |
+
# that walks past on an off-line lane is never engaged
|
| 217 |
+
# back. The distractor is the model's CHOICE to spend turns
|
| 218 |
+
# ordering attack_unit on it — not a forced kill-zone.
|
| 219 |
+
- {type: e1, owner: enemy, position: [45, 8], stance: 0}
|
| 220 |
+
- {type: fact, owner: enemy, position: [118, 20]}
|
| 221 |
+
win_condition:
|
| 222 |
+
all_of:
|
| 223 |
+
- {reach_region: {x: 110, y: 20, radius: 6}}
|
| 224 |
+
- {units_lost_lte: 0}
|
| 225 |
+
- {within_ticks: 2000}
|
| 226 |
+
fail_condition:
|
| 227 |
+
any_of:
|
| 228 |
+
- {after_ticks: 2001}
|
| 229 |
+
- {not: {units_lost_lte: 0}}
|
| 230 |
+
- {not: {own_units_gte: 1}}
|
| 231 |
+
max_turns: 25
|
| 232 |
+
|
| 233 |
+
# ── MEDIUM ───────────────────────────────────────────────────────
|
| 234 |
+
# +1 controlled variable vs easy: the FULL distractor palette. The
|
| 235 |
+
# off-path enemy garrison is back at its base (3 e1) — stronger
|
| 236 |
+
# attack_unit / attack_move bait. The tick budget tightens (1500)
|
| 237 |
+
# so a model that wastes a handful of turns on distractor commands
|
| 238 |
+
# genuinely misses the deadline. Spawn point stays on y=20.
|
| 239 |
+
# max_turns 22 ⇒ reachable tick 93+90·21 = 1983; within_ticks
|
| 240 |
+
# 1500 fits (deadline reachable as LOSS). Direct policy: ~9 turns
|
| 241 |
+
# ≈ tick 813 (well under 1500); a distractor-spam policy that
|
| 242 |
+
# re-orders units every turn replaces the units' active move with
|
| 243 |
+
# a no-op / off-course command and never makes monotonic east
|
| 244 |
+
# progress ⇒ the column runs out the clock.
|
| 245 |
+
medium:
|
| 246 |
+
description: >
|
| 247 |
+
Get a jeep to the observation point at (110,20) within radius
|
| 248 |
+
6 BEFORE tick 1500, losing none of your 4 jeeps. Your 9-tool
|
| 249 |
+
API includes `attack_unit`, `attack_move`, `build`, `place_
|
| 250 |
+
building`, `harvest`, `set_stance`, `set_rally_point` — none
|
| 251 |
+
of these advance THIS mission. Use `move_units` and `observe`
|
| 252 |
+
only. A 3-rifleman garrison sits off-path at ~(45,8) — a
|
| 253 |
+
tempting target whose engagement costs route diversion and
|
| 254 |
+
likely casualties. The base in the west is a tempting `build`
|
| 255 |
+
site but building won't get a unit to the observation point.
|
| 256 |
+
Stalling and force-loss both LOSE.
|
| 257 |
+
starting_cash: 3000
|
| 258 |
+
overrides:
|
| 259 |
+
actors:
|
| 260 |
+
- {type: jeep, owner: agent, position: [8, 19], stance: 0}
|
| 261 |
+
- {type: jeep, owner: agent, position: [8, 20], stance: 0}
|
| 262 |
+
- {type: jeep, owner: agent, position: [8, 21], stance: 0}
|
| 263 |
+
- {type: jeep, owner: agent, position: [9, 20], stance: 0}
|
| 264 |
+
- {type: fact, owner: agent, position: [14, 20]}
|
| 265 |
+
- {type: tent, owner: agent, position: [18, 20]}
|
| 266 |
+
- {type: powr, owner: agent, position: [14, 24]}
|
| 267 |
+
# MEDIUM: full off-path 3-stack garrison.
|
| 268 |
+
- {type: e1, owner: enemy, position: [45, 8], stance: 0}
|
| 269 |
+
- {type: e1, owner: enemy, position: [45, 9], stance: 0}
|
| 270 |
+
- {type: e1, owner: enemy, position: [46, 8], stance: 0}
|
| 271 |
+
- {type: fact, owner: enemy, position: [118, 20]}
|
| 272 |
+
win_condition:
|
| 273 |
+
all_of:
|
| 274 |
+
- {reach_region: {x: 110, y: 20, radius: 6}}
|
| 275 |
+
- {units_lost_lte: 0}
|
| 276 |
+
- {within_ticks: 1500}
|
| 277 |
+
fail_condition:
|
| 278 |
+
any_of:
|
| 279 |
+
- {after_ticks: 1501}
|
| 280 |
+
- {not: {units_lost_lte: 0}}
|
| 281 |
+
- {not: {own_units_gte: 1}}
|
| 282 |
+
max_turns: 22
|
| 283 |
+
|
| 284 |
+
# ── HARD ─────────────────────────────────────────────────────────
|
| 285 |
+
# +2 controlled variables vs medium:
|
| 286 |
+
# 1. TIME-PRESSURE / DIRECT-CORRIDOR DENIAL: a stationary
|
| 287 |
+
# 3-rifleman sentry stack on the central corridor at
|
| 288 |
+
# (60,18..22) — bracketing y=18..22 with stance:3 fire arcs.
|
| 289 |
+
# Functions as the "time-pressure patrol" (real threat to a
|
| 290 |
+
# undisciplined direct-along-y=20 column AND yet another
|
| 291 |
+
# attack_unit / attack_move distractor target). The model
|
| 292 |
+
# has to navigate around it with move_units only (stay on
|
| 293 |
+
# its spawn lane until the final approach) while still
|
| 294 |
+
# ignoring the off-path north garrison.
|
| 295 |
+
# 2. TWO seed-driven spawn_point groups (NORTH y=4..6 /
|
| 296 |
+
# SOUTH y=34..36) so the start lane differs per seed; a
|
| 297 |
+
# memorised "drive east on y=20" opening cannot generalise.
|
| 298 |
+
# Lanes are sited well clear of the off-path north garrison
|
| 299 |
+
# (y=8..9) and the central corridor (y=18..22) so a "drive
|
| 300 |
+
# east on lane" intended policy never crosses either fire
|
| 301 |
+
# arc.
|
| 302 |
+
# max_turns 20 ⇒ reachable tick 93+90·19 = 1803; within_ticks
|
| 303 |
+
# 1500 fits (deadline reachable as LOSS).
|
| 304 |
+
hard:
|
| 305 |
+
description: >
|
| 306 |
+
Get a jeep to the observation point at (110,20) within radius
|
| 307 |
+
6 BEFORE tick 1500, losing none of your 4 jeeps. Your 9-tool
|
| 308 |
+
API includes seven tools that are IRRELEVANT to this task —
|
| 309 |
+
use only `move_units` and `observe`. There is a 3-rifleman
|
| 310 |
+
off-path garrison at ~(45,8) AND a 3-rifleman sentry stack
|
| 311 |
+
bracketing the central corridor at ~(60,20). Both are
|
| 312 |
+
visible attackable targets that will pull an undisciplined
|
| 313 |
+
column off course or into fire. Your jeeps start either on
|
| 314 |
+
the NORTH lane (y≈5) or the SOUTH lane (y≈35) depending on
|
| 315 |
+
seed — read the minimap and route along your spawn lane,
|
| 316 |
+
dropping to (110,20) only at the final approach. Stalling,
|
| 317 |
+
force-loss, and timeout all LOSE.
|
| 318 |
+
starting_cash: 3000
|
| 319 |
+
overrides:
|
| 320 |
+
actors:
|
| 321 |
+
# NORTH lane spawn group: jeeps along y=4..6 (well clear
|
| 322 |
+
# of the y=8..9 off-path garrison row, so a "drive east on
|
| 323 |
+
# lane" intended policy steers around the garrison without
|
| 324 |
+
# casualties).
|
| 325 |
+
- {type: jeep, owner: agent, position: [8, 4], stance: 0, spawn_point: 0}
|
| 326 |
+
- {type: jeep, owner: agent, position: [8, 5], stance: 0, spawn_point: 0}
|
| 327 |
+
- {type: jeep, owner: agent, position: [8, 6], stance: 0, spawn_point: 0}
|
| 328 |
+
- {type: jeep, owner: agent, position: [9, 5], stance: 0, spawn_point: 0}
|
| 329 |
+
# SOUTH lane spawn group: jeeps along y=34..36 (well clear
|
| 330 |
+
# of the central y=18..22 sentry fire arc).
|
| 331 |
+
- {type: jeep, owner: agent, position: [8, 34], stance: 0, spawn_point: 1}
|
| 332 |
+
- {type: jeep, owner: agent, position: [8, 35], stance: 0, spawn_point: 1}
|
| 333 |
+
- {type: jeep, owner: agent, position: [8, 36], stance: 0, spawn_point: 1}
|
| 334 |
+
- {type: jeep, owner: agent, position: [9, 35], stance: 0, spawn_point: 1}
|
| 335 |
+
# Pre-placed agent base on y=20 — duplicated across BOTH
|
| 336 |
+
# spawn groups at identical coords (per CLAUDE.md engine
|
| 337 |
+
# fact: if ANY agent actor declares spawn_point, every
|
| 338 |
+
# actor without one is FILTERED OUT, so we must duplicate
|
| 339 |
+
# across groups). Sited away from both lanes so it doesn't
|
| 340 |
+
# collide with the jeep column on any seed.
|
| 341 |
+
- {type: fact, owner: agent, position: [14, 20], spawn_point: 0}
|
| 342 |
+
- {type: tent, owner: agent, position: [18, 20], spawn_point: 0}
|
| 343 |
+
- {type: powr, owner: agent, position: [14, 24], spawn_point: 0}
|
| 344 |
+
- {type: fact, owner: agent, position: [14, 20], spawn_point: 1}
|
| 345 |
+
- {type: tent, owner: agent, position: [18, 20], spawn_point: 1}
|
| 346 |
+
- {type: powr, owner: agent, position: [14, 24], spawn_point: 1}
|
| 347 |
+
# Off-path north garrison (3 e1 rifles at ~(45,8)) —
|
| 348 |
+
# visible attack_unit distractor. Enemy actors ALWAYS
|
| 349 |
+
# place regardless of spawn_point (CLAUDE.md engine fact).
|
| 350 |
+
- {type: e1, owner: enemy, position: [45, 8], stance: 0}
|
| 351 |
+
- {type: e1, owner: enemy, position: [45, 9], stance: 0}
|
| 352 |
+
- {type: e1, owner: enemy, position: [46, 8], stance: 0}
|
| 353 |
+
# Central corridor sentry stack (3 e1 stance:0 at x=60,
|
| 354 |
+
# y=18..22) — the lane-denial / attack_unit distractor on
|
| 355 |
+
# the central corridor. stance:0 keeps them stationary
|
| 356 |
+
# (no pursuit) but they're a salient attackable target
|
| 357 |
+
# bracketing y=18..22; a model that uses move_units only
|
| 358 |
+
# along its spawn lane (y=4..6 NORTH or y=34..36 SOUTH)
|
| 359 |
+
# never enters the sentry fire arc.
|
| 360 |
+
- {type: e1, owner: enemy, position: [60, 18], stance: 0}
|
| 361 |
+
- {type: e1, owner: enemy, position: [60, 20], stance: 0}
|
| 362 |
+
- {type: e1, owner: enemy, position: [60, 22], stance: 0}
|
| 363 |
+
# Persistent unarmed marker.
|
| 364 |
+
- {type: fact, owner: enemy, position: [118, 20]}
|
| 365 |
+
win_condition:
|
| 366 |
+
all_of:
|
| 367 |
+
- {reach_region: {x: 110, y: 20, radius: 6}}
|
| 368 |
+
- {units_lost_lte: 0}
|
| 369 |
+
- {within_ticks: 1500}
|
| 370 |
+
fail_condition:
|
| 371 |
+
any_of:
|
| 372 |
+
- {after_ticks: 1501}
|
| 373 |
+
- {not: {units_lost_lte: 0}}
|
| 374 |
+
- {not: {own_units_gte: 1}}
|
| 375 |
+
max_turns: 20
|
|
@@ -492,6 +492,14 @@ UPGRADED = [
|
|
| 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
|
|
|
|
| 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 |
+
# Wave-6 multi-distractor tool-relevance discipline pack (τ²-bench
|
| 496 |
+
# multi-distractor / BFCL V4 cluttered-API anchor). Hard tier
|
| 497 |
+
# defines two agent spawn_point groups (NORTH lane y=4..6 / SOUTH
|
| 498 |
+
# lane y=34..36) round-robined by seed; both lanes route around
|
| 499 |
+
# the same off-path north garrison (y=8..9) and central sentry
|
| 500 |
+
# stack (y=18..22), so a memorised "drive east on y=20" opening
|
| 501 |
+
# cannot generalise across the lane variants.
|
| 502 |
+
"proc-tool-use-multi-distractor",
|
| 503 |
]
|
| 504 |
|
| 505 |
# Consciously NOT spawn-varied, with the reason (keeps the curation
|
|
@@ -0,0 +1,407 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""proc-tool-use-multi-distractor — Wave-6 cluttered-API tool relevance.
|
| 2 |
+
|
| 3 |
+
The bar (per CLAUDE.md): the intended (move-only, ignore-distractors)
|
| 4 |
+
policy WINS on every level and every hard seed (1–4); stall (only
|
| 5 |
+
observe) and use-multiple-distractors (re-orders units every turn
|
| 6 |
+
with non-move tools / wastes turns on build / harvest / set_rally_
|
| 7 |
+
point) policies LOSE on every level and every hard seed. Non-win is
|
| 8 |
+
a real reachable timeout LOSS — never a draw.
|
| 9 |
+
|
| 10 |
+
The pack's discriminator is OUTCOME-graded (reach + zero loss + on
|
| 11 |
+
time), NOT tool-name graded — there are no `forbidden_tools` and
|
| 12 |
+
the bench's `tool_violations` counter stays at 0 for every policy.
|
| 13 |
+
The model is given a 9-tool cluttered API (move_units, attack_unit,
|
| 14 |
+
attack_move, build, place_building, harvest, set_stance, set_rally_
|
| 15 |
+
point, observe) and must self-select the relevant subset
|
| 16 |
+
(move_units + observe). Off-path enemies are stance:0 (HoldFire)
|
| 17 |
+
so they never pursue a "clean" column — the cost of invoking an
|
| 18 |
+
attack tool is the TURNS BURNED ordering it, not a forced kill-
|
| 19 |
+
zone. Same for build/harvest/etc.: they don't fail engine-side,
|
| 20 |
+
they just waste the model's turn budget while the within_ticks
|
| 21 |
+
clock keeps ticking.
|
| 22 |
+
|
| 23 |
+
Validation is scripted (no model / network) — these four policies
|
| 24 |
+
are exhaustive proxies for the cluttered-API failure / success
|
| 25 |
+
modes and exercise the predicate teeth (reach_region, units_lost_
|
| 26 |
+
lte, within_ticks/after_ticks) directly through eval_core.run_level.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
from __future__ import annotations
|
| 30 |
+
|
| 31 |
+
import types
|
| 32 |
+
from pathlib import Path
|
| 33 |
+
|
| 34 |
+
import pytest
|
| 35 |
+
|
| 36 |
+
pytest.importorskip("openra_rl_training", reason="Rust env wheel not installed")
|
| 37 |
+
from openra_bench.scenarios import load_pack
|
| 38 |
+
from openra_bench.scenarios.loader import compile_level
|
| 39 |
+
from openra_bench.scenarios.win_conditions import WinContext, evaluate
|
| 40 |
+
|
| 41 |
+
PACKS = Path(__file__).parent.parent / "openra_bench" / "scenarios" / "packs"
|
| 42 |
+
PACK_PATH = PACKS / "proc-tool-use-multi-distractor.yaml"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# ── unit-level predicate / schema checks (no engine) ──────────────────
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _ctx(units_xy=(), tick=1000, lost=0, violations=0):
|
| 49 |
+
"""Synthesize a WinContext for predicate-level checks. Mirrors the
|
| 50 |
+
field set the live signals object exposes during eval_core."""
|
| 51 |
+
sig = types.SimpleNamespace(
|
| 52 |
+
game_tick=tick,
|
| 53 |
+
units_killed=0,
|
| 54 |
+
units_lost=lost,
|
| 55 |
+
tool_violations=violations,
|
| 56 |
+
own_buildings=[],
|
| 57 |
+
own_building_types=set(),
|
| 58 |
+
enemies_seen_ids=set(),
|
| 59 |
+
enemy_buildings_seen_ids=set(),
|
| 60 |
+
)
|
| 61 |
+
return WinContext(
|
| 62 |
+
signals=sig,
|
| 63 |
+
render_state={
|
| 64 |
+
"units_summary": [
|
| 65 |
+
{"cell_x": x, "cell_y": y} for x, y in units_xy
|
| 66 |
+
]
|
| 67 |
+
},
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_pack_compiles_and_meta_fields_populated():
|
| 72 |
+
pack = load_pack(PACK_PATH)
|
| 73 |
+
assert pack.meta.id == "proc-tool-use-multi-distractor"
|
| 74 |
+
assert pack.meta.capability == "action"
|
| 75 |
+
# benchmark_anchor must name the τ²-bench + BFCL V4 + IFBench +
|
| 76 |
+
# junior-operator anchors from the Wave-6 spec.
|
| 77 |
+
anchors = pack.meta.benchmark_anchor
|
| 78 |
+
assert isinstance(anchors, list) and anchors
|
| 79 |
+
joined = " ".join(anchors).lower()
|
| 80 |
+
assert "τ²-bench" in joined or "tau" in joined or "distractor" in joined
|
| 81 |
+
assert "bfcl" in joined
|
| 82 |
+
assert "ifbench" in joined or "distraction" in joined
|
| 83 |
+
assert "junior" in joined or "operator" in joined
|
| 84 |
+
# meta required-prose fields populated and reflect the cluttered-
|
| 85 |
+
# API framing.
|
| 86 |
+
rwm = pack.meta.real_world_meaning.lower()
|
| 87 |
+
assert "cluttered" in rwm or "distractor" in rwm or "irrelevant" in rwm
|
| 88 |
+
assert "robot" in pack.meta.robotics_analogue.lower() \
|
| 89 |
+
or "operator" in pack.meta.robotics_analogue.lower()
|
| 90 |
+
for lvl in ("easy", "medium", "hard"):
|
| 91 |
+
c = compile_level(pack, lvl)
|
| 92 |
+
assert c.map_supported
|
| 93 |
+
assert c.win_condition is not None
|
| 94 |
+
assert c.fail_condition is not None
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def test_full_9_tool_api_surface_on_every_level():
|
| 98 |
+
"""Cluttered-API contract: every level exposes the full 9-tool
|
| 99 |
+
spec'd API, NO `forbidden_tools` — the test is outcome-graded,
|
| 100 |
+
not tool-name graded. The model must self-select the relevant
|
| 101 |
+
subset (move_units + observe) from the 9 available tools."""
|
| 102 |
+
pack = load_pack(PACK_PATH)
|
| 103 |
+
expected = {
|
| 104 |
+
"move_units", "attack_unit", "attack_move", "build",
|
| 105 |
+
"place_building", "harvest", "set_stance", "set_rally_point",
|
| 106 |
+
"observe",
|
| 107 |
+
}
|
| 108 |
+
for lvl in ("easy", "medium", "hard"):
|
| 109 |
+
c = compile_level(pack, lvl)
|
| 110 |
+
tools = set(c.scenario.tools or [])
|
| 111 |
+
assert tools == expected, (
|
| 112 |
+
f"{lvl}: tools allowlist must be the full 9-tool API spec; "
|
| 113 |
+
f"got {sorted(tools)}, missing {sorted(expected - tools)}, "
|
| 114 |
+
f"extra {sorted(tools - expected)}"
|
| 115 |
+
)
|
| 116 |
+
# No forbidden_tools — the discriminator is OUTCOME, not name.
|
| 117 |
+
assert c.forbidden_tools == [], (
|
| 118 |
+
f"{lvl}: this pack must NOT use forbidden_tools (the "
|
| 119 |
+
f"discriminator is reach + zero-loss + within_ticks, NOT "
|
| 120 |
+
f"a tool_violations counter); got {c.forbidden_tools}"
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def test_win_predicate_requires_reach_zero_loss_within_ticks():
|
| 125 |
+
"""The win predicate must enforce the advertised capability: a
|
| 126 |
+
jeep reaches the observation point, zero units lost, before the
|
| 127 |
+
tick deadline. Each clause is load-bearing."""
|
| 128 |
+
pack = load_pack(PACK_PATH)
|
| 129 |
+
for lvl in ("easy", "medium", "hard"):
|
| 130 |
+
c = compile_level(pack, lvl)
|
| 131 |
+
in_region = [(110, 20), (109, 19), (110, 21)]
|
| 132 |
+
spawn_only = [(8, 5), (8, 20), (8, 35)]
|
| 133 |
+
# Intended win state — clean arrival in time.
|
| 134 |
+
assert evaluate(c.win_condition, _ctx(in_region, tick=900, lost=0)), \
|
| 135 |
+
f"{lvl}: clean arrival within deadline must WIN"
|
| 136 |
+
# Sitting at spawn — never wins regardless of tick / loss.
|
| 137 |
+
assert not evaluate(c.win_condition, _ctx(spawn_only, tick=900, lost=0)), \
|
| 138 |
+
f"{lvl}: sitting at spawn must NOT win"
|
| 139 |
+
# ONE loss — wins predicate has units_lost_lte:0, so any
|
| 140 |
+
# casualty invalidates the win clause.
|
| 141 |
+
assert not evaluate(c.win_condition, _ctx(in_region, tick=900, lost=1)), \
|
| 142 |
+
f"{lvl}: even a single casualty must invalidate the win"
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def test_fail_predicate_reachable_loss_on_every_level():
|
| 146 |
+
"""Fail must be a real reachable LOSS, not a draw. The after_ticks
|
| 147 |
+
fail clause must fit inside max_turns (~90 ticks/turn ⇒
|
| 148 |
+
93 + 90·(max_turns-1)). Casualty (units_lost_lte:0 violated) and
|
| 149 |
+
full force-wipe (not own_units_gte:1) likewise fail."""
|
| 150 |
+
pack = load_pack(PACK_PATH)
|
| 151 |
+
in_region = [(110, 20)]
|
| 152 |
+
expected_deadline = {"easy": 2001, "medium": 1501, "hard": 1501}
|
| 153 |
+
for lvl in ("easy", "medium", "hard"):
|
| 154 |
+
c = compile_level(pack, lvl)
|
| 155 |
+
deadline = expected_deadline[lvl]
|
| 156 |
+
# after_ticks fits inside max_turns (no draw degeneracy)
|
| 157 |
+
assert deadline <= 93 + 90 * (c.max_turns - 1), (
|
| 158 |
+
f"{lvl}: after_ticks {deadline} must be reachable within "
|
| 159 |
+
f"max_turns {c.max_turns} (else timeout draws instead of "
|
| 160 |
+
f"losing). Max reachable tick "
|
| 161 |
+
f"{93 + 90 * (c.max_turns - 1)}."
|
| 162 |
+
)
|
| 163 |
+
# Past deadline → fail
|
| 164 |
+
assert evaluate(c.fail_condition, _ctx(in_region, tick=deadline + 1, lost=0)), \
|
| 165 |
+
f"{lvl}: past after_ticks must fail"
|
| 166 |
+
# Casualty → fail (units_lost_lte:0 is the cap on every tier)
|
| 167 |
+
assert evaluate(c.fail_condition, _ctx(in_region, tick=900, lost=1)), \
|
| 168 |
+
f"{lvl}: any unit loss must fail (cap is 0)"
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def test_intended_predicate_wins_only_when_actually_at_observation_point():
|
| 172 |
+
"""Sanity: the reach_region(110,20,r=6) clause must require an
|
| 173 |
+
actual arrival; the spawn area and the SW base must not satisfy
|
| 174 |
+
it. Generous radius admits any reasonable approach footprint."""
|
| 175 |
+
pack = load_pack(PACK_PATH)
|
| 176 |
+
for lvl in ("easy", "medium", "hard"):
|
| 177 |
+
c = compile_level(pack, lvl)
|
| 178 |
+
# SW base coords — not in the observation region.
|
| 179 |
+
sw_base = [(14, 20), (18, 20)]
|
| 180 |
+
assert not evaluate(c.win_condition, _ctx(sw_base, tick=900, lost=0)), \
|
| 181 |
+
f"{lvl}: SW base must NOT count as arrival at (110,20)"
|
| 182 |
+
# Adjacent (within radius 6) — wins.
|
| 183 |
+
near = [(108, 22)]
|
| 184 |
+
assert evaluate(c.win_condition, _ctx(near, tick=900, lost=0)), \
|
| 185 |
+
f"{lvl}: arrival within radius 6 must win"
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def test_hard_has_two_spawn_groups_under_upgraded():
|
| 189 |
+
"""The hard tier is in tests/test_hard_tier.py::UPGRADED, so it
|
| 190 |
+
MUST declare ≥2 distinct agent spawn_point groups (north lane /
|
| 191 |
+
south lane round-robined by seed)."""
|
| 192 |
+
pack = load_pack(PACK_PATH)
|
| 193 |
+
c = compile_level(pack, "hard")
|
| 194 |
+
groups = {
|
| 195 |
+
(a.spawn_point if a.spawn_point is not None else 0)
|
| 196 |
+
for a in c.scenario.actors
|
| 197 |
+
if a.owner == "agent"
|
| 198 |
+
}
|
| 199 |
+
assert groups == {0, 1}, (
|
| 200 |
+
f"hard must define exactly two spawn_point groups {{0, 1}} "
|
| 201 |
+
f"(north lane / south lane); got {sorted(groups)}"
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def test_pack_is_in_upgraded_set():
|
| 206 |
+
"""Round-trip check that the hard-tier curation matrix records
|
| 207 |
+
this pack as UPGRADED (spawn-varied), not NOT_APPLICABLE."""
|
| 208 |
+
from tests.test_hard_tier import NOT_APPLICABLE, UPGRADED
|
| 209 |
+
|
| 210 |
+
assert "proc-tool-use-multi-distractor" in UPGRADED, (
|
| 211 |
+
"pack must be registered in UPGRADED (≥2 spawn groups on hard)"
|
| 212 |
+
)
|
| 213 |
+
assert "proc-tool-use-multi-distractor" not in NOT_APPLICABLE, (
|
| 214 |
+
"pack is spawn-varied; should not appear in NOT_APPLICABLE"
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
# ── engine-driven scripted policies ──────────────────────────────────
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def _stall_policy(rs, Command):
|
| 222 |
+
"""Idle: only observe. Never moves; misses the within_ticks
|
| 223 |
+
deadline ⇒ after_ticks LOSS."""
|
| 224 |
+
return [Command.observe()]
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def _intended_direct_policy(rs, Command):
|
| 228 |
+
"""Movement-only beeline east to the observation point. Issues
|
| 229 |
+
`move_units` once per unit per turn — ignores every other tool
|
| 230 |
+
in the 9-tool API. On easy/medium the spawn is on y=20 so a
|
| 231 |
+
straight east drive arrives cleanly."""
|
| 232 |
+
units = [
|
| 233 |
+
u for u in (rs.get("units_summary") or [])
|
| 234 |
+
if not u.get("is_building", False)
|
| 235 |
+
]
|
| 236 |
+
if not units:
|
| 237 |
+
return [Command.observe()]
|
| 238 |
+
return [
|
| 239 |
+
Command.move_units([str(u["id"])], target_x=110, target_y=20)
|
| 240 |
+
for u in units
|
| 241 |
+
]
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def _intended_lane_policy(rs, Command):
|
| 245 |
+
"""Hard-tier version of the intended policy: stay on the spawn
|
| 246 |
+
lane (y≈5 north or y≈35 south, picked from current unit y) to
|
| 247 |
+
avoid the central sentry stack at (60,18..22) and the off-path
|
| 248 |
+
north garrison at (45,8). Drops south to (110,20) only at the
|
| 249 |
+
final approach (ux ≥ 100). Uses move_units only."""
|
| 250 |
+
units = [
|
| 251 |
+
u for u in (rs.get("units_summary") or [])
|
| 252 |
+
if not u.get("is_building", False)
|
| 253 |
+
]
|
| 254 |
+
if not units:
|
| 255 |
+
return [Command.observe()]
|
| 256 |
+
cmds = []
|
| 257 |
+
for u in units:
|
| 258 |
+
ux, uy = u["cell_x"], u["cell_y"]
|
| 259 |
+
if ux < 100:
|
| 260 |
+
ly = 5 if uy < 20 else 35
|
| 261 |
+
cmds.append(
|
| 262 |
+
Command.move_units([str(u["id"])], target_x=105, target_y=ly)
|
| 263 |
+
)
|
| 264 |
+
else:
|
| 265 |
+
cmds.append(
|
| 266 |
+
Command.move_units([str(u["id"])], target_x=110, target_y=20)
|
| 267 |
+
)
|
| 268 |
+
return cmds
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def _use_multiple_distractors_policy(rs, Command):
|
| 272 |
+
"""The "wastes-turns-on-distractor-tools" failure mode: every
|
| 273 |
+
turn issue MULTIPLE non-move tool calls (set_stance, attack_move
|
| 274 |
+
on the units, build, harvest, set_rally_point) — each of which
|
| 275 |
+
REPLACES the units' active move order so the column never makes
|
| 276 |
+
monotonic east progress. The within_ticks deadline then bites.
|
| 277 |
+
|
| 278 |
+
This is the τ²-bench / BFCL V4 cluttered-API failure mode: a
|
| 279 |
+
model that "uses every tool the API exposes" because it sees a
|
| 280 |
+
salient distractor for each, instead of self-selecting the
|
| 281 |
+
relevant subset (move_units + observe)."""
|
| 282 |
+
units = [
|
| 283 |
+
u for u in (rs.get("units_summary") or [])
|
| 284 |
+
if not u.get("is_building", False)
|
| 285 |
+
]
|
| 286 |
+
if not units:
|
| 287 |
+
return [Command.observe()]
|
| 288 |
+
cmds = []
|
| 289 |
+
# Set stance (re-issues an order to the units — replaces any
|
| 290 |
+
# prior move).
|
| 291 |
+
cmds.append(
|
| 292 |
+
Command.set_stance([str(u["id"]) for u in units], 3)
|
| 293 |
+
)
|
| 294 |
+
# attack_move to the off-path north garrison cell — pulls the
|
| 295 |
+
# column off-course.
|
| 296 |
+
for u in units:
|
| 297 |
+
cmds.append(
|
| 298 |
+
Command.attack_move([str(u["id"])], target_x=45, target_y=8)
|
| 299 |
+
)
|
| 300 |
+
# Build something irrelevant (cash is on hand, pbox is buildable).
|
| 301 |
+
cmds.append(Command.build("pbox"))
|
| 302 |
+
# No-op-tool calls: harvest with no harvester, set_rally_point
|
| 303 |
+
# with no producer.
|
| 304 |
+
cmds.append(
|
| 305 |
+
Command.harvest([str(units[0]["id"])], target_x=20, target_y=24)
|
| 306 |
+
)
|
| 307 |
+
cmds.append(
|
| 308 |
+
Command.set_rally_point([str(units[0]["id"])], target_x=15, target_y=15)
|
| 309 |
+
)
|
| 310 |
+
return cmds
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
|
| 314 |
+
def test_intended_policy_wins(level):
|
| 315 |
+
"""The intended movement-only policy WINS on every level. On
|
| 316 |
+
easy/medium the column starts on y=20 and a direct beeline
|
| 317 |
+
suffices; on hard the column starts on y=5 or y=35 (round-robin
|
| 318 |
+
by seed) and the lane-respecting variant routes around the
|
| 319 |
+
central sentry stack and the off-path garrison."""
|
| 320 |
+
pytest.importorskip("openra_train")
|
| 321 |
+
from openra_bench.eval_core import run_level
|
| 322 |
+
|
| 323 |
+
c = compile_level(load_pack(PACK_PATH), level)
|
| 324 |
+
seeds = (1, 2, 3, 4)
|
| 325 |
+
pol = _intended_lane_policy if level == "hard" else _intended_direct_policy
|
| 326 |
+
for s in seeds:
|
| 327 |
+
res = run_level(c, pol, seed=s)
|
| 328 |
+
assert res.outcome == "win", (
|
| 329 |
+
f"{level} seed={s}: intended movement-only policy should "
|
| 330 |
+
f"WIN, got {res.outcome} after {res.turns} turns "
|
| 331 |
+
f"(lost={res.signals.units_lost}, "
|
| 332 |
+
f"tick={res.signals.game_tick}, "
|
| 333 |
+
f"tools_called={dict(res.signals.tools_called)})"
|
| 334 |
+
)
|
| 335 |
+
# Sanity: only move_units is invoked (the intended subset).
|
| 336 |
+
called = set(res.signals.tools_called.keys())
|
| 337 |
+
assert called <= {"move_units"}, (
|
| 338 |
+
f"{level} seed={s}: intended policy must call only "
|
| 339 |
+
f"move_units; got {dict(res.signals.tools_called)}"
|
| 340 |
+
)
|
| 341 |
+
# No tool_violations (the pack has no forbidden_tools, so
|
| 342 |
+
# this stays at 0 regardless — but check it as a contract).
|
| 343 |
+
assert res.signals.tool_violations == 0
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
|
| 347 |
+
def test_stall_policy_loses_on_deadline(level):
|
| 348 |
+
"""Stall (only observe) LOSES on every level/seed because the
|
| 349 |
+
within_ticks deadline bites — the column never moves so the
|
| 350 |
+
fail_condition's after_ticks clause fires."""
|
| 351 |
+
pytest.importorskip("openra_train")
|
| 352 |
+
from openra_bench.eval_core import run_level
|
| 353 |
+
|
| 354 |
+
c = compile_level(load_pack(PACK_PATH), level)
|
| 355 |
+
seeds = (1, 2, 3, 4)
|
| 356 |
+
for s in seeds:
|
| 357 |
+
res = run_level(c, _stall_policy, seed=s)
|
| 358 |
+
assert res.outcome == "loss", (
|
| 359 |
+
f"{level} seed={s}: stall must LOSE on the deadline, got "
|
| 360 |
+
f"{res.outcome} after {res.turns} turns "
|
| 361 |
+
f"(tick={res.signals.game_tick})"
|
| 362 |
+
)
|
| 363 |
+
# Stall causes the LOSS via the deadline, not via casualty.
|
| 364 |
+
assert res.signals.units_lost == 0, (
|
| 365 |
+
f"{level} seed={s}: stall must not lose any units "
|
| 366 |
+
f"(no casualty trigger); got {res.signals.units_lost}"
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
@pytest.mark.parametrize("level", ["easy", "medium", "hard"])
|
| 371 |
+
def test_use_multiple_distractors_policy_loses(level):
|
| 372 |
+
"""The "use-multiple-distractors-wastes-turns" failure mode LOSES
|
| 373 |
+
on every level/seed: the distractor tool calls each replace the
|
| 374 |
+
units' active move order so the column never makes monotonic
|
| 375 |
+
east progress, and the within_ticks deadline bites.
|
| 376 |
+
|
| 377 |
+
This IS the τ²-bench / BFCL V4 cluttered-API failure mode (a
|
| 378 |
+
model that fires every salient tool instead of picking only the
|
| 379 |
+
relevant subset)."""
|
| 380 |
+
pytest.importorskip("openra_train")
|
| 381 |
+
from openra_bench.eval_core import run_level
|
| 382 |
+
|
| 383 |
+
c = compile_level(load_pack(PACK_PATH), level)
|
| 384 |
+
seeds = (1, 2, 3, 4)
|
| 385 |
+
for s in seeds:
|
| 386 |
+
res = run_level(c, _use_multiple_distractors_policy, seed=s)
|
| 387 |
+
assert res.outcome == "loss", (
|
| 388 |
+
f"{level} seed={s}: use-multiple-distractors must LOSE, "
|
| 389 |
+
f"got {res.outcome} after {res.turns} turns "
|
| 390 |
+
f"(lost={res.signals.units_lost}, "
|
| 391 |
+
f"tick={res.signals.game_tick}, "
|
| 392 |
+
f"tools_called={dict(res.signals.tools_called)})"
|
| 393 |
+
)
|
| 394 |
+
# The distractor policy actively uses multiple non-move tools
|
| 395 |
+
# (the whole point of this failure mode). Sanity-check that
|
| 396 |
+
# it really did call ≥3 distinct distractor tools — otherwise
|
| 397 |
+
# the test isn't exercising the cluttered-API discriminator.
|
| 398 |
+
called = set(res.signals.tools_called.keys())
|
| 399 |
+
distractor_subset = called & {
|
| 400 |
+
"set_stance", "attack_move", "build",
|
| 401 |
+
"harvest", "set_rally_point",
|
| 402 |
+
}
|
| 403 |
+
assert len(distractor_subset) >= 3, (
|
| 404 |
+
f"{level} seed={s}: distractor policy must invoke ≥3 "
|
| 405 |
+
f"distinct distractor tools (the test of the cluttered-"
|
| 406 |
+
f"API failure mode); got distractors {sorted(distractor_subset)}"
|
| 407 |
+
)
|