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:
parent
fd022821ae
commit
046454d95e
34
game-runtime/games/_wg1-gen/_adapter-probe/bundle.iife.js
Normal file
34
game-runtime/games/_wg1-gen/_adapter-probe/bundle.iife.js
Normal file
File diff suppressed because one or more lines are too long
@ -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}}}
|
||||
77
game-runtime/games/_wg1-gen/_adapter-probe/entry-bundle.js
Normal file
77
game-runtime/games/_wg1-gen/_adapter-probe/entry-bundle.js
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* entry-bundle.template.js —— W-G1 通用 bundle 入口模板(逐字拷进每个生成 game 目录为 entry-bundle.js)
|
||||
* owner:W-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-op,feed 照常(一份 bundle 同时服务 feed + 取证门)。
|
||||
* - 引擎:Option C 动态 import(node import 本入口不触引擎,保 import-safe;esbuild 把引擎内联进单文件 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 通用装载壳 buildGenericHostConfig:collision/physics/gamefeel/juice/palettePost/audioMusic/save + 偏序 + 390×844 视口;零 wanglanmei 专属)。
|
||||
import { buildGenericHostConfig } from '../../../src/host/generic-host-config.js';
|
||||
// 本目录生成的游戏工厂(默认导出 GameHostFactory,零引擎 import,A3 硬独立文件;官方靶名 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 |
165
game-runtime/games/_wg1-gen/_adapter-probe/evidence/verdict.json
Normal file
165
game-runtime/games/_wg1-gen/_adapter-probe/evidence/verdict.json
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
95
game-runtime/games/_wg1-gen/_adapter-probe/index.html
Normal file
95
game-runtime/games/_wg1-gen/_adapter-probe/index.html
Normal file
@ -0,0 +1,95 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
index.template.html —— W-G1 通用 host 页模板(逐字拷进每个生成 game 目录为 index.html)
|
||||
owner:W-G1 lane | 用法:
|
||||
· 同目录 esbuild 产物 bundle.iife.js(globalName=__GameBundle)由本页 <script src> 加载;
|
||||
· serve game-runtime 根,CDP 导航到 /games/_wg1-gen/<game>/index.html;
|
||||
· 就绪标记 __genBooted(bootGameHost().then 置);real 引擎接管标记 __gameHostEngineInitFired(boot-game-host 内置);
|
||||
· 假绿守卫钩子:window.__engineCalls(recHook 记每次引擎能力调用 call-ID)、window.__genInternals(onReady 收 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:收集 internals(uncaught[] 等)供假绿守卫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>
|
||||
19
game-runtime/games/_wg1-gen/_adapter-probe/play-spec.json
Normal file
19
game-runtime/games/_wg1-gen/_adapter-probe/play-spec.json
Normal 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
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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,
|
||||
})),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user