fix(WU-C 5.4): tap-targets harness 认单数/可空 target — 解 M2 Node 退役阻塞
M2 auto-vs-golden 门抓到的生成契约漂移修复(Codex 实现 + 主代理诊断/受控验证)。 根因:~20% click-score 款 _forensicsView 暴露单数 target(对象、菜单态为 null) 而非复数 targets(数组),tap-targets harness 只认复数数组:① _build_play_spec 推断漏判→误推 key-cycle;② 驱动器 targetsPath 找不到单数 target。 两处修(认两种合法形态、既有数组行为零变): - cheap_run.py:_build_play_spec 以【键存在性】判 tap-targets(targets/target 键 在即可,菜单态 null/空数组仍归 tap-targets)——非值类型(空数组过/null 不过的 非对称是漏判根因)。 - play.cdp.cjs:runTapTargets 取值非数组时回退读单数 target、包成 1 元素数组。 验证:退化款 avg-click-score-14 auto 路 E_live/G_input/H_progress 0.667→1.0 (3/3 确定性复现);test_ensure_play_spec 8→13 绿。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3d64129d8e
commit
ffe3a5c993
@ -214,7 +214,8 @@ def _build_play_spec(state: Optional[dict]) -> dict:
|
||||
tap-targets occupied 族(state 有 targets 数组):加厚到金标同质——补 F_wiring 的 expectedEngineCallPrefixes
|
||||
语义期望(外生 held 期望、预置常量,防自证);key-cycle 族(按键类):薄版,期望前缀待输入键契约(WU-C 5.4)。
|
||||
"""
|
||||
has_targets = isinstance(state, dict) and isinstance(state.get("targets"), list)
|
||||
# 以键存在性判定「可点目标」类游戏:游戏暴露 targets/target 键即声明有可点目标,菜单态 null/空数组仍视为 tap-targets
|
||||
has_targets = isinstance(state, dict) and ("targets" in state or "target" in state)
|
||||
if has_targets:
|
||||
driver = {"type": "tap-targets", "targetMode": "occupied", "targetsPath": "targets", "steps": 50, "stepMs": 220}
|
||||
return {
|
||||
|
||||
@ -31,6 +31,30 @@ def test_tap_targets_adds_golden_prefixes():
|
||||
assert spec["expectedEngineCallPrefixes"] == _GOLDEN_PREFIXES
|
||||
|
||||
|
||||
def test_singular_target_object_uses_tap_targets():
|
||||
"""单数 target 对象是合法点击目标形态,应走 tap-targets 驱动。"""
|
||||
spec = R._build_play_spec({"target": {"x": 1, "y": 2, "r": 3, "occupied": True}, "phase": "play"})
|
||||
assert spec["driver"]["type"] == "tap-targets"
|
||||
|
||||
|
||||
def test_singular_target_null_menu_state_uses_tap_targets():
|
||||
"""菜单态 target 可为 null,但暴露 target 键即声明点击目标类。"""
|
||||
spec = R._build_play_spec({"target": None, "phase": "menu", "score": 0})
|
||||
assert spec["driver"]["type"] == "tap-targets"
|
||||
|
||||
|
||||
def test_targets_array_uses_tap_targets():
|
||||
"""复数 targets 数组保持既有 tap-targets 驱动行为。"""
|
||||
spec = R._build_play_spec({"targets": [{"x": 1, "y": 2, "r": 3, "occupied": True}], "phase": "play"})
|
||||
assert spec["driver"]["type"] == "tap-targets"
|
||||
|
||||
|
||||
def test_empty_targets_array_still_uses_tap_targets():
|
||||
"""空 targets 数组仍代表点击类导出形态,不可退回 key-cycle。"""
|
||||
spec = R._build_play_spec({"targets": [], "phase": "play"})
|
||||
assert spec["driver"]["type"] == "tap-targets"
|
||||
|
||||
|
||||
def test_matches_golden_fixtures():
|
||||
"""加厚后的 prefixes 与三份金标 fixtures 的 expectedEngineCallPrefixes 逐字段一致(同质)。"""
|
||||
spec = R._build_play_spec({"targets": []}) # targets=[] 仍是 list → tap-targets
|
||||
@ -71,6 +95,12 @@ def test_key_cycle_thin_no_prefixes():
|
||||
assert "WU-C 5.4" in spec.get("_note", ""), "key-cycle 薄版应标注延后原因"
|
||||
|
||||
|
||||
def test_state_without_target_keys_is_key_cycle_thin():
|
||||
"""没有 targets/target 键时保持按键类薄版回退。"""
|
||||
spec = R._build_play_spec({"phase": "play", "score": 0})
|
||||
assert spec["driver"]["type"] == "key-cycle"
|
||||
|
||||
|
||||
def test_none_state_is_key_cycle_thin():
|
||||
spec = R._build_play_spec(None) # None → 保守按键类
|
||||
assert spec["driver"]["type"] == "key-cycle"
|
||||
|
||||
@ -254,21 +254,26 @@ async function runTapTargets(cdp, driver, hashes) {
|
||||
continue;
|
||||
}
|
||||
const targets = getPath(s, targetsPath);
|
||||
if (Array.isArray(targets) && targets.length) {
|
||||
// 兼容单数 target 对象:若 targets 不是数组,尝试读单数 target 并包成单元素数组。
|
||||
let resolvedTargets = Array.isArray(targets) ? targets : (() => {
|
||||
const singular = getPath(s, 'target');
|
||||
return (singular && typeof singular.x === 'number' && typeof singular.y === 'number') ? [singular] : [];
|
||||
})();
|
||||
if (Array.isArray(resolvedTargets) && resolvedTargets.length) {
|
||||
let t;
|
||||
if (driver.safeOnly === true) {
|
||||
// 规避/推理族(扫雷):仅点 safe===true 的目标,确定性避负 → 把核心循环(洪水填充/邻数揭示/win 判定)跑透;
|
||||
// 无安全目标(核心应已揭完→win)即停,交 latch 校验 win 终态;绝不回退点不安全目标(否则又踩雷、核心零暴露)。
|
||||
t = targets.find((g) => g && g.occupied !== true && g.safe === true && typeof g.x === 'number');
|
||||
t = resolvedTargets.find((g) => g && g.occupied !== true && g.safe === true && typeof g.x === 'number');
|
||||
if (!t) break;
|
||||
} else if (driver.targetMode === 'occupied') {
|
||||
// 反应/服务族(打地鼠/经营服务客…):只点 occupied===true 的目标(地鼠冒头/顾客在等才可点);
|
||||
// 本拍无可点(没冒头/没客)→ 不 tap、空转等下一拍(targets 随时变,下拍重采)。与放置族相反语义。
|
||||
t = targets.find((g) => g && g.occupied === true && typeof g.x === 'number');
|
||||
t = resolvedTargets.find((g) => g && g.occupied === true && typeof g.x === 'number');
|
||||
} else {
|
||||
// 安全放置族(井字棋/invaders…):任一未占用目标都是合法推进招,盲点首个;全占用/未导出 occupied → 按序轮点。
|
||||
t = targets.find((g) => g && g.occupied !== true && typeof g.x === 'number');
|
||||
if (!t) { t = targets[cyc % targets.length]; }
|
||||
t = resolvedTargets.find((g) => g && g.occupied !== true && typeof g.x === 'number');
|
||||
if (!t) { t = resolvedTargets[cyc % resolvedTargets.length]; }
|
||||
}
|
||||
cyc++;
|
||||
if (t && typeof t.x === 'number') { await tap(cdp, Math.max(4, Math.min(386, t.x)), Math.max(4, Math.min(840, t.y))); drove++; }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user