feat(adapter): E1 build→九门端到端验证——_adapter-probe 真九门全过(score 0→3 latch✓)

U1 前置另一半(plan 002):声明式 gameDefinition → 适配器 → entry-bundle → build.mjs → 真九门绿。
- _adapter-probe/:generated-factory(adaptGameDefinition+内联 source)+ entry-bundle(拷模板)+ index(拷模板)
  + play-spec(tap-targets driver, assert score increased, expectLatch) + bundle.iife.js(211KB, LittleJS 真入包)。
- 真九门(本机快走查):A装载/B未捕获0/C掌帧Δ30/D真渲染/E活性5/5/F真接线(particles+audio)/
  G输入有效/H 机制+latch(score 0→3 + 终态驻留)/I(无 controlCheck);首局门 可玩51ms/首反馈/60s闭环7s 全过。
- 适配器跟进:① _forensicsView 改契约形态 { seed, state:()=>snapshot }(对齐 boot-game-host:283 + tictactoe);
  ② 命中加引擎粒子+音效反馈(game-feel + 满足 F 门),经 ctx.getEngine 容错。单测仍 6/6 绿。

证成:E1(repo 标"真空白"最高风险件)声明式路端到端可行;build-from-sourceProject 管线(E2)可行;
回答 adversarial MVP-latch 质疑——单 behavior 声明式游戏确过 H 门 latch。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-17 13:46:17 -07:00
parent fd022821ae
commit 046454d95e
11 changed files with 467 additions and 16 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"inputs":{"src/core/plugin.js":{"bytes":44317,"imports":[],"format":"esm"},"src/plugins/runtime-probe/impl.js":{"bytes":16639,"imports":[],"format":"esm"},"host-dev/engine-caps.js":{"bytes":13897,"imports":[],"format":"esm"},"host-dev/engine-math.js":{"bytes":6155,"imports":[],"format":"esm"},"src/host/boot-game-host.js":{"bytes":28239,"imports":[{"path":"src/core/plugin.js","kind":"import-statement","original":"../core/plugin.js"},{"path":"src/plugins/runtime-probe/impl.js","kind":"import-statement","original":"../plugins/runtime-probe/impl.js"},{"path":"host-dev/engine-caps.js","kind":"import-statement","original":"../../host-dev/engine-caps.js"},{"path":"host-dev/engine-math.js","kind":"import-statement","original":"../../host-dev/engine-math.js"}],"format":"esm"},"src/plugins/collision/impl.js":{"bytes":24812,"imports":[],"format":"esm"},"src/plugins/physics-lite/impl.js":{"bytes":14507,"imports":[],"format":"esm"},"src/plugins/gamefeel/impl.js":{"bytes":21715,"imports":[],"format":"esm"},"src/plugins/particles-juice/impl.js":{"bytes":31830,"imports":[],"format":"esm"},"src/plugins/palette-post/impl.js":{"bytes":20240,"imports":[],"format":"esm"},"src/plugins/audio-music/impl.js":{"bytes":24659,"imports":[],"format":"esm"},"src/plugins/save-progress/impl.js":{"bytes":14608,"imports":[],"format":"esm"},"src/host/generic-host-config.js":{"bytes":7803,"imports":[{"path":"src/plugins/collision/impl.js","kind":"import-statement","original":"../plugins/collision/impl.js"},{"path":"src/plugins/physics-lite/impl.js","kind":"import-statement","original":"../plugins/physics-lite/impl.js"},{"path":"src/plugins/gamefeel/impl.js","kind":"import-statement","original":"../plugins/gamefeel/impl.js"},{"path":"src/plugins/particles-juice/impl.js","kind":"import-statement","original":"../plugins/particles-juice/impl.js"},{"path":"src/plugins/palette-post/impl.js","kind":"import-statement","original":"../plugins/palette-post/impl.js"},{"path":"src/plugins/audio-music/impl.js","kind":"import-statement","original":"../plugins/audio-music/impl.js"},{"path":"src/plugins/save-progress/impl.js","kind":"import-statement","original":"../plugins/save-progress/impl.js"}],"format":"esm"},"src/adapter/create-game-from-definition.js":{"bytes":10000,"imports":[],"format":"esm"},"games/_wg1-gen/_adapter-probe/generated-factory.js":{"bytes":1616,"imports":[{"path":"src/adapter/create-game-from-definition.js","kind":"import-statement","original":"../../../src/adapter/create-game-from-definition.js"}],"format":"esm"},"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytes":640153,"imports":[],"format":"esm"},"games/_wg1-gen/_adapter-probe/entry-bundle.js":{"bytes":6230,"imports":[{"path":"src/host/boot-game-host.js","kind":"import-statement","original":"../../../src/host/boot-game-host.js"},{"path":"src/host/generic-host-config.js","kind":"import-statement","original":"../../../src/host/generic-host-config.js"},{"path":"games/_wg1-gen/_adapter-probe/generated-factory.js","kind":"import-statement","original":"./generated-factory.js"},{"path":"node_modules/littlejsengine/dist/littlejs.esm.js","kind":"dynamic-import","original":"littlejsengine"}],"format":"esm"}},"outputs":{"games/_wg1-gen/_adapter-probe/bundle.iife.js":{"imports":[],"exports":[],"entryPoint":"games/_wg1-gen/_adapter-probe/entry-bundle.js","inputs":{"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytesInOutput":164138},"games/_wg1-gen/_adapter-probe/entry-bundle.js":{"bytesInOutput":507},"src/core/plugin.js":{"bytesInOutput":8989},"src/plugins/runtime-probe/impl.js":{"bytesInOutput":2794},"host-dev/engine-caps.js":{"bytesInOutput":1597},"host-dev/engine-math.js":{"bytesInOutput":498},"src/host/boot-game-host.js":{"bytesInOutput":5502},"src/plugins/collision/impl.js":{"bytesInOutput":1605},"src/plugins/physics-lite/impl.js":{"bytesInOutput":115},"src/plugins/gamefeel/impl.js":{"bytesInOutput":2533},"src/plugins/particles-juice/impl.js":{"bytesInOutput":4164},"src/plugins/palette-post/impl.js":{"bytesInOutput":4216},"src/plugins/audio-music/impl.js":{"bytesInOutput":6001},"src/plugins/save-progress/impl.js":{"bytesInOutput":3933},"src/host/generic-host-config.js":{"bytesInOutput":697},"src/adapter/create-game-from-definition.js":{"bytesInOutput":3237},"games/_wg1-gen/_adapter-probe/generated-factory.js":{"bytesInOutput":728}},"bytes":211825}}}

View File

@ -0,0 +1,77 @@
/**
* entry-bundle.template.js W-G1 通用 bundle 入口模板逐字拷进每个生成 game 目录为 entry-bundle.js
* ownerW-G1 lane 消费方esbuild --global-name=__GameBundle P1 宿主 bootGameHost 真玩
*
*
* 职责生成的游戏工厂 + 通用宿主 + 标准 7 插件 wiring + 引擎汇成一个 iife 全局 __GameBundle
* 产出靶契约冻结·逐字对齐 entry-bundle.js / game-package.schema.json engineBundle
* iife 顶层全局名固定 **__GameBundle**宿主从 window.__GameBundle GameHostFactory bootGameHost
*
* 最大化复用·最小新代码
* - 宿主复用 src/host/boot-game-host.js 的通用 bootGameHost不改
* - 插件 wiring用官方 L0 通用装载壳 src/host/generic-host-config.js buildGenericHostConfig标准 7 插件 + 偏序 + 视口 + 工厂 wiring与官方 entry-generic.js 同一份
* override factory 为本目录生成的工厂 生成游戏跑在官方 generic 标准装配
* - 本入口 = 官方 entry-generic.js harness 可取证超集多透传 recHook/onReady CDP 八门F门真接线
* studio 渠道调 bootGameHost({canvas,seed}) recHook 为空=no-opfeed 照常一份 bundle 同时服务 feed + 取证门
* - 引擎Option C 动态 importnode import 本入口不触引擎 import-safeesbuild 把引擎内联进单文件 iife
*
* Q4 铁律本入口是集成段 host唯一引擎 import 落点动态 import 后经 opts.engine 注入
* ./factory.js 游戏工厂零引擎 import能力一律经 boot.ctx.getEngine()
*
*/
'use strict';
// 通用宿主(引擎骨架参数化;本身零引擎 import引擎经 opts.engine 注入)。
import { bootGameHost as bootGameHostCore } from '../../../src/host/boot-game-host.js';
// 标准插件装配(官方 L0 通用装载壳 buildGenericHostConfigcollision/physics/gamefeel/juice/palettePost/audioMusic/save + 偏序 + 390×844 视口;零 wanglanmei 专属)。
import { buildGenericHostConfig } from '../../../src/host/generic-host-config.js';
// 本目录生成的游戏工厂(默认导出 GameHostFactory零引擎 importA3 硬独立文件;官方靶名 generated-factory.js
import gameFactory from './generated-factory.js';
/** 【契约导出】GameHostFactory宿主从 window.__GameBundle.GameHostFactory 取它(契约冻结)。= 本目录生成的工厂。 */
export const GameHostFactory = gameFactory;
// 默认导出同为 GameHostFactory便于 import default 取用;与具名导出同一引用)。
export default gameFactory;
/**
* P2/W-G1 boot 入口 host 页在浏览器内启动本 bundle引擎由本 bundle 注入后驱动
* - 动态 import 引擎Option C调用时才求值esbuild 已把引擎内联进本 bundle无外部请求
* - engineMode 强制 'real'W-G1 真玩取证一律引擎掌帧
* - 插件/视口/工厂 wiring 取自 buildRefHostConfig ref 同一标准集factory override 为生成工厂
* - recHook / onReady 透传供取证 harness 注入引擎 call-ID 探针internals(uncaught) 收集假绿守卫
*
* @param {Object} opts host 页传入入参
* @param {HTMLCanvasElement} opts.canvas 页面 #game 画布real 通道引擎自建 mainCanvas#game-engine canvas 作占位/输入兜底
* @param {HTMLElement} [opts.statusEl] 状态行可空
* @param {number} [opts.seed] 主随机种子缺省固定值保确定性
* @param {Function} [opts.recHook] 引擎 call-ID 取证钩子 id=>void取证路径注入 window.__engineCalls.push渠道产物不注
* @param {Function} [opts.onReady] (host, internals)=>void 就绪回调取证收集 internals.uncaught
* @param {any} [opts.engine] 覆盖引擎名字空间缺省动态 import littlejsengine
* @returns {Promise<object>} bootGameHost host 句柄frameCount/stepFrames/tap/do/state
*/
export async function bootGameHost(opts) {
const o = opts || {};
// 动态 import 引擎求值时机延后到本函数被调用浏览器运行期。esbuild 内联,无外部请求。
const engine = o.engine || (await import('littlejsengine'));
// 主随机种子(缺省固定值,与 ref 取证同口径,保确定性可复现)。
const seed = (o.seed != null ? o.seed : 0x1234abcd) >>> 0;
// 官方 generic 标准装配7 插件 + 偏序 + 视口 + 工厂 wiring与 entry-generic.js 同一 buildGenericHostConfig
const cfg = buildGenericHostConfig({ seed });
// 调通用宿主real 通道引擎掌帧factory override 为本目录生成工厂;其余 wiring 取标准集。
return bootGameHostCore({
canvas: o.canvas,
statusEl: o.statusEl || null,
engine,
seed,
mode: o.mode || 'play', // W-G1 真玩=play
engineMode: 'real', // 一律引擎掌帧real 通道)
viewport: cfg.viewport, // 390×844与 ref 同,移动竖屏基准)
factory: gameFactory, // ★ override本目录【生成的游戏】其余 wiring 全复用 ref
plugins: cfg.plugins, // 标准 7 插件实例集(经 opts.runtime.plugins 注入给工厂)
registerOrder: cfg.registerOrder, // 注册偏序(保六锚)
buildFactoryOpts: cfg.buildFactoryOpts, // 注入 runtime:{plugins,bundle,getRealAudioCtx}
recHook: o.recHook || null, // 取证:引擎 call-ID 探针(默认 no-op
onReady: o.onReady || null, // 取证:收集 internals(uncaught)
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,165 @@
{
"gameId": "_adapter-probe",
"url": "http://localhost:4320/games/_wg1-gen/_adapter-probe/index.html",
"ts": null,
"guards": {
"A_boot": {
"pass": true
},
"C_frame": {
"pass": true,
"f0": 55,
"f1": 85,
"delta": 30
},
"I_control": {
"pass": true,
"skipped": "未声明 controlCheck向后兼容"
},
"D_render": {
"pass": true,
"bright": 60694,
"maxCh": 255
},
"E_live": {
"pass": true,
"distinctStates": 5,
"samples": 5
},
"B_uncaught": {
"pass": true,
"uncaught": []
},
"F_wiring": {
"pass": true,
"callCount": 6,
"sample": [
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx"
],
"expected": [
"particles.",
"audio.synth"
]
},
"G_input": {
"pass": true,
"inputHash": "941173d7",
"controlHash": "57c7e2dc",
"atFrame": 214,
"ctrlFrame": 215
},
"H_progress": {
"pass": true,
"checks": [
{
"path": "score",
"op": "increased",
"before": 0,
"after": 3,
"pass": true,
"why": "适配性真玩(自动点目标)→命中计分上升;点击失灵/_forensicsView 没更新则不升"
}
],
"latch": {
"pass": true,
"dwelledMs": 600,
"after": "gameover",
"reason": ""
},
"state0": {
"phase": "playing",
"score": 0,
"result": null,
"progress": 0,
"tickModel": "event",
"targets": [
{
"idx": 0,
"x": 112,
"y": 232,
"occupied": false
},
{
"idx": 1,
"x": 202,
"y": 452,
"occupied": false
},
{
"idx": 2,
"x": 292,
"y": 652,
"occupied": false
}
]
},
"state1": {
"phase": "gameover",
"score": 3,
"result": "win",
"progress": 1,
"tickModel": "event",
"targets": [
{
"idx": 0,
"x": 112,
"y": 232,
"occupied": true
},
{
"idx": 1,
"x": 202,
"y": 452,
"occupied": true
},
{
"idx": 2,
"x": 292,
"y": 652,
"occupied": true
}
]
}
}
},
"pass": true,
"notes": [],
"firstPlay": {
"enabled": true,
"categoryDerived": "placement",
"playableMs": 51,
"playableThresholdMs": 2000,
"warmupMs": 188,
"firstFeedback": true,
"firstMove": {
"kind": "tap",
"x": 112,
"y": 232
},
"notes": [
"playableMs=预热后 warm 稳态,不覆盖 bundle 首加载退化;warmupMs 记冷启;真机 first-load 阈值校准留 staging(P2)。本门 v0 守得住卡死/装载失败退化,守不住大 bundle/慢首加载退化。"
],
"playableOk": true,
"playableScope": "warm",
"firstFeedbackDetail": {
"method": "single-trip",
"stateChanged": true,
"frameChanged": true,
"category": "placement",
"frameIsCausal": true,
"phase0": "playing",
"phase1": "playing",
"score0": 0,
"score1": 1
},
"coreLoopReached": true,
"coreLoopElapsedMs": 7026,
"coreLoopThresholdMs": 60000,
"pass": true
}
}

View File

@ -0,0 +1,37 @@
'use strict';
/**
* generated-factory.js E1 适配器探针U1 build九门 验证件,非生产游戏)
* owner前端+引擎线Mac计划 002 U1声明式 gameDefinition 经适配器 entry-bundle build.mjs 九门真玩端到端
*
* 默认导出 GameHostFactory = adaptGameDefinition(内联 SourceProject)零引擎 import适配器经 boot.ctx.getEngine
* 内联 source src/adapter/fixtures/tap-target.source.json 保证 esbuild 自包含打包
*/
import { adaptGameDefinition } from '../../../src/adapter/create-game-from-definition.js';
const SOURCE = {
schemaVersion: '1.0',
sourceHash: '0000000000000000000000000000000000000000000000000000000000000000',
profile: { tickModel: 'event', inputModel: 'discrete-choice', progressModel: 'metric' },
gameDefinition: {
entities: [
{ id: 't1', transform: { position: { x: 80, y: 200 } }, components: ['r-target'] },
{ id: 't2', transform: { position: { x: 170, y: 420 } }, components: ['r-target'] },
{ id: 't3', transform: { position: { x: 260, y: 620 } }, components: ['r-target'] },
],
components: [
{ id: 'r-target', kind: 'render', shape: 'rect', w: 64, h: 64, color: '#ffcc33' },
],
behaviors: [
{ id: 'b-tap', trigger: 'input', op: 'hit-nearest-target' },
],
scenes: [
{ id: 's1', entityRefs: ['t1', 't2', 't3'] },
],
rules: [
{ id: 'win', condition: 'score >= config.winScore', outcome: 'win' },
],
},
config: { winScore: 3, hitRadius: 60 },
};
export default adaptGameDefinition(SOURCE);

View File

@ -0,0 +1,95 @@
<!DOCTYPE html>
<!--
index.template.html —— W-G1 通用 host 页模板(逐字拷进每个生成 game 目录为 index.html
ownerW-G1 lane 用法:
· 同目录 esbuild 产物 bundle.iife.jsglobalName=__GameBundle由本页 <script src>
· serve game-runtime 根CDP 导航到 /games/_wg1-gen/<game>/index.html
· 就绪标记 __genBootedbootGameHost().then 置real 引擎接管标记 __gameHostEngineInitFiredboot-game-host 内置);
· 假绿守卫钩子window.__engineCallsrecHook 记每次引擎能力调用 call-ID、window.__genInternalsonReady 收 uncaught[])。
布局收编(对齐 ref 教训canvas 顶在 (0,0) 满幅 390×844状态行 absolute overlay + pointer-events:none
绝不下推/截触摸(否则 CDP 真输入坐标整体偏移 + 底部不可点)。引擎自建 mainCanvas#game-engine 叠加渲染。
-->
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<!-- 固定取证视口390×844 移动竖屏基准user-scalable=no 防缩放扰动像素哈希。 -->
<meta name="viewport" content="width=390, height=844, initial-scale=1, user-scalable=no" />
<title>W-G1 generated game</title>
<style>
html, body { margin: 0; padding: 0; background: #111; color: #ccc;
font-family: -apple-system, "PingFang SC", monospace; overflow: hidden; }
#wrap { position: relative; width: 390px; margin: 0 auto; }
#game { width: 390px; height: 844px; background: #000; display: block; touch-action: none; }
/* 状态行默认隐藏boot-game-host 的「engine RAF running…」调试浮层会被截图带进画面、污染 player 视觉判分;
harness 真玩门读 JS 变量(__genBooted/__genBootError/console)判定,不依赖此可视行,故隐藏不影响七门。
仅 BOOT ERROR 时由下方脚本显式显示,便于人工排查。 */
#status { display: none; position: absolute; top: 0; left: 0; right: 0; z-index: 2; pointer-events: none;
font-size: 12px; line-height: 1.4; min-height: 15px; text-align: center; word-break: break-all;
opacity: 0.85; text-shadow: 0 1px 2px rgba(0,0,0,0.8); }
</style>
</head>
<body>
<div id="wrap">
<div id="status">booting…</div>
<!-- 受控画布opts.canvas 占位/输入兜底real 引擎自建 #game-engine 叠加。 -->
<canvas id="game"></canvas>
</div>
<!-- esbuild 产物iife 全局 __GameBundle含 GameHostFactory + bootGameHost-->
<script src="./bundle.iife.js"></script>
<script>
// 引导:取 __GameBundle.bootGameHost 启动;注入取证钩子;失败留痕 + 重抛(假绿守卫要看 console 零未捕获)。
(function () {
'use strict';
var statusEl = document.getElementById('status');
var canvas = document.getElementById('game');
// 假绿守卫C·真接线探针每次引擎能力被调recHook 记一条 call-ID 进此数组(证「真调引擎/插件库」非自绘伪装)。
window.__engineCalls = [];
function recHook(id) { try { window.__engineCalls.push(id); } catch (_) {} }
// 就绪/错误标记初值。
window.__genBooted = false;
window.__genBootError = null;
// W-G1 组C·首局门断言①「可玩≤2s」精确计时页面侧 performance.now() 在「可交互首帧」(__genBooted=true) 打点,
// 写 window.__playableAtMs= 自 navigationStart/timeOrigin 起的毫秒。harness 直接读它,
// 规避 ① CDP 轮询 ~300ms 粒度抖动 ② waitBoot 固定 settle floor 对计时的污染——二者都会让 harness 侧计时失真。
window.__playableAtMs = null;
try {
var G = window.__GameBundle;
if (!G || typeof G.bootGameHost !== 'function') {
throw new Error('__GameBundle.bootGameHost 未找到bundle.iife.js 未正确加载或 globalName 不符)');
}
G.bootGameHost({
canvas: canvas,
statusEl: statusEl,
recHook: recHook,
// onReady收集 internalsuncaught[] 等供假绿守卫B 读。
onReady: function (host, internals) {
window.__genHost = host;
window.__genInternals = internals || null;
}
})
.then(function (host) {
window.__genHost = window.__genHost || host;
// 可测性别名守卫H 经此读游戏可观测状态host.state() 透传 game._forensicsView().state游戏未实现则返回 {phase:'booting'})。
// 宿主侧挂载,工厂仍零 window与 ref 在 onReady 挂 __refForensics 同范式。
window.__gameState = function () { try { return host && typeof host.state === 'function' ? host.state() : null; } catch (_) { return null; } };
// 首局门断言①打点可交互首帧达成的墙钟performance.now() 以 navigationStart 为 0 点),供 harness 读取判 ≤2s。
try { window.__playableAtMs = (typeof performance !== 'undefined' && performance.now) ? performance.now() : null; } catch (_) {}
window.__genBooted = true; // CDP 探活标记区分「脚本没加载」与「boot 抛错」)
})
.catch(function (e) {
window.__genBooted = false;
window.__genBootError = String((e && e.message) || e);
if (statusEl) { statusEl.style.display = 'block'; statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError; }
throw e; // 重抛 → 'unhandledrejection' 被假绿守卫逮住
});
} catch (e) {
window.__genBooted = false;
window.__genBootError = String((e && e.message) || e);
if (statusEl) statusEl.textContent = 'BOOT ERROR: ' + window.__genBootError;
throw e;
}
})();
</script>
</body>
</html>

View File

@ -0,0 +1,19 @@
{
"driver": {
"type": "tap-targets",
"steps": 12,
"stepMs": 300
},
"expectedEngineCallPrefixes": [
"particles.",
"audio.synth"
],
"assertAfterPlay": [
{
"path": "score",
"op": "increased",
"why": "适配性真玩(自动点目标)→命中计分上升;点击失灵/_forensicsView 没更新则不升"
}
],
"expectLatch": true
}

View File

@ -56,7 +56,7 @@ test('init → playing, 目标建好, score=0', () => {
const game = adaptGameDefinition(SOURCE)();
const { boot } = makeBoot();
game.init(boot);
const v = game._forensicsView();
const v = game._forensicsView().state();
assert.equal(v.phase, 'playing');
assert.equal(v.score, 0);
assert.equal(v.targets.length, 3);
@ -80,7 +80,7 @@ test('tap 命中目标 → score++ + occupied(输入因果 G/H)', () => {
game.init(boot);
const c0 = center(0);
inputBridge._emit('pointerdown', { x: c0.x, y: c0.y });
const v = game._forensicsView();
const v = game._forensicsView().state();
assert.equal(v.score, 1);
assert.equal(v.targets[0].occupied, true);
});
@ -90,7 +90,7 @@ test('tap 落空(远离所有目标) → 不计分', () => {
const { boot, inputBridge } = makeBoot();
game.init(boot);
inputBridge._emit('pointerdown', { x: 5, y: 820 });
assert.equal(game._forensicsView().score, 0);
assert.equal(game._forensicsView().state().score, 0);
});
test('命中全部 → rule win → phase=gameover + latch 终态驻留', () => {
@ -99,7 +99,7 @@ test('命中全部 → rule win → phase=gameover + latch 终态驻留', () =>
game.init(boot);
for (let i = 0; i < 3; i++) { const c = center(i); inputBridge._emit('pointerdown', { x: c.x, y: c.y }); }
game.update(0.016); // 规则在 update 评估
let v = game._forensicsView();
let v = game._forensicsView().state();
assert.equal(v.score, 3);
assert.equal(v.phase, 'gameover');
assert.equal(v.result, 'win');
@ -107,7 +107,7 @@ test('命中全部 → rule win → phase=gameover + latch 终态驻留', () =>
const c0 = center(0);
inputBridge._emit('pointerdown', { x: c0.x, y: c0.y });
game.update(0.016);
v = game._forensicsView();
v = game._forensicsView().state();
assert.equal(v.phase, 'gameover');
assert.equal(v.score, 3);
});
@ -118,5 +118,5 @@ test('destroy 幂等', () => {
game.init(boot);
game.destroy();
game.destroy();
assert.equal(game._forensicsView().targets.length, 0);
assert.equal(game._forensicsView().state().targets.length, 0);
});

View File

@ -124,7 +124,22 @@ export function adaptGameDefinition(source) {
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; }
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 (_) { /* 同上 */ }
}
}
}
}
}
}
@ -167,17 +182,25 @@ export function adaptGameDefinition(source) {
}
}
/** 取证视图harness 九门H 进展/latch、tap-targets driver读它。 */
/** = { seed?, state:()=>snapshot }boot-game-host.js:283 `_forensicsView().state()` tictactoe
* harness 九门H 进展/latch+ tap-targets driver host.state() snapshot */
function _forensicsView() {
return {
phase: state.phase,
score: state.score,
result: state.result,
tickModel: profile.tickModel || null,
// tap-targets driver 读 targets[{x,y,idx,occupied}] 自适应出招(逻辑坐标=中心点)。
targets: targets.map((tg, idx) => ({
x: tg.x + tg.w / 2, y: tg.y + tg.h / 2, idx, occupied: tg.hit,
})),
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,
})),
};
},
};
}