games-development-ai/game-runtime/src/adapter/create-game-from-definition.js
lili 67371e77dd 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>
2026-06-17 14:00:05 -07:00

231 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
/**
* create-game-from-definition.js —— E1 2D 适配器U1 最小贯穿实验切片)
* owner前端+引擎线Mac计划docs/plans/2026-06-17-002-feat-mac-engine-frontend-tier0-plan.md U1
*
* 【做什么】把固定架构的声明式 `SourceProject.gameDefinition`contracts/agent-loop/source-project.schema.json
* 适配成命中 game-host.d.ts `GameHostFactory`→`GameInstance` 契约的可玩实例:
* entities → 渲染对象 · components(render) → 画法 · behaviors → 逻辑 · scenes → 关卡构成 · rules → 胜负 · config → 平衡参数。
*
* 【边界KTD1跨线已锁方向】本切片是**声明式解释器**:解释 entities/components/scenes/rules/config
* + 一个**最小 behavior 词汇**(够单 behavior 游戏端到端过九门)。复杂 behavior 的真逻辑由 code agent 产、
* 经 build.mjs 编进 __GameBundle非本适配器解释——本切片只证「声明式路」端到端可行接缝形态验证
*
* 【受控面铁律】零引擎 import能力一律经 boot.ctxgetEngine 可能为 null须容错render 只画到入参 g(=boot.mainContext)
* 输入坐标 = 逻辑像素 x∈[0,390]/y∈[0,844];随机/时钟走 ctx.random/ctx.time禁 Math.random/Date.now
*/
const VIEWPORT_W = 390;
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>"。
* field ∈ state 数值字段(如 scoreop ∈ >= > == <= <rhs = 数字字面量 或 "config.<key>"。
* @returns {boolean} 解析失败/字段缺失 → false不抛胜负条件保守不触发
*/
function evalCondition(cond, state, config) {
if (typeof cond !== 'string') return false;
const parts = cond.trim().split(/\s+/);
if (parts.length !== 3) return false;
const [lhs, op, rhsRaw] = parts;
const lhsVal = state[lhs];
if (typeof lhsVal !== 'number') return false;
const rhsVal = rhsRaw.startsWith('config.') ? config[rhsRaw.slice('config.'.length)] : Number(rhsRaw);
if (typeof rhsVal !== 'number' || Number.isNaN(rhsVal)) return false;
switch (op) {
case '>=': return lhsVal >= rhsVal;
case '>': return lhsVal > rhsVal;
case '==': return lhsVal === rhsVal;
case '<=': return lhsVal <= rhsVal;
case '<': return lhsVal < rhsVal;
default: return false;
}
}
/**
* 适配 SourceProject或裸 gameDefinition→ GameHostFactory。
* @param {object} source SourceProject含 .gameDefinition/.config/.assets/.profile或裸 gameDefinition。
* @returns {(opts?: object) => import('../core/game-host.d.ts').GameInstance} GameHostFactory
*/
export function adaptGameDefinition(source) {
const src = source || {};
// 兼容:传整 SourceProject 取 .gameDefinition传裸 gameDefinition 直接用。
const gd = src.gameDefinition || src || {};
const config = src.config || gd.config || {};
const profile = src.profile || {};
const entities = Array.isArray(gd.entities) ? gd.entities : [];
const components = Array.isArray(gd.components) ? gd.components : [];
const scenes = Array.isArray(gd.scenes) ? gd.scenes : [];
const rules = Array.isArray(gd.rules) ? gd.rules : [];
const behaviors = Array.isArray(gd.behaviors) ? gd.behaviors : [];
const compById = new Map(components.map((c) => [c.id, c]));
// 激活场景 = 首个 scene无则全实体激活实体按 entityRefs 过滤。
const activeScene = scenes[0];
const activeIds = activeScene && Array.isArray(activeScene.entityRefs)
? new Set(activeScene.entityRefs)
: null;
const activeEntities = activeIds ? entities.filter((e) => activeIds.has(e.id)) : entities;
// 预解析每个实体的 render 组件(声明式画法)。
function renderCompOf(entity) {
const ids = Array.isArray(entity.components) ? entity.components : [];
for (const cid of ids) {
const c = compById.get(cid);
if (c && c.kind === 'render') return c;
}
return null;
}
return function createGame(/* opts */) {
let ctx = null;
let sub = null;
let nowMs = 0;
// 运行态:目标(可点击实体)+ 计分 + phasebooting|playing|gameover对齐 P0 latch 终态)。
/** @type {{id:string,x:number,y:number,w:number,h:number,color:string,hit:boolean}[]} */
let targets = [];
const state = { phase: 'booting', score: 0, result: null, t: 0 };
/** init建目标、订阅受控输入、置 playing。 */
function init(boot) {
ctx = boot.ctx;
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',
hit: false,
};
});
sub = ctx.getInput().on('pointerdown', onTap); // 受控订阅(禁 addEventListener
state.phase = 'playing';
}
/** 输入:解释 trigger=input 的 behavior本切片支持 op=hit-nearest-target。 */
function onTap(e) {
if (state.phase !== 'playing') return; // latch终态后输入无效
nowMs = ctx.time.nowMs();
const px = e.x || 0, py = e.y || 0; // 已是逻辑像素,勿再乘视口
for (const b of behaviors) {
if (b.trigger !== 'input' || !SUPPORTED_INPUT_OPS.has(b.op)) continue;
if (b.op === 'hit-nearest-target') {
const radius = typeof config.hitRadius === 'number' ? config.hitRadius : 60;
let best = null, bestD2 = radius * radius;
for (const tg of targets) {
if (tg.hit) continue;
const cx = tg.x + tg.w / 2, cy = tg.y + tg.h / 2;
const d2 = (cx - px) * (cx - px) + (cy - py) * (cy - py);
if (d2 <= bestD2) { best = tg; bestD2 = d2; }
}
if (best) {
best.hit = true;
state.score += 1;
// 命中反馈(game-feel + 满足九门 F 真接线):引擎粒子+音效,经 ctx.getEngine 容错(null 不连坐)。
const eng = ctx.getEngine && ctx.getEngine();
if (eng) {
const cx = best.x + best.w / 2, cy = best.y + best.h / 2;
if (eng.particles) {
try { eng.particles.spawnEmitter({ pos: { x: cx, y: cy }, count: 20, speed: 120, particleTime: 0.5,
colorStart: { r: 1, g: 0.8, b: 0.2, a: 1 }, colorEnd: { r: 1, g: 0.3, b: 0, a: 0 } }); } catch (_) { /* 引擎缺省不连坐 */ }
}
if (eng.audio && eng.audio.synth) {
try { eng.audio.synth.synthSfx([1, 0.05, 260, , , 0.15, , 1.3]); } catch (_) { /* 同上 */ }
}
}
}
}
}
}
/** update刷钟 + 评估 rules胜负 → phase=gameover + 终态驻留)。 */
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') {
state.phase = 'gameover';
state.result = r.outcome;
break;
}
}
}
}
/** render画到入参 g(=mainContext)。满幅底(D 门)+ 目标(命中变暗)+ 计分 HUD + 时间脉冲(E 门活性)+ 终态字。 */
function render(g) {
g.fillStyle = '#0d1117';
g.fillRect(0, 0, VIEWPORT_W, VIEWPORT_H);
// 时间脉冲条保证无输入时帧间也有变化E 门活性,走 ctx.time 非 Date.now
const pulse = 0.5 + 0.5 * Math.sin(state.t * 2);
g.fillStyle = 'rgba(80,160,255,' + (0.25 + 0.5 * pulse).toFixed(3) + ')';
g.fillRect(0, 0, VIEWPORT_W, 6);
for (const tg of targets) {
g.fillStyle = tg.hit ? '#2a3340' : tg.color;
g.fillRect(tg.x, tg.y, tg.w, tg.h);
}
const winScore = typeof config.winScore === 'number' ? config.winScore : targets.length;
g.fillStyle = '#cdd9e5';
g.font = '20px monospace';
g.fillText('Score: ' + state.score + '/' + winScore, 20, 40);
if (state.phase === 'gameover') {
g.fillStyle = state.result === 'win' ? '#4ade80' : '#f87171';
g.font = '32px monospace';
g.fillText(state.result === 'win' ? 'YOU WIN' : 'GAME OVER', 110, 430);
}
}
/** 取证视图:契约 = { seed?, state:()=>snapshot }boot-game-host.js:283 调 `_forensicsView().state()`;参照 tictactoe
* harness 九门H 进展/latch+ tap-targets driver 经 host.state() 读 snapshot。 */
function _forensicsView() {
return {
seed: 1,
state: () => {
const ws = typeof config.winScore === 'number' ? config.winScore : targets.length;
return {
phase: state.phase,
score: state.score,
result: state.result,
progress: ws ? state.score / ws : 0,
tickModel: profile.tickModel || null,
// tap-targets driver 读 targets[{idx,x,y,occupied}] 自适应出招(逻辑坐标=中心点)。
targets: targets.map((tg, idx) => ({
idx, x: tg.x + tg.w / 2, y: tg.y + tg.h / 2, occupied: tg.hit,
})),
};
},
};
}
/** destroy幂等卸载取消订阅。 */
function destroy() {
try { sub && sub.cancel(); } catch (_) { /* 幂等 */ }
sub = null;
targets = [];
}
return { init, update, render, _forensicsView, destroy };
};
}