games-development-ai/docs/agent-specs/2026-06-13-P1-王蓝莓上引擎-execution.md
zizi 99b6c82dec feat(runner-v2): P1 王蓝莓上引擎=游戏宿主装载契约 spike 实(mini-desktop real 引擎 5 门全过)
Runner v2 全弧第一相(HJ-RUNNER-ARC-001 / P1):用王蓝莓把"上层游戏宿主装载契约"
从零 spike 到"引擎真掌帧 + 游戏美术与引擎粒子同屏可玩",解锁 P2 泛化 / P3 生成。

契约与共享件
- 新增装载契约 game-runtime/src/core/game-host.d.ts(第9类 additive·暂名):
  GameHostBootContext / GameInstance / GameHostFactory;游戏=纯模块工厂,零引擎 import,
  能力经下层 ctx.getEngine()(受控面 6 项零改),绘制面 render(g) 的 g=引擎 mainContext
- makeEngineCaps 闭包提升为可 import 模块 host-dev/engine-caps.js(createEngineCaps):
  host-dev/ref/未来生成游戏共用;engineMode 短路 / __engineCalls 探针 / null-engine 语义逐行不变

王蓝莓 real 通道(games/wanglanmei-ref)
- 增 ?engine=real:主循环从浏览器 RAF 改挂 engineInit 五回调(掌帧唯一源=引擎);
  游戏美术绘制面从 #game 改引擎 mainContext(同屏合并);bundle 延后到 setupReal 内以
  mainContext 建(getContext2d 闭包快照修);保 ?engine=stub 逐像素确定性回滚
- game-shop.js 抽为独立 GameHostFactory(零引擎 import,P3 生成 agent 可复制范本)
- entry.js 动态 import('littlejsengine')=node-import-safe ∧ esbuild iife 入包两全
- 驱动 e2e-input.cdp.cjs real 改造:禁 stepFrames 手摇(防双掌帧),真触摸+轮询;
  门②同屏像素判据(暖色 warmHits ∧ 引擎粒子 brightHits 同张 #game-engine)

真机验收(mini-desktop real 引擎,5 门)
引擎掌帧 dFrame=31∈[20,45] / 同屏 warmHits=682∧brightHits=13028 同张 #game-engine /
particles.spawnEmitter+synthSfx call-ID 运行时真调引擎 / CDP 517 真触摸跑通整局
(第1天接待18·营收¥79.9·连击9) / console 零错;本机 88 单测全绿(含修复 4 红+补回归)
证据:games/wanglanmei-ref/evidence/p1-real/(e2e-evidence.json pass=true + 5 截图肉眼复核)

诚实 caveat:门② brightHits 在结算屏混入亮 UI,同屏合并由 warmHits(美术落引擎画布)+
call-ID(粒子真调引擎)+ 引擎架构(粒子渲同一 mainContext)三证成立,非阻断。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:32:50 +00:00

45 KiB
Raw Blame History

P1 · 王蓝莓上引擎spike 游戏宿主装载契约)· 执行版

状态execution 版 · 2026-06-13 · 给执行 agent 实现。配套 review 版 2026-06-13-P1-王蓝莓上引擎-review.md;母梁 2026-06-13-runner-v2-arc-review.md §5.1。 修订2026-06-13 对抗复审deep·全程读真源码→ 纳入 4 majorreal 就绪标记 / driver 范式切换 / 同屏暖色阈值坐实 / bundle 延后建)+ 5 项不明假设处置IDL 落点改 game-runtime/src·缺省分叉显式声明·game 工厂硬独立·部署改 git-clone-scratch·步骤0 降常规核对)。逐条裁决见 §12。 模型分流:一律 opus创始人拍前提(创始人已拍):装载契约 = 新立 additive 第 9 类契约(不污染 PluginContext);生命周期 = init/update/render/onInput + destroyboot 注入引擎 mainContext 给游戏层。 铁律:受控面 6 项契约(getContext2d/onFrame/getInput/getAudioContext/time/randomapi.d.ts:269/281/286/303零改;引擎 import 只活 entry/hostQ4区分 verified/inferred/assumption凡引代码事实带 file:line。 装载契约落点(复审改)IDL 落 game-runtime/src/core/game-host.d.ts(与 api.d.ts 同目录),contracts/——contracts/ 是跨三仓五工位的业务契约 SSOTAPI/DB/SDK/event…README §1装载契约是 game-runtime 内部「host↔游戏工厂」接线面消费方只在 game-runtimehost boot + 生成 agent且让跨仓 SSOT 反向相对 import ../game-runtime/src/* 违「契约即 SSOT」且 contracts 拆独立仓时即断README §1 明列拆仓计划)。落同目录后 import 方向自然(import type { PluginContext } from './api.d.ts')。详见 §12-A1。


1. 目标 / 范围边界

1.1 目标

wanglanmei-ref?engine=real 通道:① 引擎真掌帧(engineInit 五回调驱动游戏 update/render,废浏览器 RAF② 游戏美术与引擎粒子同屏可玩(游戏绘制面从 #game 2D 改引擎 mainContext,合并成一张画布);③ 人在 mini-desktop 浏览器真玩;④ 保 ?engine=stub 回滚(逐像素确定性)。同时把 makeEngineCapshost.js:197-318)提升为可 import 模块 host-dev/engine-caps.jshost-dev / ref 共用。

1.2 范围内

  • 新立装载契约工件 game-runtime/src/core/game-host.d.tsadditive 第 9 类契约;落 game-runtime/src contracts/,详见 §12-A1
  • host-dev/engine-caps.js 提升(搬 host.js:197-318 闭包为 createEngineCaps({LJS, engineMath, engineMode, recHook})host.js 改 import。
  • wanglanmei-ref real 通道改造:entry.js import 引擎、main.js 主循环挂引擎五回调、绘制面改 mainContextcreateHostDevContextengineFactorymockNowMs 改逻辑帧钟、RAF 门控。
  • ref 内拆出符合装载契约的 game 工厂(硬约束:独立文件 game-shop.js,默认导出 GameHostFactory,零引擎 import禁 main.js 内联糊过,详见步骤 3 + §12-A3
  • e2e-input.cdp.cjsengine=real 通道断言 + 同屏像素判据 + real 通道 driver 范式切换(真触摸+轮询,禁 stepFrames 手摇,详见步骤 8 + §12-M2
  • index.html__refBootedReal 就绪标记接线real 通道;详见步骤 4.3 + §12-M1
  • SIZES.md 增"游戏产物"口径。

1.3 范围外(非目标)

  • 受控面 6 项契约(零改)。
  • P2泛化宿主/装载任意 GamePackage→ 母 spec Phase DO2 已拍)。
  • P3生成游戏按契约产出→ W-G1 自身 spec。
  • 后端发布主链 / game-studio / feed / 生成管线。
  • 不接 OSS不改 feed 引擎选型。
  • 不动 stub 通道既有行为(逐行保旧)。

2. 前置

# 前置 状态 / 证据
P-1 A0 引擎掌帧地基已收口real 通道范式可用) verifiedhost.js:917 engineInit 五回调 + host.js:919 setupAfterEngine(mainContext)门0 8/83e5cab4
P-2 A1-A4 引擎能力包装已落地makeEngineCaps 已接线) verifiedhost.js:359 engineFactory: makeEngineCapsengine-math.js 已独立模块
P-3 mini-desktop 具备 esbuild + chrome6c6g 禁 chrome verifieda6-runbook.sh / ref-e2e-runbook.sh 均跑 mini-desktop
P-4 ref 现状49 单测绿 + e2e 四件套stub 基线) verifiedBUILD.md RESULT.txt 49/49
P-5 LittleJS 已是 ref devDep构建可引擎入包 inferredhost-dev 同仓共用 littlejsengine;执行首步须核 games/wanglanmei-ref 经 monorepo node_modules 可解析(与 host-dev entry 同口径)

3. 涉及文件路径

文件 动作 关键行锚
game-runtime/src/core/game-host.d.ts 新增 装载契约 IDL§5落 src/core 与 api.d.ts 同目录( contracts/§12-A1
contracts/README.md 改(索引指针) 不折进跨仓 8 类加一行指针「游戏宿主装载契约game-runtime 内部,落 game-runtime/src/core/game-host.d.ts§12-A1
host-dev/engine-caps.js 新增 createEngineCaps(搬 host.js:197-318
host-dev/host.js makeEngineCaps 闭包 → import createEngineCapshost.js:197-318 删,host.js:204/359 改)
games/wanglanmei-ref/entry.js entry.js:15import * as LJS from 'littlejsengine'
games/wanglanmei-ref/src/main.js real 通道) main.js:112engineFactoryreal 须延后到 engineInit().then 内建·§12-M4/617-661frame→game.update/render/648drawFrame g→mainContext/668-691RAF 门控)/618mockNowMs/583-602real 输入改挂 mainCanvas·§12-M4
games/wanglanmei-ref/src/game-shop.js 新增(硬约束·非建议) 把 main.js 游戏逻辑/绘制段封成装载契约 game 工厂;默认导出 GameHostFactory、零引擎 import步骤 3 + §12-A3
games/wanglanmei-ref/index.html real 通道 __refBootedReal 就绪标记接线§12-M1stub 仍用 __refBooted
games/wanglanmei-ref/test/e2e-input.cdp.cjs engine=real 通道rect 选择器切 #game-engine、waitRefBoot 等 __refBootedReal、real 范式=真触摸+轮询禁 stepFrames、同屏像素判据步骤 8 + §12-M1/M2/M3
games/wanglanmei-ref/test/ref-e2e-runbook.sh 改(或旁建 real 版) 沿 git-clone-scratch 口径ref-e2e-runbook.sh:20-21 tar-pipeURL 加 &engine=real;构建后 grep -q ParticleEmitter 核引擎入包§12-A4
SIZES.md 增"游戏产物=引擎+包装层"口径

4. 数据流

sequenceDiagram
  participant HTML as index.html
  participant Boot as bootRef(main.js)
  participant Caps as engine-caps.js
  participant LJS as littlejsengine
  participant Game as game(装载契约工厂)

  HTML->>Boot: bootRef({canvas,statusEl})
  alt engine=real
    Boot->>Boot: setGLEnable(false)+setDebugWatermark(false)+setCanvasClearColor(BLACK)
    Boot->>Caps: createEngineCaps({LJS,engineMath,engineMode:'real',recHook})
    Caps-->>Boot: engineFactoryparticles/audio/math 三面)
    Boot->>LJS: engineInit(gameInit,gameUpdate,...,gameRender,...)
    LJS-->>Boot: .then() → mainContext 就绪
    Boot->>Boot: createHostDevContext({context2d:mainContext, engineFactory, clock:逻辑帧钟})
    Boot->>Game: game.init(ctx, mainContext)
    loop 引擎内部 RAF
      LJS->>Game: gameUpdate → game.update(FIXED_DT)
      LJS->>Game: gameRender → game.render(mainContext)
      Game->>LJS: ctx.getEngine().particles.spawnEmitter粒子自渲落 mainContext
    end
  else engine=stub保旧
    Boot->>Boot: prepareCanvas(#game) 2D + createHostDevContext(无 engineFactory)
    Boot->>Boot: 浏览器 RAF + mockNowMs 墙钟 → frame(dtReal) → drawFrame(#game)
  end

关键数据流不变量

  • real掌帧唯一源=引擎(游戏不自起 RAF绘制面唯一=引擎 mainContext(游戏美术+引擎粒子同屏);插件能力经 ctx.getEngine()(下层已冻)。
  • real 时序硬约束(复审 M1/M4createHostDevContextbundlegame.init、输入桥挂 mainCanvas、就绪标记 __refBootedReal=true 全部在 engineInit().then(setupReal)(上图 .then() → mainContext 就绪 之后)——引擎掌帧的必然结构:引擎先起、mainContext/mainCanvas 才有;顶层提前建 bundle 会快照错 ctxhost.js:323-324)。
  • stub与今天 main.js 逐行等价(回滚锚);就绪标记仍用 index.html__refBootedindex.html:81)。

5. 装载契约 IDL新立 additive 第 9 类契约 · game-runtime/src/core/game-host.d.ts

设计原则:游戏 = 纯模块工厂(零引擎 importQ4。host 经 engineInit 五回调驱动游戏生命周期;游戏绘制面 render(g)g = 引擎 mainContextboot 注入);插件能力经下层已冻结 ctx.getEngine()(不在本契约重复定义)。api.d.ts 是 additive 关系:本契约只新增 GameHostFactory/GameInstance/GameHostBootContext 三个类型,受控面 6 项零改。 落点(复审改):落 game-runtime/src/core/game-host.d.ts(与 api.d.ts 同目录),故 import 方向自然=同目录 sibling ./api.d.ts落 contracts/(理由见 §12-A1

// game-runtime/src/core/game-host.d.ts —— 游戏宿主装载契约(第 9 类契约 · additive · game-runtime 内部)
// ownerRunner v2 P1  消费方:集成段 hostboot 游戏)+ 生成 agent产出靶
//
// 【为什么新立、不折进 api.d.ts】
//   api.d.ts 的 PluginContext 是「插件↔引擎」面getEngine() 等api.d.ts:303已冻结
//   本契约是「游戏整体↔引擎宿主」面——游戏的 update/render 主体不是插件、不走 PluginContext。
//   两者正交(边界模型「同一职不留两条并行路」):插件经 getEngine() 拿能力,游戏经本契约挂宿主。
//   引擎 import 只活集成段 hostQ4 铁律),游戏工厂零引擎 import。
// 【为什么落 game-runtime/src 而非 contracts/】
//   contracts/ 是跨三仓五工位的业务契约 SSOTAPI/DB/SDK/event…contracts/README.md §1
//   本契约是 game-runtime 内部「host↔游戏工厂」接线面消费方只在 game-runtime
//   故与 api.d.ts 同目录、import 方向顺;落 contracts 会让跨仓 SSOT 反向相对 import ../game-runtime/src拆仓即断
import type { PluginContext } from './api.d.ts';

/**
 * 游戏挂宿主时拿到的 boot 上下文。host 在游戏 init 时一次性注入。
 *   - ctx下层已冻结受控面PluginContext。游戏经 ctx.getEngine() 拿插件能力(粒子/音频/数学),
 *     经 ctx.getInput() 拿受控输入,经 ctx.random/time 拿受控随机/时钟。**不得绕过 ctx 触碰裸引擎/DOM。**
 *   - mainContext引擎 mainContextsetGLEnable(false) 后的纯 2D 绘制面)。游戏 render(g) 的 g 即它;
 *     游戏美术画在此面 → 与引擎粒子(引擎自渲落同一 mainContext同屏。**此字段是装载契约的核心**
 *     (游戏取引擎绘制面的唯一约定,不借 PluginContext.getContext2d——那是插件面 *   - seed主随机种子确定性canvas可见画布引用输入 rect 映射用real=引擎 mainCanvas */
export interface GameHostBootContext {
  ctx: PluginContext;                         // 下层已冻结受控面(复用)
  mainContext: CanvasRenderingContext2D;      // 引擎 mainContextboot 注入render 绘制面)
  canvas: HTMLCanvasElement;                  // 可见画布real=引擎 mainCanvas输入 rect 映射用)
  seed: number;                               // 主随机种子
}

/**
 * 游戏实例host 经引擎五回调驱动的稳定生命周期。
 *   生命周期对齐 engineInit 五回调host.js:883-887 engGameInit/Update/Render
 *     · init  ← gameInit引擎首帧前一次性建游戏对象、订阅 ctx.getInput()、装资产。
 *     · update(dt) ← gameUpdate引擎每逻辑步dt=FIXED_DT 恒定core.update/事件/特效计时。
 *     · render(g)  ← gameRender引擎清屏后g=mainContext画游戏美术引擎粒子已自渲落同一 g。
 *     · onInput(ev)(可选):归一化输入事件回调。**注**ref 经 ctx.getInput().on 订阅即可,
 *       onInput 是「不便用 ctx.getInput 的极简游戏」的备选入口二者择一ref 用前者。
 *     · destroy()卸载卫生P2 装载多游戏复位用P1 ref 单游戏可空实现但须声明)。
 */
export interface GameInstance {
  init(boot: GameHostBootContext): void | Promise<void>;  // 异步装资产ref loadAssets 异步)
  update(dt: number): void;                                // 逻辑步dt 秒,引擎恒定步长)
  render(g: CanvasRenderingContext2D): void;               // g=mainContextboot 注入的同一面)
  onInput?(ev: { type: string; x?: number; y?: number; key?: string }): void; // 可选输入入口
  destroy(): void;                                         // 卸载(幂等)
}

/**
 * 游戏工厂:纯模块导出(零引擎 import。host 调它建 GameInstance。
 *   生成 agent 的唯一产出靶 = 实现并默认导出一个 GameHostFactory。
 *   【硬约束·§12-A3】必须是「独立、可被 P3 生成 agent 复制的工厂模块」:
 *     · 独立文件默认导出ref=games/wanglanmei-ref/src/game-shop.js 的 `export default createShopGame`
 *     · 零引擎 import引擎能力一律经 boot.ctx.getEngine()
 *     · 禁在 main.js 内用内联闭包「形似实现」糊过——否则 P3 缺真实参照实现,违 P1「spike 装载契约到可被 P3 复制」目标。
 * @param opts 工厂选项(无玩法语义;预留 difficulty/variant 等 additive 扩展位)。
 */
export type GameHostFactory = (opts?: Record<string, unknown>) => GameInstance;

IDL 三条不可破约定(对齐边界模型)

  1. 掌帧唯一源=引擎GameInstance 不含任何 RAF/计时器;update/render 只被引擎五回调驱动。推论§12-M2:取证 driver 在 real 通道亦不得手摇 stepFrames(否则与引擎 RAF 双掌帧,破此条)——真触摸后等引擎自然推帧 + 轮询状态。
  2. 绘制面唯一=引擎 mainContextrender(g) 的 g 必为 boot 注入的 mainContext;游戏美术与引擎粒子同屏。推论§12-M4real 通道 bundle 必须在 engineInit().then 内、以 context2d=mainContext 建(受控面 getContext2d 一次性快照闭包捕获值,host.js:323-324);不得在顶层先建再改 ctx。
  3. 能力唯一面=下层 ctx.getEngine():粒子/音频/easing 经它(不在本契约重复定义引擎能力面)。

additive 关系明确game-host.d.tsimport type { PluginContext } from './api.d.ts',不修改 api.d.ts 任何导出;受控面 6 项零改。


6. 关键实现步骤(有序)

步骤 0核前置执行首步read-only · 常规核对)

  • mini-desktop scratch clone 后核 games/wanglanmei-ref/entry.jsimport * as LJS from 'littlejsengine' 能被 esbuild 解析(与 host-dev entry 同 monorepo node_modules口径复审降级·§12-A5:此项已被 host-dev 既有事实强力支撑(host-dev/entry.js 末行已 import * as LJS from 'littlejsengine' 且 host-dev real 门 6/6 已过,同 monorepo node_modules→ 不是可能阻断的高不确定项,作常规构建核对:构建 ref-bundle 后 grep -q ParticleEmitter dist/ref-bundle.js 即可(无需专门 /tmp dry-build 阻断门)。

步骤 1新立装载契约工件 game-runtime/src/core/game-host.d.ts§12-A1

  • 按 §5 IDL 落盘到 game-runtime/src/core/game-host.d.ts(与 api.d.ts 同目录import 方向=同目录 sibling ./api.d.ts
  • contracts/README.md 把它折进跨仓 8 类清单仅加一行指针登记「游戏宿主装载契约game-runtime 内部,落 game-runtime/src/core/game-host.d.ts),消费方=host boot + 生成 agent」理由 §12-A1
  • 验证tsc --noEmit(若仓有 ts 校验)或 node 类型注释自洽;确认只 import type { PluginContext } from './api.d.ts'、不改 api.d.ts 任何导出diff 证 api.d.ts 零改)。

步骤 2提升 engine-caps.js搬运已有件语义零变

2.1 新建 host-dev/engine-caps.js,导出 createEngineCaps({ LJS, engineMath, engineMode, recHook })

  • host.js:197-318 makeEngineCaps 闭包体整体搬入,逐行保语义
    • 首行 if (engineMode !== 'real') return null;(保 null-engine 回归短路,host.js:198)。
    • rec 改为参数注入:const rec = recHook || (() => {});默认 no-op;原闭包内 window.__engineCalls.push 语义由调用方经 recHook 提供)。
    • mathRaw = engineMath(原 buildEngineMath(LJS) 的结果由调用方传入,或 createEngineCaps 内部 buildEngineMath(LJS)——二选一,保 host.js:204 同口径)。
    • particles/audio.synth 三面包装体(host.js:214-316)逐字搬入,引擎引用经入参 LJS.*。 2.2 host.js 改:删内联 makeEngineCaps197-318),改 import { createEngineCaps } from './engine-caps.js';在 buildBundlehost.js:353)处 engineFactory: () => createEngineCaps({ LJS, engineMath: buildEngineMath(LJS), engineMode, recHook: id => { try { (window.__engineCalls||(window.__engineCalls=[])).push(id); } catch(_){} } })
  • 关键host-dev 仍注入 rec门② 探针在 host-dev 取证产物里有符合现状——host-dev 不是渠道产物)。 2.3 验证(回归基线)mini-desktop 跑 a6-runbook.sh门0 间接 + 门②)+ gate0 driver → 门0 8/8 + real 门 6/6 逐项不变(提升不得改任何判据数值)。

步骤 3ref 拆出装载契约 game 工厂(game-shop.js · 硬约束独立文件 · §12-A3

  • 硬约束:必须是独立文件 games/wanglanmei-ref/src/game-shop.jsexport default createShopGameGameHostFactory 形态),零引擎 import(引擎能力一律经 boot.ctx.getEngine())。在 main.js 内联闭包形似实现糊过——P3 生成 agent 需要一个真实、可复制的参照工厂§12-A3。stub 通道亦经此工厂的 stub 适配驱动(绘制面 g=#game ctx保 stub 行为不变由逐像素门⑤兜。
  • main.js 的游戏逻辑段与绘制段封成 createShopGame(): GameInstance(默认导出 GameHostFactory
    • init(boot):搬 main.js:158-178 游戏对象创建(createShopCore/createLinePicker/createHostFx 等)+ main.js:165-167 异步 loadAssets + main.js:582-611 输入订阅(经 boot.ctx.getInput())。保存 boot.mainContext 为绘制面。
    • update(dt):搬 main.js:617-647 逻辑段(mockNowMs += dt*1000core.update(dtWorld)bundle.tick(dt)applyEventsmaybeReplayBufferedServefx.steprush 联动闲时台词ghosts 清理;rebuildZones)。bundle.tick 仍由游戏 update 调(驱动插件 onFrame 计时;引擎粒子自渲与此解耦,同 host.js A2 注释 host.js:726-728)。
    • render(g):搬 main.js:648-658 绘制段(drawFrame(g, {...}) + palettePost.renderPost(g))。g=boot.mainContext
    • destroy()cancel 输入订阅 + dispose registryP1 可最小实现,须声明)。
  • 难点real 坐标系):引擎 mainContext 无 DPR 变换(引擎每帧 updateCanvas 重置为单位阵)。游戏按逻辑 390×844 绘制 → render(g) 须在 g.save()g.setTransform(DPR,0,0,DPR,0,0) + g.imageSmoothingEnabled=false,对齐 host.js:489-490 renderParticles 的补偿做法。stub 通道 g=prepareCanvas(#game) 已自带 DPR 变换(main.js:69)→ 不补。

步骤 4main.js bootRef real 通道接线

4.1 读 engineMode = getParam('engine', 'stub')缺省 stub 保旧real 显式开。注host-dev 缺省 real但 ref 缺省 stub 更稳——真玩取证显式带 &engine=real)。 4.2 stub 分支:逐行保旧(prepareCanvas(#game) + createHostDevContext 无 engineFactory + 浏览器 RAF + frame(dtReal) + drawFrame(#game))。把现有 main.js 主体收进 stub 分支(或经 game 工厂的 stub 适配,但 stub 务必行为不变——逐像素门⑤兜)。 4.3 real 分支(仿 host.js:897-928;控制流=实质重构不是局部插桩——§12-M4

  • LJS.setGLEnable(false) / setShowSplashScreen(false) / setDebugWatermark(false) / setCanvasClearColor(LJS.BLACK) / setCanvasFixedSize(vec2(390*DPR, 844*DPR))(全在 engineInit 前)。
  • 建独立 engineRoot div 挂 body不复用 #wrap,引擎会覆盖容器样式,host.js:911-915)。
  • engineMath = buildEngineMath(LJS)engineFactory = () => createEngineCaps({ LJS, engineMath, engineMode:'real', recHook })(取证时注 recHook渠道不注 —— 见步骤 7
  • engineInit(engGameInit, engGameUpdate, engGameUpdatePost, engGameRender, engGameRenderPost, [], engineRoot).then(() => setupReal(LJS.mainContext)).catch(e => { window.__refBootError=String(e&&e.message||e); window.__refBootedReal=false; ... })
  • setupReal(mainContext) —— 一切 bundle/game/输入/就绪标记都在此engineInit().then 内)
    • 【M4 硬约束·bundle 必须延后到此建】 bundle = createHostDevContext({ context2d: mainContext, seed, audioFactory, clock: () => mockNowMs, engineFactory })context2d 必为 mainContext——受控面 getContext2d 一次性快照闭包捕获的 ctxhost.js:323-324 注释明确「读闭包捕获值,故必须就绪后建 bundle」绝不可在顶层先用 #game ctx 建 bundle 再改,否则 getContext2d 快照到 stub #game ctx受控面 context2d 与游戏 render 面分裂(= spec 要消除的渲染面分裂,会换形式复现)。可直接引 host.js:353-371buildBundle+ host.js:379-381setupAfterEngine 在 then 内建 bundle作样板。
    • 建 game 工厂 → game.init({ ctx: bundle.context, mainContext, canvas: LJS.mainCanvas, seed })(异步,init 内 await loadAssets
    • window.__refHostwindow.__refForensicswindow.__refChecklist全部移进 then 内挂real 下顶层不可早挂——顶层时 bundle/game 尚不存在)。
    • LJS.mainCanvas.id='game-engine'CDP 取证选择器,仿 host.js:830);暴露 window.__engine 双时钟(仿 host.js:840)。
    • 【M1 硬约束·real 专属就绪标记】 game.init 完成后置 window.__refBootedReal = true(仿 host-dev __hostBootedRealhost.js:920)。这是 real 通道唯一就绪信号——必须在 engineInit().then 内、game.init/资产/受控面全就绪后置。stub 通道照旧用 index.html__refBootedbootRef().then 置,index.html:81real 通道额外置 __refBootedReal。driver 在 real 下改等 __refBootedReal(步骤 8 + §12-M1
  • 五回调:engGameUpdate(){ game.update(FIXED_DT); }engGameRender(){ game.render(mainContext); };其余空/留痕(engGameInit(){ window.__refEngineInitFired=true; })。五回调函数定义在 bootRef 作用域engineInit 须先拿到),实体经 holder 转发到 setupReal 内真实现(仿 host.js:880-887_engStep/_renderParticles holder 范式)。
  • mockNowMsreal 下由 game.updatemockNowMs += dt*1000 累加(逻辑帧钟,非墙钟,同 host.js:526)。
  • RAF 门控startRaf/rafLoopmain.js:668-691)仅 stub 调real 分支不调(引擎内部 RAF 驱动五回调)。 4.4 输入real·控制流重构 §12-M4real 下输入监听改挂 LJS.mainCanvas(不再硬绑传入的 opts.canvas=#game;坐标按引擎 canvas rect仿 host.js:536-538 inputTarget = real ? LJS.mainCanvas : canvastoCanvasXYgetBoundingClientRect 改读 LJS.mainCanvas。监听挂载 + inputBridge 取用同样移进 setupRealbundle 就绪后才有 bundle.inputBridge)。经 bundle.inputBridge._emit 灌受控面;游戏经 boot.ctx.getInput().on 订阅(同 main.js:604-611,订阅在 game 工厂 init 内)。stub 通道输入逐行不变(挂 #gamemain.js:583-602)。

步骤 5entry.js 引擎入包

  • entry.js:15 后增 import * as LJS from 'littlejsengine'; + export const Engine = LJS;(仿 host-dev/entry.js:21/28)。Q4:引擎 import 只活 entry/hostgame 工厂零引擎 import构建后核 game-shop.js 编译产物经 LJS namespace 间接,不直 import

步骤 6SIZES.md 口径

  • 增"游戏产物(引擎+包装层)"表ref stub=197KB不打引擎/ ref real≈540KB raw min引擎真入标 gz 实测(size.mjs);注"插件增量法不覆盖游戏产物口径"。

步骤 7探针绝不入渠道产物物理保证

  • createEngineCapsrecHook 默认 no-opengineFactory 默认不传 recHook。
  • 王蓝莓真玩取证:取证脚本经 URL/全局开关让 main.js real 分支注入 recHook = id => window.__engineCalls.push(id)(仅取证路径)。
  • 渠道产物(未来 P2/P3 出仓 bundleesbuild 入口不引取证注入点 → recHook 恒 no-op → 探针代码无副作用、不挂 window.__engineCallsP1 阶段 ref-bundle 即取证用,注 recHook 可接受ref-bundle 非渠道产物,同 host-bundle 现状 host.js:202)。

步骤 8e2e driver real 通道改造清单(范式切换·非增量 · §12-M1/M2/M3

背景(复审 major 2/1:现行 e2e-input.cdp.cjs driver 主体建在 window.__refHost.stepFrames(n) 手摇逻辑帧 上(e2e-input.cdp.cjs:109,贯穿 :144 tapAt 内 stepFrames(3):236/:277 营业循环 stepFrames(20)),坐标 rect 写死 document.getElementById('game'):127),就绪只等 __refBooted/__refHost:76)。这三处在 real 通道整体失效——必须按下列清单改造,不是「加个 engine=real 通道断言」的增量

real 通道 driver 改造(保 stub 通道路径逐行不变real 走新分支):

(a) 就绪标记M1waitRefBootengine=real 时改等 window.__refBootedReal(而非 __refBooted。real 下 __refBootedindex.html:81 bootRef().then 置)会在引擎/game 就绪前被置 truebootRef 顶层同步段先 resolvedriver 据它驱动会打空/读 undefined → 假失败或间歇绿。host-dev 正是为此引入 __hostBootedRealhost.js:920注释「CDP 取证须等 __hostBootedReal 再驱动」host.js:851/931)。新分支:evaluate('({ b: !!window.__refBootedReal, e: window.__refBootError||null, h: !!window.__refHost })')

(b) 坐标 rect 选择器M3initMappingdocument.getElementById('game'):127)在 engine=real 时切到 document.getElementById('game-engine')(引擎自建可见画布,host.js:830real 下 #game 不是可见画布)。否则取错画布/null → 坐标全错。stub 通道仍用 #game

(c) 确定性序列范式切换M2·硬·边界铁律real 通道禁用 stepFrames 手摇——引擎内部 RAF 已掌帧(装载契约第 1 条「掌帧唯一源=引擎」game 不暴露/不手摇 stepFrames,否则与引擎 RAF 双掌帧违铁律。原「tapAt 后 stepFrames(3)」「循环 stepFrames(20)」改为:真触摸 → delay(ms) 等引擎自然推帧 → 轮询 __refForensics.state()/measures() 判相位推进(轮询到目标态或超时)。这与 stub 的「手摇定量推帧」是范式切换harness §5-坑6 已订:逐像素门交 stub 通道独占手摇,引擎通道只证掌帧+渲染落 2D+真玩闭环不比逐像素哈希。real 真玩去掉 mode=evidencereal 取证不靠 stepFrames靠真触摸+轮询)。

(d) 同屏像素扫描:合 gate0 G0-7 + a6 A6-5 像素扫描CANVAS=#game-engine,判据见 §8 门②(暖色阈值已开工前坐实,见门②)。


7. 边界失败路径

失败 触发 处置
引擎 engineInit 失败 WebGL/资源异常 镜像 window.__refBootError(仿 host.js:925status 提示"可加 ?engine=stub 回滚";门④/CDP 据此判 real 失败→回滚 stub
driver 在引擎就绪前驱动(打空/读 undefined real 下 __refBooted 早于引擎/game 就绪被置§12-M1 __refBootedReal 只在 engineInit().then 内 game.init 完成后置driver real 分支等它(步骤 8a
资产装载缺失 图片/mp3 缺 游戏 init 走程序化回退(保 main.js:165 容错),照常启动;取证 waitAssets 超时如实记录续跑(e2e-input.cdp.cjs:87
粒子渲到分离画布(假阳性) 绘制面未真改 mainContext 门② 同屏像素判据逮(游戏画布粒子命中=0=fail见 §8 门②
AudioContext headless suspended 无手势 --autoplay-policy=no-user-gesture-requiredharness §5门② 音频降软门(同 a6-engine-wiring.cjs:89
engine-caps 提升回归 探针/短路语义漂移 步骤 2.3 门0 8/8 + real 门 6/6 回归卡住;不过即 revert engine-caps.js
受控面被改 误扩 6 项 code review + 契约 diff 卡死(硬约束)

8. 验证方法harness 门)

全在 mini-desktop 跑6c6g 禁 chrome部署口径=沿现行 ref-e2e-runbook.sh 的 git-clone-scratch复审改·§12-A4git clone --depth 50 --branch dev/2.0.0 ssh://git@101.200.34.71:2222/...git checkout <精确SHA> 到 scratchref-e2e-runbook.sh:20-21)→ esbuild 锁参构建 → IP gate → systemd-run serve(static-serve.cjs 根=games/wanglanmei-ref 端口 4320:44)+chrome 两单元(:47)→ 就绪 10×0.5s + serve 5×6s 在线 + CDP 10×2s 两段探活(:55/:59/:64)→ driver:69)→ journal 归档 → 停单元。chrome 旗标含 --autoplay-policy=no-user-gesture-requiredref-e2e-runbook.sh:48harness §5real 版只需在 runbook 的 driver URL 加 &engine=real + 构建后 grep -q ParticleEmitter dist/ref-bundle.js 核引擎入包严禁裸 ./ rsync / tar-pipe 整树6c6g cwd 易重置到仓根吞整树harness §6-律4git-clone-scratch 是 ref runbook 既有口径,勿改成 host-dev/a6 的 tar-pipe staging 配方。

构建命令mini-desktopcwd=game-runtime

node scripts/build.mjs games/wanglanmei-ref/entry.js games/wanglanmei-ref/dist/ref-bundle.js --global-name=WanglanmeiRef
# 此时引擎真入包 ~540KB核对grep -q ParticleEmitter dist/ref-bundle.js

真玩 URLhttp://localhost:4320/?engine=real&seed=305441741(取证模式 &mode=evidence__refHost 确定性驱动;真人交互去掉 mode

判据(可执行阈值) driver / 口径
① 掌帧 window.__engine.snapshot() 两采样间隔 500msframe 增量 ∈ [20,45]@60fps ~30排单步假推进+失控空转) 复用 gate0 G0-2gate0-engine-takeover.cjs:75ref driver 加同款断言ref 暴露 window.__engine
② 同屏像素(堵假阳性) #game-engine(引擎接管后 DOM 可见、=mainContext 的 canvashost.js:830)。两类命中均 >0(a) 游戏美术·暖色(开工前已坐实,非占位):店面暖色主色像素命中数 warmHits > NN=200按 780×1688 抽样网格调,详见下「门②(a) 暖色判据」);(b) 引擎粒子·亮色burst 白 brightHits>0maxCh>80a6 A6-5 口径,a6-engine-wiring.cjs:111)。为何这证合并非两画布:若粒子渲到另一张分离 canvas则截 #game-engine 时粒子命中=0粒子不在这张→ (b) fail若游戏渲到 #game 旧画布、引擎渲 mainContext则截 #game-engine 时游戏命中=0→ (a) fail。两类同时命中 ⟺ 两者真在同一张 #game-engine ref driver 新增(合 gate0 G0-7 + a6 A6-5 像素扫描CANVAS=#game-engine
③ 门②真接线 取证注 recHook 后,window.__engineCallsparticles.spawnEmitter call-ID经 particles-juice 真实 spawnEmitter非直调引擎 复用 a6 A6-2a6-engine-wiring.cjs:79ref driver 触发游戏内真粒子serve_ok 飞行/burst后读 __engineCalls
④ CDP 真玩 + 四件套 CDP 真触摸逐 zone 真玩闭环(开店→进货→营业→结算→升级),首日接待数>0、serve_ok>0、到达结算、升级 upgrades.speed1=true;四件套 evidence输入轨迹 JSONL/截图+canvas 哈希/probe JSONL/checklist JSONevidence/round-N/zone 名以 main.js computeZones 为唯一真相serve-/inc-/up-render.js:967/975/981驱动器猜语义名必踩坑harness §2-律4 e2e-input.cdp.cjs 加 real 通道(步骤 8&engine=real、选择器 #game-engine、等 __refBootedReal、真触摸+轮询harness §2-律2
⑤ stub 回归 ?engine=stub&mode=evidence 菜单帧双开哈希一致(清档 reload 后比harness §5-坑6ref 49 单测仍绿 现有 e2e-input.cdp.cjs 门 Bstub 通道不变)
⑥ 提升回归 engine-caps 提升后host-dev 门0 8/8 + real 门a66/6 逐项不变 步骤 2.3
⑦ console 干净 uncaught=[]real 通道,含未捕获 error/unhandledrejection ref drivermain.js:90-96 uncaught 归集)

门④ driver 范式(复审 M2real 通道 driver=真触摸+轮询(禁 stepFrames 手摇,否则双掌帧违铁律),坐标走 #game-engine rect就绪等 __refBootedReal——见步骤 8。zone 名以 main.js computeZones 为唯一真相(serve-/inc-/up-render.js:967/975/981驱动器猜语义名必踩坑harness §2-律4

门②(a) 暖色判据(开工前坐实 · 复审 major 3

背景gate0 G0-7 的 coloredHits 判据是 青蓝b>120 && b>r+30gate0-engine-takeover.cjs:111a6 A6-5 是 亮度max(r,g,b)>80:111,注释明告青蓝口径对引擎白粒子失效)——二者均不适配王蓝莓暖色店面R 高 B 低与青蓝正相反。harness §6-2 已订「换引擎能力后像素门颜色假设必同步重核」。故门②(a) 判据开工前从 palette.js 坐实,不留执行时占位。

已坐实色域(源:games/wanglanmei-ref/src/palette.js:23-41 BASE 暖调家族,已读真源核对):

用途 色名 RGB RB 命中
柜台/货架木带 wood (169,118,79) 90
深木描边 woodDark (124,84,55) 69 R<140 不计wood 覆盖)
砖橙按钮/警示 brick (201,111,74) 127
金/钱强调 gold (232,169,66) 166
琥珀进度条 amber (240,196,117) 123
差评红 danger (178,72,54) 124
纸底(须排除) paper #f6e7c9 (246,231,201) 45 ✗ 反例
高亮纸(须排除) paperHi (255,246,227) 28 ✗ 反例
暖天光(须排除) sky (243,217,168) 75 ✗ 反例
奶油粒子亮色(须排除) cream (255,243,220) 35 ✗ 反例(近白,归引擎粒子域)

判据(暖木/砖/金色域,排纸底与暖天光)

warmHit ⟺ (r > 140) && (r - b > 85) && (g < r)
  • 命中wood(R169,RB90,118<169)✓ / brick(201,127,✓) / gold(232,166,✓) / amber(240,123,✓) / danger(178,124,✓)——店面木带/货架/按钮/HUD 金额(render.js 大量 pal.c('wood'|'brick'|'gold'|'amber'),已读真源:render.js:120/165/179/200/253/308 等);
  • 排除paper(RB45<85)✗ / paperHi(28)✗ / sky(75<85)✗ / cream(35)✗——纸底/天光/近白粒子均落空,堵「把暖纸背景误判成店面美术」的假阳性;
  • 鲁棒性rush 压力态色板经 shiftHsl(hue 10, sat 1.18, light 0.9)palette.js:57)把暖色更暖RB 进一步拉大、饱和升),仍稳在色域内,不会因 rush 漂出;
  • 阈值 warmHits > 200:扫 780×1688 backing或抽样网格与 gate0/a6 同扫法),店面木带+货架+按钮+招牌占屏面积远超 200 抽样点;过窄(真渲也判 0=假阴性)与过宽(纸底误命中=假阳性)两侧均已由上面色域不等式+反例锚定。N 在 driver 落地时按实际抽样网格密度微调,但色域不等式不动(不等式是堵假阳性的客观锚,开工前定死)。

9. 完成条件

  • 装载契约 game-runtime/src/core/game-host.d.ts 落盘§12-A1+ contracts/README.md 指针登记(不折进跨仓 8 类);只 import type { PluginContext } from './api.d.ts'api.d.ts 零改diff 证)。
  • game 工厂 game-shop.js独立文件、默认导出 GameHostFactory、零引擎 import(非 main.js 内联§12-A3 硬约束)。
  • host-dev/engine-caps.js 提升完成host.js import 它;门0 8/8 + real 门 6/6 回归逐项不变evidence 证)。
  • ref ?engine=real 通道:门① 掌帧 ∈[20,45] ∧ 门② 同屏(#game-engine 上游戏美术 warmHits>200(§门②(a) 坐实暖色判据)+ 引擎粒子 brightHits>0∧maxCh>80 双命中)∧ 门③ particles.spawnEmitter call-ID ∧ 门④ CDP 真玩闭环(真触摸+轮询范式,非 stepFrames 手摇+四件套 ∧ 门⑦ console 零未捕获——全绿
  • real 通道就绪标记 __refBootedReal 接线正确driver real 分支等它§12-M1bundle 在 engineInit().then 内以 context2d=mainContext§12-M4
  • ref ?engine=stub 门⑤ 逐像素双开哈希一致 + 49 单测绿stub 通道行为逐行不变)。
  • SIZES.md 增游戏产物口径gz 实测)。
  • 受控面 6 项契约零改(硬约束守住)。
  • 人在 mini-desktop 浏览器 ?engine=real 真玩一局(开店→营业→结算→升级),截图归档。

10. 回滚

  • 代码?engine=stub 与今天 main.js 逐行等价real 过不了→用户/取证走 stub既有交付零连累。
  • 契约game-runtime/src/core/game-host.d.ts additive落 src/core§12-A1移除不破存量受控面零改。
  • 模块engine-caps.js 提升若回归→git revert 该文件 + host.js import 行host.js 退内联闭包ref real 自带 engineFactory 经 createEngineCaps 不连累。
  • 构建ref-bundle stub 版197KB不打引擎随时可重出回退引擎入包。

11. 风险与假设(诚实标注)

  • [verified] 装载契约全仓不存在grep 零命中ref 游戏层 file:linemain.js:112/583-602/618/621/648/668-691stepFrames 手摇 :664/:109host.js real 范式(host.js:323-324/353-371/379-381/488-490/536-538/830/840/904/917-920/930-932,缺省 real :340index.html __refBooted 置位时机(index.html:80-81driver 现状(e2e-input.cdp.cjs:76/109/127/144/236/277makeEngineCaps 闭包(host.js:197-318+ engineFactory 注入(host.js:359burst colorStart 白(particles-juice/impl.js:217gate0 青蓝 :111 / a6 亮度 :111palette 暖色 BASEpalette.js:23-41rush shiftHsl :57zone kind 枚举(render.js:967/975/981ref runbook git-clone-scratchref-e2e-runbook.sh:20-21/44/47/48contracts 跨仓 SSOT 定位(contracts/README.md:1-3 §1含拆仓计划sdk-interface.d.ts 自洽无 game-runtime import先例
  • [verified·复审降级] ref entry 加引擎 import 能被 esbuild 解析入包——host-dev/entry.js 末行已 import * as LJS from 'littlejsengine' 且 host-dev real 门 6/6 已过(同 monorepo node_modules已被既有事实强力支撑作常规构建核对grep -q ParticleEmitter,步骤 0非阻断门§12-A5
  • [inferred] ref real ~540KB 不触 B1 入场券——基于 host-dev entry 544KB 实测 + gz 量级P1 用 size.mjs 实测游戏产物 gz 坐实(步骤 6
  • [已裁·非待确认] game 工厂必须独立文件 game-shop.js、默认导出、零引擎 import§12-A3 硬约束,不再留「可 main.js 内联」口子)。
  • [已裁·非占位] 门②(a) 暖色判据已开工前从 palette.js 坐实具体色域(r>140 && rb>85 && g<r,排纸底/天光/近白,见 §8「门②(a) 暖色判据」)——核心门判据不带占位落地。
  • [待确认·留创始人] 工件命名 game-host.d.ts(暂名);若创始人偏好其它名(如 game-loadable.d.ts),执行版改名即可,形态/落点不变。这是唯一留给创始人的 open item不阻断执行(命名改名零成本)。

12. 复审裁决依据2026-06-13 对抗复审 · 逐条)

复审口径execution 是落地契约 + 高风险(立新契约 + 改真游戏主循环deep 校准全程读真源码、凡引代码事实带 file:line。无 fatal4 major 全纳入、5 项不明假设逐条处置。下表为各条裁决与依据,正文已落地。

编号 复审条目 裁决 落地位置 / 依据
M1 real 就绪标记缺失(__refBooted 早于引擎/game 就绪被置driver 据它打空/flaky 纳入·硬约束:新增 real 专属 window.__refBootedReal(只在 engineInit().then 内、game.init 完成后置driver real 分支改等它 步骤 4.3setupReal 末置 __refBootedReal+ 步骤 8adriver 等它)+ §7 失败路径;依据 index.html:81/host.js:920/851/931/e2e-input.cdp.cjs:76
M2 stepFrames 手摇范式在引擎掌帧下失效(且沿用=双掌帧违铁律);改造被低估为「加通道断言」 纳入·范式切换real driver 禁 stepFrames改真触摸+delay等引擎自然推帧+轮询状态;明列这是相对 stub 的范式切换非增量 步骤 8c确定性序列范式切换+ §8 门④范式注 + IDL 不可破约定 1 推论;依据 e2e-input.cdp.cjs:109/144/236/277、harness §5-坑6
M3 门②(a) 暖色阈值与 gate0/a6 青蓝/亮度口径相反,却留执行时占位 纳入·开工前坐实:从 palette.js BASE 取定暖色色域不等式 r>140 && rb>85 && g<r + 纸底/天光/近白反例排除;核心门判据不带占位 §8「门②(a) 暖色判据」(已坐实色域表+不等式+反例);依据 palette.js:23-41/57gate0:111a6:111、harness §6-2
M4 render(g) 注入路径 / bundle 重建顺序未钉死ref 顶层同步建 bundle 会快照错 ctx 纳入·硬步骤real bundle 必须在 engineInit().then(setupReal) 内、以 context2d=mainContext 建(不得顶层先建再改);输入桥+game.init+就绪标记同移进 setupReal明示这是 bootRef 控制流重构 步骤 4.3M4 硬约束)+ 4.4(输入移进 setupReal+ IDL 不可破约定 2 推论;依据 host.js:323-324/353-371/379-381/536-538main.js:112/584/590-602
A1 IDL 落 contracts/ 并反向相对 import ../game-runtime/src/api.d.tsimport 方向/落点合理性) 改落点:落 game-runtime/src/core/game-host.d.ts(与 api.d.ts 同目录import=sibling ./api.d.ts);不折进 contracts/ 跨仓 8 类(仅 README 指针。理由contracts/ 是跨三仓五工位业务契约 SSOT本契约是 game-runtime 内部接线面,让 SSOT 反向相对依赖业务源码违「契约即 SSOT」且拆仓即断 头部「装载契约落点」+ §3 文件表 + §5 IDL 落点注 + 步骤 1依据 contracts/README.md:1-3(拆仓计划)、sdk-interface.d.ts(自洽先例)
A2 ref 缺省 stub vs host-dev 缺省 real两套缺省心智 显式声明此分叉是有意为之:真玩取证显式带 &engine=real 更稳、stub 兜逐像素回归P3 复制 ref 作模板时须知缺省 stub=「新游戏默认不上引擎」是过渡现状引擎是正路boot 注入装载契约) 步骤 4.1(缺省 stub+ 本表显式声明;依据 host.js:340host-dev realvs main.js:82ref play/stub
A3 game 工厂「建议非强制」留 main.js 内联口子P3 缺真实参照实现) 钉成硬边界game 工厂必须独立文件、默认导出、零引擎 import、禁 main.js 内联糊过 §1.2 + 步骤 3 开头 + IDL GameHostFactory 注 + §9 完成条件 + §11依据 P1 目标spike 装载契约到可被 P3 复制)
A4 §8 部署写 tar-pipe与现行 ref-e2e-runbook.shgit-clone-scratch不符 统一为 git-clone-scratchreal 版只在 runbook driver URL 加 &engine=real + 构建后 grep ParticleEmitter勿用 host-dev/a6 的 tar-pipe staging 配方 §8 部署口径段;依据 ref-e2e-runbook.sh:20-21/44/47/48
A5 步骤0 inferred 风险被高估host-dev 已证可入包) 降级为常规构建核对grep ParticleEmitter 即可,非可能阻断的高不确定项 步骤 0 + §11依据 host-dev/entry.js 已 import 引擎且 real 门 6/6 已过