From 67371e77dd4200e02219b237568af28af4e41742 Mon Sep 17 00:00:00 2001 From: lili Date: Wed, 17 Jun 2026 14:00:05 -0700 Subject: [PATCH] =?UTF-8?q?feat(adapter):=20E1=20=E6=B3=9B=E5=8C=96?= =?UTF-8?q?=E2=80=94=E2=80=94drift-targets(update=20=E8=A1=8C=E4=B8=BA)+?= =?UTF-8?q?=20=E7=AC=AC=E4=BA=8C=20archetype=20=E7=9C=9F=E4=B9=9D=E9=97=A8?= =?UTF-8?q?=E7=BB=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit U1 展开(plan 002):证适配器不止 tap-targets,泛化到 realtime/update 行为。 - 加 trigger=update 词汇 op=drift-targets(目标随 ctx.time 慢漂移,amplitude/speed 可配);targets 记 baseX/baseY/phase。 - 新 fixture moving-targets.source.json(tickModel=realtime + drift behavior + tap behavior + win rule)。 - drift 单测 +2(共 8):update 后目标漂移 / 漂移下 tap 当前位仍命中计分。单测 11/11 绿(adapter 8 + scaffold 3)。 - e2e(集成段,本机):scaffoldFromSource 落 moving-targets → build → 真九门 A–I + 首局门全过 (tap driver 命中漂移目标、score 0→3、latch 驻留;瞬时验证件已清)。 证成:两迥异 archetype(静态点击 + realtime 漂移)经同一适配器都过真九门 → 退役 adversarial F4 「声明式最小集过不了九门」之虑;E1 声明式路泛化成立。 Co-Authored-By: Claude Opus 4.8 --- game-runtime/src/adapter/adapter.test.mjs | 22 ++++++++++++++++ .../adapter/create-game-from-definition.js | 16 +++++++++++- .../fixtures/moving-targets.source.json | 26 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 game-runtime/src/adapter/fixtures/moving-targets.source.json diff --git a/game-runtime/src/adapter/adapter.test.mjs b/game-runtime/src/adapter/adapter.test.mjs index 20d19a19..5b8cc7a4 100644 --- a/game-runtime/src/adapter/adapter.test.mjs +++ b/game-runtime/src/adapter/adapter.test.mjs @@ -23,6 +23,7 @@ import { createHostDevContext } from '../core/plugin.js'; const HERE = dirname(fileURLToPath(import.meta.url)); const SOURCE = JSON.parse(readFileSync(join(HERE, 'fixtures', 'tap-target.source.json'), 'utf8')); +const DRIFT_SOURCE = JSON.parse(readFileSync(join(HERE, 'fixtures', 'moving-targets.source.json'), 'utf8')); /** 录制版 2D 上下文:记 fillRect/fillText 调用,setter 容纳 fillStyle/font/strokeStyle 赋值。 */ function recordingCtx2d() { @@ -120,3 +121,24 @@ test('destroy 幂等', () => { game.destroy(); assert.equal(game._forensicsView().state().targets.length, 0); }); + +test('drift-targets:update 后目标漂移(realtime/update 行为泛化)', () => { + const game = adaptGameDefinition(DRIFT_SOURCE)(); + const { boot } = makeBoot(); + game.init(boot); + const before = game._forensicsView().state().targets.map((t) => t.x); + game.update(0.1); + game.update(0.1); + const after = game._forensicsView().state().targets.map((t) => t.x); + assert.ok(after.some((x, i) => x !== before[i]), '至少一个目标 x 应漂移(update 行为生效)'); +}); + +test('drift 下 tap 当前(漂移后)位仍命中计分', () => { + const game = adaptGameDefinition(DRIFT_SOURCE)(); + const { boot, inputBridge } = makeBoot(); + game.init(boot); + game.update(0.1); + const t0 = game._forensicsView().state().targets[0]; // 当前漂移后中心 + inputBridge._emit('pointerdown', { x: t0.x, y: t0.y }); + assert.equal(game._forensicsView().state().score, 1); +}); diff --git a/game-runtime/src/adapter/create-game-from-definition.js b/game-runtime/src/adapter/create-game-from-definition.js index 7c54d392..9b8ca24d 100644 --- a/game-runtime/src/adapter/create-game-from-definition.js +++ b/game-runtime/src/adapter/create-game-from-definition.js @@ -20,6 +20,8 @@ const VIEWPORT_H = 844; /** 最小 behavior 词汇表(声明式路支持的 op;其余 op = 走 code-agent 逻辑码,本切片不解释)。 */ const SUPPORTED_INPUT_OPS = new Set(['hit-nearest-target']); +/** trigger=update 的 op(本切片支持 drift-targets:目标随时间慢漂移,证 realtime/update behavior 路泛化)。 */ +const SUPPORTED_UPDATE_OPS = new Set(['drift-targets']); /** * 极小安全条件求值(无 eval):解析 " "。 @@ -92,12 +94,13 @@ export function adaptGameDefinition(source) { /** init:建目标、订阅受控输入、置 playing。 */ function init(boot) { ctx = boot.ctx; - targets = activeEntities.map((e) => { + targets = activeEntities.map((e, i) => { const rc = renderCompOf(e) || {}; const pos = (e.transform && e.transform.position) || { x: 0, y: 0 }; return { id: e.id, x: pos.x, y: pos.y, + baseX: pos.x, baseY: pos.y, phase: i * 1.7, // drift-targets 用:漂移基准 + 错相 w: typeof rc.w === 'number' ? rc.w : 48, h: typeof rc.h === 'number' ? rc.h : 48, color: typeof rc.color === 'string' ? rc.color : '#3cf', @@ -148,6 +151,17 @@ export function adaptGameDefinition(source) { function update(dt) { if (ctx) { nowMs = ctx.time.nowMs(); state.t = nowMs / 1000; } if (state.phase !== 'playing') return; // 已终态:latch 驻留,不再评估 + // trigger=update 行为(本切片:drift-targets 目标慢漂移,证 realtime/update 路)。 + for (const b of behaviors) { + if (b.trigger !== 'update' || !SUPPORTED_UPDATE_OPS.has(b.op)) continue; + if (b.op === 'drift-targets') { + const amp = typeof b.amplitude === 'number' ? b.amplitude : 24; + const spd = typeof b.speed === 'number' ? b.speed : 0.8; + for (const tg of targets) { + tg.x = tg.baseX + amp * Math.sin(state.t * spd + tg.phase); // 慢漂移:driver 逐步读 _forensicsView 仍可命中 + } + } + } for (const r of rules) { if (evalCondition(r.condition, state, config)) { if (r.outcome === 'win' || r.outcome === 'lose') { diff --git a/game-runtime/src/adapter/fixtures/moving-targets.source.json b/game-runtime/src/adapter/fixtures/moving-targets.source.json new file mode 100644 index 00000000..c7acea30 --- /dev/null +++ b/game-runtime/src/adapter/fixtures/moving-targets.source.json @@ -0,0 +1,26 @@ +{ + "schemaVersion": "1.0", + "sourceHash": "1111111111111111111111111111111111111111111111111111111111111111", + "profile": { "tickModel": "realtime", "inputModel": "discrete-choice", "progressModel": "metric" }, + "gameDefinition": { + "entities": [ + { "id": "m1", "transform": { "position": { "x": 100, "y": 240 } }, "components": ["r-mov"] }, + { "id": "m2", "transform": { "position": { "x": 200, "y": 440 } }, "components": ["r-mov"] }, + { "id": "m3", "transform": { "position": { "x": 150, "y": 640 } }, "components": ["r-mov"] } + ], + "components": [ + { "id": "r-mov", "kind": "render", "shape": "rect", "w": 60, "h": 60, "color": "#7cf" } + ], + "behaviors": [ + { "id": "b-drift", "trigger": "update", "op": "drift-targets", "amplitude": 24, "speed": 0.8 }, + { "id": "b-tap", "trigger": "input", "op": "hit-nearest-target" } + ], + "scenes": [ + { "id": "s1", "entityRefs": ["m1", "m2", "m3"] } + ], + "rules": [ + { "id": "win", "condition": "score >= config.winScore", "outcome": "win" } + ] + }, + "config": { "winScore": 3, "hitRadius": 60 } +}