feat(adapter): E1 泛化——drift-targets(update 行为)+ 第二 archetype 真九门绿

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 <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-17 14:00:05 -07:00
parent bc1c337776
commit 67371e77dd
3 changed files with 63 additions and 1 deletions

View File

@ -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-targetsupdate 后目标漂移(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);
});

View File

@ -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解析 "<field> <op> <rhs>"
@ -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') {

View File

@ -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 }
}