Spaces:
Running
Running
feat(scenario): combat-target-priority-highvalue — focus high-threat units first (SC2 focus-fire anchor)
Browse files
openra_bench/scenarios/packs/combat-target-priority-highvalue.yaml
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# combat-target-priority-highvalue — ACTION capability: threat-weighted
|
| 2 |
+
# target prioritization. A 4-tank squad faces a MIXED enemy cluster:
|
| 3 |
+
# cheap rifle chaff (e1) plus THREE high-threat anti-armour rocket
|
| 4 |
+
# soldiers (e3). The squad must FOCUS THE THREE ROCKET SOLDIERS FIRST
|
| 5 |
+
# — they out-damage the chaff and whittle the squad below the survival
|
| 6 |
+
# cap if left alive. Killing the chaff first leaves the rockets firing
|
| 7 |
+
# through the whole engagement and the squad LOSES.
|
| 8 |
+
#
|
| 9 |
+
# Sibling of combat-focus-fire-priority (same SC2 focus-fire idiom);
|
| 10 |
+
# THIS pack encodes the capability via a SURVIVAL floor
|
| 11 |
+
# (`own_units_gte`) plus an explicit non-combat building anchor
|
| 12 |
+
# (`building_count_gte:fact:1`) — the "preserve the squad" framing
|
| 13 |
+
# rather than the "attrition cap" (`units_lost_lte`) framing. Same
|
| 14 |
+
# anchor family, different predicate idiom.
|
| 15 |
+
#
|
| 16 |
+
# Discrimination (the four script-policy bar — verified deterministic
|
| 17 |
+
# across seeds 1-4 on every level):
|
| 18 |
+
# • stall (only observe): the tanks idle at the staging edge and
|
| 19 |
+
# never close; the kill bar (units_killed_gte:K) is never met and
|
| 20 |
+
# the after_ticks fail clause fires → LOSS.
|
| 21 |
+
# • brute attack_move (attack-move onto the cluster centroid): the
|
| 22 |
+
# engine auto-targets the NEAREST hostile — the e1 chaff screen is
|
| 23 |
+
# posted three columns closer than the rocket soldiers, so the
|
| 24 |
+
# squad chews the chaff first while the three e3s fire anti-armour
|
| 25 |
+
# rockets through the whole engagement; all four tanks are lost
|
| 26 |
+
# → busts `own_units_gte` → LOSS.
|
| 27 |
+
# • kill-chaff-first (attack_unit the e1 chaff one at a time): the
|
| 28 |
+
# three e3 rockets keep landing through the entire chaff mop-up;
|
| 29 |
+
# ~3 tanks die before the rockets are silenced → busts the
|
| 30 |
+
# survival floor → LOSS.
|
| 31 |
+
# • intended focus-threats-first: attack_unit ALL FOUR tanks onto
|
| 32 |
+
# ONE e3 at a time — 4 tank cannons concentrate ~400 HP of armour
|
| 33 |
+
# against ~50 HP of infantry, so each e3 dies in 1-2 decision
|
| 34 |
+
# turns before it can land more than ~1 rocket; with all three
|
| 35 |
+
# rockets silenced inside the first ~3 turns the chaff cannot
|
| 36 |
+
# scratch the squad → mop up the e1s → kill bar met, the squad
|
| 37 |
+
# stays above the survival floor → WIN.
|
| 38 |
+
#
|
| 39 |
+
# Why the spec works (verified against the engine combat sheet):
|
| 40 |
+
# • e3 = anti-tank rocket soldier (Dragon launcher, range ~5, high
|
| 41 |
+
# anti-armour DPS). THREE e3 firing unopposed wipe a 4-tank squad
|
| 42 |
+
# in ~540 ticks (~6 decision turns) — measured. Left firing
|
| 43 |
+
# through a chaff mop-up they reliably kill ~3 tanks.
|
| 44 |
+
# • e1 = basic rifle infantry. Per-shot damage vs 2tnk armour is
|
| 45 |
+
# low; the e1 chaff alone cannot drop the squad below the floor
|
| 46 |
+
# inside the budget once the three e3s are dead.
|
| 47 |
+
# • 4× 2tnk concentrated on a single e3 kill it in 1-2 decision
|
| 48 |
+
# turns; the same fire SPREAD across the cluster kills the first
|
| 49 |
+
# e3 in 4+ turns, granting all three rockets free anti-tank
|
| 50 |
+
# shots. That focus-vs-spread delta is the discrimination.
|
| 51 |
+
# • Measured (deterministic): chaff-first loses 3 tanks, focus-
|
| 52 |
+
# threats-first loses 0, brute loses all 4, on every level.
|
| 53 |
+
#
|
| 54 |
+
# Engine guardrails:
|
| 55 |
+
# • Map: rush-hour-arena (128 × 40, playable x in [2..126], y in
|
| 56 |
+
# [2..38]). Tanks stage at x=48; the enemy cluster holds at
|
| 57 |
+
# x=66-70 (stance:2 Defend — a stationary wall, auto-fires in
|
| 58 |
+
# range but never advances, so the screen+rocket geometry is
|
| 59 |
+
# stable and the rockets fire the moment a tank is in range).
|
| 60 |
+
# The e1 screen is a COMPACT contiguous column so the whole
|
| 61 |
+
# cluster stays inside the tank squad's vision (no straggler
|
| 62 |
+
# slips into the fog and starves the kill count).
|
| 63 |
+
# • Agent `fact` at the staging edge — satisfies the win clause
|
| 64 |
+
# `building_count_gte:fact:1` (a non-combat anchor: the squad
|
| 65 |
+
# must still own its base while it focus-fires) AND, being an
|
| 66 |
+
# agent actor, keeps the agent side alive for the evaluator.
|
| 67 |
+
# • Far-east enemy `fact` at (120,20) prevents engine auto-done
|
| 68 |
+
# when the last cluster unit falls — the kill bar is satisfied on
|
| 69 |
+
# units, not buildings, so the marker keeps the episode running
|
| 70 |
+
# until the win/fail evaluator sees the terminal frame (no DRAW).
|
| 71 |
+
# • after_ticks/within_ticks reachable within max_turns
|
| 72 |
+
# (tick ≤ 93 + 90·(max_turns-1)); within_ticks 2700 < 2703 at
|
| 73 |
+
# max_turns 30; a staller hits after_ticks 2701 and LOSES.
|
| 74 |
+
|
| 75 |
+
meta:
|
| 76 |
+
id: combat-target-priority-highvalue
|
| 77 |
+
title: 'Target Priority — Kill the High-Threat Units FIRST'
|
| 78 |
+
capability: action
|
| 79 |
+
real_world_meaning: >
|
| 80 |
+
Four medium tanks face a mixed enemy cluster at close range:
|
| 81 |
+
a screen of cheap rifle infantry (e1 chaff) backed by THREE
|
| 82 |
+
high-threat anti-armour rocket soldiers (e3). The decision is
|
| 83 |
+
threat-weighted target prioritization: concentrate ALL FOUR
|
| 84 |
+
tanks' fire on the rocket soldiers FIRST (each dies in 1-2
|
| 85 |
+
decision turns to 4-vs-1 fire, before it can land more than a
|
| 86 |
+
couple of anti-tank rockets), THEN mop up the chaff. Engaging
|
| 87 |
+
the chaff first — via attack_move auto-targeting the nearer
|
| 88 |
+
rifle screen, or by explicitly attacking the cheap infantry —
|
| 89 |
+
leaves the three rocket soldiers firing through the entire
|
| 90 |
+
mop-up and they whittle the squad below the survival floor.
|
| 91 |
+
A brute attack-move play and a kill-chaff-first play both LOSE;
|
| 92 |
+
the threat-first focus play WINS.
|
| 93 |
+
robotics_analogue: >
|
| 94 |
+
Military target prioritization under fire — when engaging a
|
| 95 |
+
mixed force, the high-threat asset (anti-armour weapon, AA
|
| 96 |
+
battery, command vehicle) is neutralised FIRST, even when it is
|
| 97 |
+
not the nearest target, because it out-damages the cheap escort.
|
| 98 |
+
Killing the low-threat chaff first lets the priority threat keep
|
| 99 |
+
firing and attrits the friendly force. Threat-weighted
|
| 100 |
+
engagement — not nearest-first — preserves the squad.
|
| 101 |
+
benchmark_anchor:
|
| 102 |
+
- "SC2 focus-fire target priority"
|
| 103 |
+
- "threat-weighted engagement"
|
| 104 |
+
- "military target prioritization"
|
| 105 |
+
author: openra-bench
|
| 106 |
+
|
| 107 |
+
base_map: rush-hour-arena
|
| 108 |
+
starting_cash: 0
|
| 109 |
+
|
| 110 |
+
base:
|
| 111 |
+
agent: {faction: allies, cash: 0}
|
| 112 |
+
enemy: {faction: soviet, cash: 0, bot_type: ''}
|
| 113 |
+
tools: [move_units, attack_unit, attack_move, stop]
|
| 114 |
+
planning: true
|
| 115 |
+
# React the instant contact happens or a tank is lost — combat
|
| 116 |
+
# micro is reactive; without these interrupts the focus policy only
|
| 117 |
+
# re-targets on the next turn boundary and the e3s get free shots.
|
| 118 |
+
interrupts:
|
| 119 |
+
enemy_unit_spotted: true
|
| 120 |
+
own_unit_destroyed: true
|
| 121 |
+
termination: {max_ticks: 6000}
|
| 122 |
+
actors: []
|
| 123 |
+
|
| 124 |
+
levels:
|
| 125 |
+
# ── EASY ─────────────────────────────────────────────────────────
|
| 126 |
+
# Bare threat-priority skill: 4 tanks vs a cluster of 9 e1 chaff +
|
| 127 |
+
# 3 e3 high-threat. FORGIVING survival floor — the squad may lose
|
| 128 |
+
# one tank and still win (own_units_gte:3). A staller LOSES on the
|
| 129 |
+
# kill bar via after_ticks; a brute attack-move spreads onto the
|
| 130 |
+
# chaff and the three e3s wipe the squad; kill-chaff-first loses 3
|
| 131 |
+
# tanks to the rockets and busts the floor. Focus-threats-first
|
| 132 |
+
# keeps all four tanks → WIN.
|
| 133 |
+
easy:
|
| 134 |
+
description: >
|
| 135 |
+
Four medium tanks stage at x=48, y=18..21 with your
|
| 136 |
+
construction yard (fact) behind them at the west edge. To the
|
| 137 |
+
east is a mixed enemy cluster: a screen of NINE rifle infantry
|
| 138 |
+
(e1, cheap chaff) at x=66 backed by THREE rocket soldiers (e3,
|
| 139 |
+
anti-tank Dragon launcher — the high-threat units) at x=69.
|
| 140 |
+
Threat-priority doctrine: concentrate ALL FOUR tanks' fire on
|
| 141 |
+
ONE rocket soldier first (kill it in 1-2 decision turns), then
|
| 142 |
+
the second, then the third, THEN mop up the rifle chaff.
|
| 143 |
+
Engaging the chaff first leaves the rocket soldiers firing
|
| 144 |
+
anti-tank rockets through the whole mop-up and they pick off
|
| 145 |
+
your tanks. Win when 12 enemy units are killed AND at least 3
|
| 146 |
+
tanks survive AND your construction yard still stands, before
|
| 147 |
+
tick 2700.
|
| 148 |
+
overrides:
|
| 149 |
+
actors:
|
| 150 |
+
# Strike force — 4 medium tanks staging mid-west.
|
| 151 |
+
- {type: 2tnk, owner: agent, position: [48, 18], stance: 1}
|
| 152 |
+
- {type: 2tnk, owner: agent, position: [48, 19], stance: 1}
|
| 153 |
+
- {type: 2tnk, owner: agent, position: [48, 20], stance: 1}
|
| 154 |
+
- {type: 2tnk, owner: agent, position: [48, 21], stance: 1}
|
| 155 |
+
# Agent construction yard — the non-combat win anchor
|
| 156 |
+
# (building_count_gte:fact:1); also keeps the agent side
|
| 157 |
+
# alive for the evaluator.
|
| 158 |
+
- {type: fact, owner: agent, position: [4, 20]}
|
| 159 |
+
# Enemy cluster — a COMPACT contiguous screen of NINE cheap
|
| 160 |
+
# rifle infantry along x=66, y=15..23 (the NEAREST hostiles
|
| 161 |
+
# ⇒ engine auto-target picks them under attack_move) backed
|
| 162 |
+
# by THREE rocket soldiers at x=69, inside Dragon range 5 of
|
| 163 |
+
# the front line. stance:2 (Defend) — the cluster holds as a
|
| 164 |
+
# stationary wall and auto-fires the moment a tank is in
|
| 165 |
+
# range. The column is compact so the whole cluster stays in
|
| 166 |
+
# the tank squad's vision (no straggler hides in the fog).
|
| 167 |
+
- {type: e1, owner: enemy, position: [66, 15], stance: 2}
|
| 168 |
+
- {type: e1, owner: enemy, position: [66, 16], stance: 2}
|
| 169 |
+
- {type: e1, owner: enemy, position: [66, 17], stance: 2}
|
| 170 |
+
- {type: e1, owner: enemy, position: [66, 18], stance: 2}
|
| 171 |
+
- {type: e1, owner: enemy, position: [66, 19], stance: 2}
|
| 172 |
+
- {type: e1, owner: enemy, position: [66, 20], stance: 2}
|
| 173 |
+
- {type: e1, owner: enemy, position: [66, 21], stance: 2}
|
| 174 |
+
- {type: e1, owner: enemy, position: [66, 22], stance: 2}
|
| 175 |
+
- {type: e1, owner: enemy, position: [66, 23], stance: 2}
|
| 176 |
+
# THREE rocket soldiers — the high-threat units. Tucked
|
| 177 |
+
# behind the chaff screen at x=69 (3 cells back ⇒ not the
|
| 178 |
+
# nearest target, so attack_move auto-picks the chaff) but
|
| 179 |
+
# still inside Dragon range 5 once the tanks reach contact.
|
| 180 |
+
- {type: e3, owner: enemy, position: [69, 19], stance: 2}
|
| 181 |
+
- {type: e3, owner: enemy, position: [69, 20], stance: 2}
|
| 182 |
+
- {type: e3, owner: enemy, position: [69, 21], stance: 2}
|
| 183 |
+
# Far persistent enemy marker — prevents engine auto-done
|
| 184 |
+
# when the cluster falls so the evaluator sees the terminal
|
| 185 |
+
# frame (no DRAW collapse).
|
| 186 |
+
- {type: fact, owner: enemy, position: [120, 20]}
|
| 187 |
+
win_condition:
|
| 188 |
+
all_of:
|
| 189 |
+
- {units_killed_gte: 12}
|
| 190 |
+
- {own_units_gte: 3}
|
| 191 |
+
- {within_ticks: 2700}
|
| 192 |
+
- {building_count_gte: {type: fact, n: 1}}
|
| 193 |
+
# Fail floor aligned to the win floor (own_units_gte:3) — a squad
|
| 194 |
+
# at own=2 is a real LOSS, not a dead-zone DRAW. easy still
|
| 195 |
+
# FORGIVES one tank lost (own=3 wins); the kill-chaff-first play
|
| 196 |
+
# drops to own≤2 and LOSES here.
|
| 197 |
+
fail_condition:
|
| 198 |
+
any_of:
|
| 199 |
+
- {after_ticks: 2701}
|
| 200 |
+
- {not: {own_units_gte: 3}}
|
| 201 |
+
max_turns: 30
|
| 202 |
+
|
| 203 |
+
# ── MEDIUM ───────────────────────────────────────────────────────
|
| 204 |
+
# +1 axis vs easy: a TIGHTER survival floor — the squad must keep
|
| 205 |
+
# ALL FOUR tanks alive (own_units_gte:4, zero loss). The cluster is
|
| 206 |
+
# the same 9 e1 + 3 e3; the controlled variable is purely the
|
| 207 |
+
# zero-loss floor. A brute and a kill-chaff-first play both LOSE:
|
| 208 |
+
# the three e3s firing through the chaff mop-up score ~3 tank kills
|
| 209 |
+
# and bust the zero-loss floor. The intended focus-threats-first
|
| 210 |
+
# WINS — concentrating all four tanks on each rocket soldier in
|
| 211 |
+
# turn ends it in 1-2 decision ticks before it can land more than
|
| 212 |
+
# ~1 rocket, preserving the whole squad.
|
| 213 |
+
medium:
|
| 214 |
+
description: >
|
| 215 |
+
Four medium tanks stage at x=48, y=18..21 with your
|
| 216 |
+
construction yard behind them. The eastern enemy cluster is a
|
| 217 |
+
compact screen of NINE rifle infantry (e1, cheap chaff) at
|
| 218 |
+
x=66 backed by THREE rocket soldiers (e3, anti-tank Dragon
|
| 219 |
+
launcher — the high-threat units) at x=69. Threat-priority
|
| 220 |
+
doctrine: concentrate ALL FOUR tanks' fire on ONE rocket
|
| 221 |
+
soldier first (kill in 1-2 decision turns), then the second,
|
| 222 |
+
then the third, THEN mop up the rifle chaff. Engaging the
|
| 223 |
+
chaff first — via attack_move auto-targeting the nearer rifle
|
| 224 |
+
screen, or by attacking the cheap infantry first — leaves the
|
| 225 |
+
rocket soldiers firing through the entire mop-up and they kill
|
| 226 |
+
your tanks. Win when 12 enemy units are killed AND all four
|
| 227 |
+
tanks survive AND your construction yard still stands, before
|
| 228 |
+
tick 2700.
|
| 229 |
+
overrides:
|
| 230 |
+
actors:
|
| 231 |
+
- {type: 2tnk, owner: agent, position: [48, 18], stance: 1}
|
| 232 |
+
- {type: 2tnk, owner: agent, position: [48, 19], stance: 1}
|
| 233 |
+
- {type: 2tnk, owner: agent, position: [48, 20], stance: 1}
|
| 234 |
+
- {type: 2tnk, owner: agent, position: [48, 21], stance: 1}
|
| 235 |
+
- {type: fact, owner: agent, position: [4, 20]}
|
| 236 |
+
# Compact contiguous screen of NINE cheap rifle infantry
|
| 237 |
+
# along x=66, y=15..23 (engine auto-target picks the nearest
|
| 238 |
+
# hostile under attack_move).
|
| 239 |
+
- {type: e1, owner: enemy, position: [66, 15], stance: 2}
|
| 240 |
+
- {type: e1, owner: enemy, position: [66, 16], stance: 2}
|
| 241 |
+
- {type: e1, owner: enemy, position: [66, 17], stance: 2}
|
| 242 |
+
- {type: e1, owner: enemy, position: [66, 18], stance: 2}
|
| 243 |
+
- {type: e1, owner: enemy, position: [66, 19], stance: 2}
|
| 244 |
+
- {type: e1, owner: enemy, position: [66, 20], stance: 2}
|
| 245 |
+
- {type: e1, owner: enemy, position: [66, 21], stance: 2}
|
| 246 |
+
- {type: e1, owner: enemy, position: [66, 22], stance: 2}
|
| 247 |
+
- {type: e1, owner: enemy, position: [66, 23], stance: 2}
|
| 248 |
+
# THREE rocket soldiers behind the chaff screen at x=69 —
|
| 249 |
+
# inside Dragon range 5 of the contact line, not the nearest
|
| 250 |
+
# target (e1s at x=66 are 3 cells closer ⇒ engine auto-
|
| 251 |
+
# target picks the chaff under attack_move). With 3 e3s
|
| 252 |
+
# firing through the chaff mop-up a brute or chaff-first
|
| 253 |
+
# play takes ~3 tank kills and busts the zero-loss floor.
|
| 254 |
+
- {type: e3, owner: enemy, position: [69, 19], stance: 2}
|
| 255 |
+
- {type: e3, owner: enemy, position: [69, 20], stance: 2}
|
| 256 |
+
- {type: e3, owner: enemy, position: [69, 21], stance: 2}
|
| 257 |
+
- {type: fact, owner: enemy, position: [120, 20]}
|
| 258 |
+
win_condition:
|
| 259 |
+
all_of:
|
| 260 |
+
- {units_killed_gte: 12}
|
| 261 |
+
- {own_units_gte: 4}
|
| 262 |
+
- {within_ticks: 2700}
|
| 263 |
+
- {building_count_gte: {type: fact, n: 1}}
|
| 264 |
+
# Fail floor aligned to the zero-loss win floor (own_units_gte:4)
|
| 265 |
+
# — any tank lost (own≤3) is a real LOSS, not a dead-zone DRAW.
|
| 266 |
+
fail_condition:
|
| 267 |
+
any_of:
|
| 268 |
+
- {after_ticks: 2701}
|
| 269 |
+
- {not: {own_units_gte: 4}}
|
| 270 |
+
max_turns: 30
|
| 271 |
+
|
| 272 |
+
# ── HARD ────────────────────────────────────────────────���────────
|
| 273 |
+
# +1 axis vs medium: the strike force (and its construction yard)
|
| 274 |
+
# stage from TWO seed-driven spawn_point groups (NORTH corridor
|
| 275 |
+
# y=10..13 OR SOUTH corridor y=27..30) so the engagement geometry
|
| 276 |
+
# varies per seed. The chaff screen (10 e1, y=15..24) + rocket trio
|
| 277 |
+
# is symmetric across y=20 — both spawns face the same priority
|
| 278 |
+
# decision (focus one e3 → next → next → mop up chaff). The
|
| 279 |
+
# zero-loss survival floor from medium is retained.
|
| 280 |
+
hard:
|
| 281 |
+
description: >
|
| 282 |
+
Four medium tanks stage at ONE of two corridors (NORTH
|
| 283 |
+
y=10..13 OR SOUTH y=27..30, chosen by seed, anti-
|
| 284 |
+
memorisation) with your construction yard behind them. The
|
| 285 |
+
eastern enemy cluster is a compact screen of TEN rifle
|
| 286 |
+
infantry (e1, cheap chaff) at x=66 backed by THREE rocket
|
| 287 |
+
soldiers (e3, anti-tank — the high-threat units) at x=69.
|
| 288 |
+
Threat-priority doctrine: concentrate ALL FOUR tanks on ONE
|
| 289 |
+
rocket soldier at a time — kill it cleanly, then the next,
|
| 290 |
+
then the third, THEN mop up the rifle chaff. Starting on the
|
| 291 |
+
rifle chaff leaves the rockets firing through the whole
|
| 292 |
+
engagement and they kill your tanks. Win when 13 enemy units
|
| 293 |
+
are killed AND all four tanks survive AND your construction
|
| 294 |
+
yard still stands, before tick 2700.
|
| 295 |
+
overrides:
|
| 296 |
+
actors:
|
| 297 |
+
# spawn_point 0 — NORTH staging corridor (y=10..13).
|
| 298 |
+
- {type: 2tnk, owner: agent, position: [48, 10], stance: 1, spawn_point: 0}
|
| 299 |
+
- {type: 2tnk, owner: agent, position: [48, 11], stance: 1, spawn_point: 0}
|
| 300 |
+
- {type: 2tnk, owner: agent, position: [48, 12], stance: 1, spawn_point: 0}
|
| 301 |
+
- {type: 2tnk, owner: agent, position: [48, 13], stance: 1, spawn_point: 0}
|
| 302 |
+
- {type: fact, owner: agent, position: [4, 11], spawn_point: 0}
|
| 303 |
+
# spawn_point 1 — SOUTH staging corridor (y=27..30).
|
| 304 |
+
- {type: 2tnk, owner: agent, position: [48, 27], stance: 1, spawn_point: 1}
|
| 305 |
+
- {type: 2tnk, owner: agent, position: [48, 28], stance: 1, spawn_point: 1}
|
| 306 |
+
- {type: 2tnk, owner: agent, position: [48, 29], stance: 1, spawn_point: 1}
|
| 307 |
+
- {type: 2tnk, owner: agent, position: [48, 30], stance: 1, spawn_point: 1}
|
| 308 |
+
- {type: fact, owner: agent, position: [4, 29], spawn_point: 1}
|
| 309 |
+
# Compact contiguous screen of TEN cheap rifle infantry along
|
| 310 |
+
# x=66, y=15..24 — symmetric across y=19.5 so either spawn
|
| 311 |
+
# faces an equivalent decision. Engine auto-target picks the
|
| 312 |
+
# nearest hostile under attack_move ⇒ this chaff line is what
|
| 313 |
+
# a brute engages first.
|
| 314 |
+
- {type: e1, owner: enemy, position: [66, 15], stance: 2}
|
| 315 |
+
- {type: e1, owner: enemy, position: [66, 16], stance: 2}
|
| 316 |
+
- {type: e1, owner: enemy, position: [66, 17], stance: 2}
|
| 317 |
+
- {type: e1, owner: enemy, position: [66, 18], stance: 2}
|
| 318 |
+
- {type: e1, owner: enemy, position: [66, 19], stance: 2}
|
| 319 |
+
- {type: e1, owner: enemy, position: [66, 20], stance: 2}
|
| 320 |
+
- {type: e1, owner: enemy, position: [66, 21], stance: 2}
|
| 321 |
+
- {type: e1, owner: enemy, position: [66, 22], stance: 2}
|
| 322 |
+
- {type: e1, owner: enemy, position: [66, 23], stance: 2}
|
| 323 |
+
- {type: e1, owner: enemy, position: [66, 24], stance: 2}
|
| 324 |
+
# THREE rocket soldiers behind the chaff screen at x=69 —
|
| 325 |
+
# inside Dragon range 5 of the contact line, symmetric across
|
| 326 |
+
# y=20 so either spawn faces an equivalent focus-fire
|
| 327 |
+
# decision. Starting on the chaff lets all three fire through
|
| 328 |
+
# the whole mop-up and bust the zero-loss floor.
|
| 329 |
+
- {type: e3, owner: enemy, position: [69, 19], stance: 2}
|
| 330 |
+
- {type: e3, owner: enemy, position: [69, 20], stance: 2}
|
| 331 |
+
- {type: e3, owner: enemy, position: [69, 21], stance: 2}
|
| 332 |
+
- {type: fact, owner: enemy, position: [120, 20]}
|
| 333 |
+
win_condition:
|
| 334 |
+
all_of:
|
| 335 |
+
- {units_killed_gte: 13}
|
| 336 |
+
- {own_units_gte: 4}
|
| 337 |
+
- {within_ticks: 2700}
|
| 338 |
+
- {building_count_gte: {type: fact, n: 1}}
|
| 339 |
+
# Fail floor aligned to the zero-loss win floor (own_units_gte:4)
|
| 340 |
+
# — any tank lost (own≤3) is a real LOSS, not a dead-zone DRAW.
|
| 341 |
+
fail_condition:
|
| 342 |
+
any_of:
|
| 343 |
+
- {after_ticks: 2701}
|
| 344 |
+
- {not: {own_units_gte: 4}}
|
| 345 |
+
max_turns: 30
|
tests/test_combat_target_priority_highvalue.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""combat-target-priority-highvalue pack — full no-cheat validation.
|
| 2 |
+
|
| 3 |
+
Wave-11 ACTION pack: threat-weighted target prioritization (SC2
|
| 4 |
+
focus-fire target priority / military target prioritization anchor).
|
| 5 |
+
A 4-tank squad faces a mixed enemy cluster — a screen of cheap rifle
|
| 6 |
+
chaff (e1) backed by THREE high-threat anti-armour rocket soldiers
|
| 7 |
+
(e3). The squad must FOCUS THE ROCKET SOLDIERS FIRST: 4-vs-1
|
| 8 |
+
concentrated fire deletes each e3 in 1-2 decision turns before it can
|
| 9 |
+
land more than ~1 rocket. Killing the chaff first leaves all three
|
| 10 |
+
rockets firing through the entire mop-up and they whittle the squad
|
| 11 |
+
below the survival floor.
|
| 12 |
+
|
| 13 |
+
Win = units_killed_gte:K AND own_units_gte:F AND within_ticks:2700
|
| 14 |
+
AND building_count_gte:fact:1 (a non-combat anchor: the squad must
|
| 15 |
+
still own its construction yard while it focus-fires). Fail floors
|
| 16 |
+
are aligned to the win floor so every non-timeout outcome is a real
|
| 17 |
+
WIN or LOSS — no dead-zone DRAW.
|
| 18 |
+
|
| 19 |
+
Bar (per CLAUDE.md), verified deterministic across seeds 1-4 on
|
| 20 |
+
every level:
|
| 21 |
+
• stall (observe only) → LOSS every level/seed (after_ticks).
|
| 22 |
+
• brute attack_move → LOSS (auto-targets the chaff screen;
|
| 23 |
+
the three e3s wipe the squad).
|
| 24 |
+
• kill-chaff-first → LOSS (rockets fire through the chaff
|
| 25 |
+
mop-up and bust the survival floor).
|
| 26 |
+
• focus-threats-first → WIN every level/seed (all three e3s
|
| 27 |
+
silenced inside the first ~3 turns, squad preserved).
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
from __future__ import annotations
|
| 31 |
+
|
| 32 |
+
import pytest
|
| 33 |
+
|
| 34 |
+
pytest.importorskip("openra_train", reason="Rust env wheel not installed")
|
| 35 |
+
|
| 36 |
+
from openra_bench.eval_core import run_level
|
| 37 |
+
from openra_bench.scenarios import load_pack
|
| 38 |
+
from openra_bench.scenarios.loader import PACKS_DIR, compile_level
|
| 39 |
+
|
| 40 |
+
PACK = PACKS_DIR / "combat-target-priority-highvalue.yaml"
|
| 41 |
+
LEVELS = ("easy", "medium", "hard")
|
| 42 |
+
SEEDS = (1, 2, 3, 4)
|
| 43 |
+
# enemy cluster advance target — the chaff column, mid-y.
|
| 44 |
+
_ADV = (66, 20)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# ── helpers ───────────────────────────────────────────────────────
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _tank_ids(obs):
|
| 51 |
+
return [
|
| 52 |
+
str(u["id"])
|
| 53 |
+
for u in (obs.get("units_summary") or [])
|
| 54 |
+
if str(u.get("type", "")).lower() == "2tnk"
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def _enemy_units(obs):
|
| 59 |
+
"""(id, type) for every visible enemy combat unit."""
|
| 60 |
+
out = []
|
| 61 |
+
for e in (obs.get("enemy_summary") or []):
|
| 62 |
+
t = str(e.get("type", "")).lower()
|
| 63 |
+
if t in ("e1", "e3"):
|
| 64 |
+
out.append((str(e.get("id")), t))
|
| 65 |
+
return out
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# ── policies ──────────────────────────────────────────────────────
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _stall_policy():
|
| 72 |
+
"""Do nothing — the kill bar is never met, after_ticks LOSS."""
|
| 73 |
+
def pol(obs, Cmd):
|
| 74 |
+
return [Cmd.observe()]
|
| 75 |
+
return pol
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def _brute_policy():
|
| 79 |
+
"""attack_move onto the cluster centroid — the engine auto-targets
|
| 80 |
+
the NEAREST hostile (the chaff screen), so the three e3s fire
|
| 81 |
+
through the whole engagement and wipe the squad."""
|
| 82 |
+
def pol(obs, Cmd):
|
| 83 |
+
ids = _tank_ids(obs)
|
| 84 |
+
if not ids:
|
| 85 |
+
return [Cmd.observe()]
|
| 86 |
+
es = [
|
| 87 |
+
e for e in (obs.get("enemy_summary") or [])
|
| 88 |
+
if str(e.get("type", "")).lower() in ("e1", "e3")
|
| 89 |
+
]
|
| 90 |
+
if es:
|
| 91 |
+
cx = sum(e["cell_x"] for e in es) // len(es)
|
| 92 |
+
cy = sum(e["cell_y"] for e in es) // len(es)
|
| 93 |
+
return [Cmd.attack_move(ids, cx, cy)]
|
| 94 |
+
return [Cmd.attack_move(ids, _ADV[0], _ADV[1])]
|
| 95 |
+
return pol
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _focus_policy(first: str):
|
| 99 |
+
"""attack_unit, prioritising the `first` unit type. first='e1' is
|
| 100 |
+
the kill-chaff-first trap; first='e3' is the intended threat-first
|
| 101 |
+
focus play."""
|
| 102 |
+
def pol(obs, Cmd):
|
| 103 |
+
ids = _tank_ids(obs)
|
| 104 |
+
if not ids:
|
| 105 |
+
return [Cmd.observe()]
|
| 106 |
+
es = _enemy_units(obs)
|
| 107 |
+
prio = [e for e in es if e[1] == first]
|
| 108 |
+
rest = [e for e in es if e[1] != first]
|
| 109 |
+
if prio:
|
| 110 |
+
return [Cmd.attack_unit(ids, prio[0][0])]
|
| 111 |
+
if rest:
|
| 112 |
+
return [Cmd.attack_unit(ids, rest[0][0])]
|
| 113 |
+
# no enemy in view — close to contact range.
|
| 114 |
+
return [Cmd.attack_move(ids, _ADV[0], _ADV[1])]
|
| 115 |
+
return pol
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# ── tests ─────────────────────────────────────────────────────────
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
@pytest.mark.parametrize("level", LEVELS)
|
| 122 |
+
@pytest.mark.parametrize("seed", SEEDS)
|
| 123 |
+
def test_stall_loses(level, seed):
|
| 124 |
+
"""A do-nothing policy must LOSE on the deadline — no draw."""
|
| 125 |
+
c = compile_level(load_pack(PACK), level)
|
| 126 |
+
res = run_level(c, _stall_policy(), seed=seed)
|
| 127 |
+
assert res.outcome == "loss", (
|
| 128 |
+
f"{level}/seed{seed}: stall must LOSE, got {res.outcome}"
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
@pytest.mark.parametrize("level", LEVELS)
|
| 133 |
+
@pytest.mark.parametrize("seed", SEEDS)
|
| 134 |
+
def test_brute_attack_move_loses(level, seed):
|
| 135 |
+
"""A brute attack-move play auto-targets the chaff screen; the
|
| 136 |
+
three rocket soldiers fire through the engagement and wipe the
|
| 137 |
+
squad — a real LOSS, not a draw."""
|
| 138 |
+
c = compile_level(load_pack(PACK), level)
|
| 139 |
+
res = run_level(c, _brute_policy(), seed=seed)
|
| 140 |
+
assert res.outcome == "loss", (
|
| 141 |
+
f"{level}/seed{seed}: brute must LOSE, got {res.outcome}"
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
@pytest.mark.parametrize("level", LEVELS)
|
| 146 |
+
@pytest.mark.parametrize("seed", SEEDS)
|
| 147 |
+
def test_kill_chaff_first_loses(level, seed):
|
| 148 |
+
"""Explicitly attacking the cheap e1 chaff first leaves the three
|
| 149 |
+
rockets firing through the whole mop-up; the squad drops below the
|
| 150 |
+
survival floor — a real LOSS."""
|
| 151 |
+
c = compile_level(load_pack(PACK), level)
|
| 152 |
+
res = run_level(c, _focus_policy("e1"), seed=seed)
|
| 153 |
+
assert res.outcome == "loss", (
|
| 154 |
+
f"{level}/seed{seed}: kill-chaff-first must LOSE, got {res.outcome}"
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
@pytest.mark.parametrize("level", LEVELS)
|
| 159 |
+
@pytest.mark.parametrize("seed", SEEDS)
|
| 160 |
+
def test_focus_threats_first_wins(level, seed):
|
| 161 |
+
"""The intended capability — concentrate all four tanks on the
|
| 162 |
+
rocket soldiers FIRST — must WIN every level and seed."""
|
| 163 |
+
c = compile_level(load_pack(PACK), level)
|
| 164 |
+
res = run_level(c, _focus_policy("e3"), seed=seed)
|
| 165 |
+
assert res.outcome == "win", (
|
| 166 |
+
f"{level}/seed{seed}: focus-threats-first must WIN, got {res.outcome}"
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def test_hard_agent_spawn_axis_has_two_groups():
|
| 171 |
+
"""The hard tier must define ≥2 agent-side spawn_point groups
|
| 172 |
+
(the seed-driven staging-corridor axis); the strike force and its
|
| 173 |
+
construction yard are duplicated under each group."""
|
| 174 |
+
c = compile_level(load_pack(PACK), "hard")
|
| 175 |
+
agent_sps = {
|
| 176 |
+
a.spawn_point
|
| 177 |
+
for a in c.scenario.actors
|
| 178 |
+
if a.owner == "agent" and a.spawn_point is not None
|
| 179 |
+
}
|
| 180 |
+
assert len(agent_sps) >= 2, (
|
| 181 |
+
f"hard needs ≥2 agent spawn_point groups, got {sorted(agent_sps)}"
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def test_tick_budget_alignment():
|
| 186 |
+
"""within_ticks / after_ticks must be reachable inside max_turns
|
| 187 |
+
(tick ≤ 93 + 90·(max_turns-1)) so the deadline actually bites."""
|
| 188 |
+
for level in LEVELS:
|
| 189 |
+
c = compile_level(load_pack(PACK), level)
|
| 190 |
+
max_tick = 93 + 90 * (c.max_turns - 1)
|
| 191 |
+
win_clauses = c.win_condition.all_of or []
|
| 192 |
+
within = next(
|
| 193 |
+
(cl["within_ticks"] for cl in win_clauses if "within_ticks" in cl),
|
| 194 |
+
None,
|
| 195 |
+
)
|
| 196 |
+
assert within is not None and within <= max_tick, (
|
| 197 |
+
f"{level}: within_ticks {within} not reachable by {max_tick}"
|
| 198 |
+
)
|
| 199 |
+
fail_clauses = c.fail_condition.any_of or []
|
| 200 |
+
after = next(
|
| 201 |
+
(cl["after_ticks"] for cl in fail_clauses if "after_ticks" in cl),
|
| 202 |
+
None,
|
| 203 |
+
)
|
| 204 |
+
assert after is not None and after <= max_tick, (
|
| 205 |
+
f"{level}: after_ticks {after} not reachable by {max_tick}"
|
| 206 |
+
)
|