games-development-ai/docs/agent-specs/2026-06-13-P1-王蓝莓上引擎-review.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

21 KiB
Raw Blame History

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

状态review 版 · 2026-06-13 · 给创始人决策(结论先行,不写代码级执行细节,多用 Mermaid已过对抗复审2026-06-13 deep·全程读真源码无致命项4 重大项 + 5 项不明假设已全部纳入 execution 版钉死(逐条裁决见 execution §12。readiness = ready-with-caveats(仅 1 个非阻断 open item契约工件命名见 §8定位:本文是 Runner v2 全弧 P1 相(第一相)的评审版。母梁见 2026-06-13-runner-v2-arc-review.md §5.1。P1 的任务 = 把"游戏宿主装载契约"arc 上层契约)用王蓝莓 spike 到真跑在引擎上P2/P3 归并入母 spec见 §7前提(创始人已拍):① 推进 P1全弧第一相O1 装载契约 = 新立 additive 第 9 类契约(不污染既有插件契约 PluginContext),生命周期 = init/update/render/onInput + destroyboot 时把引擎 mainContext 注入游戏层;③ O2 = P2 合并进母 spec Phase D④ 不用 fable一律 opus。 复审改点(结论级,详见 execution §12:契约工件game-runtime/src/core/game-host.d.ts(与 api.d.ts 同目录, contracts/——contracts 是跨三仓五工位业务契约 SSOT本契约是 game-runtime 内部 host↔游戏工厂接线面让 SSOT 反向依赖业务源码且拆仓即断real 通道补 __refBootedReal 就绪标记 + driver 范式从「stepFrames 手摇」切到「真触摸+轮询」(引擎掌帧下手摇=双掌帧违铁律同屏门暖色阈值开工前坐实不留占位bundle 延后到 engineInit().then 内以 mainContext 建(防渲染面分裂)。 关联:边界铁律 engine-plugin-boundary-model、A0 地基 a0-engine-takeover-done、harness 配方 .agents/skills/game-e2e-cdp-harness.md§5 门0 / §6 门②、A0 双通道(?engine=stub 回滚)。


1. 结论先行

P1 = 给 wanglanmei-ref?engine=real 通道,用它把"游戏宿主装载契约"(新立 additive 第 9 类契约spike 实,证三件事并人在 mini-desktop 浏览器真玩:

  1. 引擎真掌帧engineInit 五回调驱动游戏 update/render,废 main.js 自管的浏览器 RAF
  2. 游戏美术与引擎粒子同屏可玩(游戏绘制面从自带 #game 2D 画布改引擎 mainContext,两面合并成一张画布);
  3. 回滚保真?engine=stub 通道逐行保旧行为 + 逐像素确定性A0 双通道范式)。

同时把已存在但仍是闭包的 makeEngineCapshost.js:197-318)提升为可 import 模块 host-dev/engine-caps.jshost-dev / ref / 未来生成游戏共用同一份引擎能力包装层——这是"模块化已有件"(搬运),非从零造arc §0-F2 已订正原稿"engine-caps.js 不存在=真新增工作")。

装载契约的本质O1 已拍形态):一款游戏 = 一个纯模块工厂(零引擎 importQ4 铁律),暴露稳定生命周期 init(ctx, mainCtx) / update(dt) / render(g) / onInput(ev) / destroy();其中 render(g)g = 引擎 mainContextboot 时注入),插件能力仍经下层已冻结的 ctx.getEngine() 拿。该契约落 game-runtime/src/core/game-host.d.ts(与既有 api.d.ts 同目录,复审改点;理由见上「复审改点」),与 api.d.tsadditive 关系——受控面 6 项契约零改getContext2d/getInput/getAudioContext/getEngineapi.d.ts:269/281/286/303)。game 工厂必须是独立、可被 P3 复制的实现(独立文件默认导出、零引擎 import禁 main.js 内联糊过——这是 P3 生成 agent 的真实参照靶,复审钉死的硬边界)。

⚠️ 为何必须新立、不能借 PluginContextref 的游戏层(core.update/drawFrame(g)main.js:621/648不是插件、不走 PluginContext;今天全仓没有"游戏整体↔引擎 mainContext"的约定面(grep GameModule/装载契约contracts/+src/ 零命中,已复证)。getEngine()api.d.ts:303)是插件拿引擎能力的面,凑不上"游戏 update/render 主体怎么挂引擎"。这正是装载契约要补的缺口——它与 PluginContext 一个都不能替另一个(边界模型"同一职不留两条并行路")。


2. 装载契约决策O1 已拍)

决策点 已拍结论 理由 / 证据
归属 新立 additive 第 9 类契约,不折进 api.d.ts 插件面 避免污染插件契约语义,与边界模型"一职一路"一致;PluginContext=插件面、GameHost=游戏面两者正交arc O1 建议①)。
落点(复审改) game-runtime/src/core/game-host.d.ts(与 api.d.ts 同目录), contracts/ contracts/ 是跨三仓五工位的业务契约 SSOTcontracts/README.md §1含拆仓计划本契约是 game-runtime 内部 host↔游戏工厂接线面消费方只在 game-runtime落同目录 import 方向顺(./api.d.ts);落 contracts 会让 SSOT 反向相对 import ../game-runtime/src,违「契约即 SSOT」且拆仓即断execution §12-A1
生命周期粒度 init(ctx, mainCtx) / update(dt) / render(g) / onInput(ev) / destroy() 五件 init/update/render/onInput 对齐引擎五回调(engGameInit/Update/Renderhost.js:883-887destroy 补卸载卫生P2 装载多游戏时复位)。其余(resize/onVisibilityadditive 后补。
绘制面约定 render(g)g = 引擎 mainContextboot 时注入游戏层 A0 已证 setGLEnable(false) 后引擎纯 2D 渲进 mainContexthost.js:904/919);游戏绘制面改它即与引擎粒子同屏(消除渲染面分裂)。不借 PluginContext
能力获取 插件能力经下层已冻结 ctx.getEngine();游戏绝不 import littlejsengine Q4 铁律:引擎 import 只活 entry/hostentry.js:18-21/host.js:44)。

装载关系图(游戏 ↔ host ↔ 引擎)

flowchart TB
  subgraph host["集成段 host 层(唯一 import 引擎处·Q4 铁律)"]
    EI["LJS.engineInit 五回调<br/>(引擎内部 RAF 驱动)"]
    EC["engine-caps.js 模块<br/>(提升自 host.js:197 闭包·非从零)<br/>particles/audio.synth/math 三面薄包装"]
    BOOT["bootRef real 分支<br/>setGLEnable(false)+建 engineFactory<br/>+ 注入 mainContext 给游戏层"]
  end
  subgraph game["游戏侧(装载契约·零引擎 import·新立第9契约"]
    GH["game.init(ctx, mainCtx)<br/>game.update(dt)<br/>game.render(g=mainContext)<br/>game.onInput(ev)<br/>game.destroy()"]
  end
  subgraph low["下层契约(已冻结·复用·零改)"]
    PC["PluginContext.getEngine()<br/>受控面 6 项 + 引擎能力面"]
  end
  EI -->|gameUpdate→| GH
  EI -->|gameRender→ 引擎 mainContext| GH
  BOOT -->|注入 mainCtx| GH
  EC -->|engineFactory| BOOT
  PC -. 插件能力 ctx.getEngine() .-> GH
  GH -. 禁 .->|import littlejsengine| X1["❌ Q4 违例"]

3. 同屏方案与最大风险

3.1 同屏方案real 通道改造主干)

改造点 stub 通道(保旧,回滚锚) real 通道(新增,引擎掌帧)
掌帧源 浏览器 RAF + mockNowMs(墙钟近似),main.js:668-691 引擎 engineInit 五回调RAF 旁置/门控;mockNowMs 改逻辑帧钟(每逻辑步 +FIXED_DT×1000
逻辑段 frame(dtReal)core.update/bundle.tick/applyEvents/fx.step 同段移入 game.update(dt),由 gameUpdate 回调驱动
绘制段 drawFrame(g)+palettePost.renderPost(g)g=#game 2D 同段移入 game.render(g)g=引擎 mainContextboot 注入),由 gameRender 回调驱动
插件受控面 context2d host prepareCanvas(#game) 的 ctx 快照引擎 mainContextcreateHostDevContextcontext2d 改它)
粒子能力 engineFactory 未传 → getEngine()=null → 粒子 NOOP_HANDLE(无粒子) engine-caps.jsengineFactory → 粒子真经引擎 ParticleEmitter 自渲落 mainContext

3.2 最大风险(先标在最前)

风险 等级 缓解 / 回滚
真渲同屏过不了 / 假阳性(粒子渲到另一张分离画布,游戏画布无粒子命中) P0 这是 P1 第一道真门,也是全弧第一道门。判据必须堵死假阳性:截取引擎接管后 DOM 可见那张 canvas=mainContext 的 canvasA0 已给它打 id #game-enginehost.js:830断言同一张画布上既有游戏美术像素命中店面暖色主色又有引擎粒子像素命中burst 白 maxCh>80复审改M3:店面暖色阈值开工前已从 palette.js 坐实具体色域r>140 && rb>85 && g<r,排纸底/天光/近白),不留执行时占位——gate0/a6 的青蓝/亮度口径与暖色正相反,留占位会假阳/假阴execution §8「门②(a)」)。若粒子渲到另一张画布 → 游戏画布粒子命中=0 → fail。回滚=?engine=stub
掌帧冲突main.js 自管 RAF + mockNowMs 与引擎掌帧直接冲突) P0 A0 双通道已证引擎掌帧可行real 通道废 RAF、mockNowMs 改逻辑帧钟,对齐 host.js real 分支既定做法(host.js:526/884)。
real 通道就绪标记缺失(复审 M1——driver 在引擎/game 就绪前驱动→打空/间歇绿 P0 real 下 __refBootedindex.html:81 bootRef().then 置)早于引擎就绪;补 real 专属 __refBootedReal(只在 engineInit().then 内 game.init 完成后置,仿 host-dev __hostBootedReal host.js:920driver real 分支等它execution 步骤 4.3/8a
driver 范式失效(复审 M2——stepFrames 手摇在引擎掌帧下双掌帧违铁律、#game rect 取错画布 P0 real driver 范式切换(非增量):禁 stepFrames改真触摸+轮询rect 选择器切 #game-engine。与 stub「手摇定量推帧」分道stub 兜逐像素real 只证掌帧+渲染+真玩harness §5-坑6。execution 步骤 8。
渲染面分裂(复审 M4——real 顶层同步建 bundle 会快照错 ctx受控面 context2d≠游戏 render 面) P0 bundle 必须延后到 engineInit().then 内、以 context2d=mainContextgetContext2d 一次性快照闭包捕获值,host.js:323-324);输入桥+game.init+就绪标记同移进。execution 步骤 4.3M4
engine-caps 模块化引入回归(探针/短路语义漂移) P1 提升须保 engineMode 短路 / __engineCalls 探针 / null-engine 回归语义逐行不变门0 8/8 + real 门 6/6 作回归基线。recHook 设计成可注入(默认 no-op——探针绝不入渠道产物(见 §6
receHook 探针泄进渠道产物 P1 物理保证:createEngineCaps({recHook}) 默认 recHook=no-opref-bundle 构建时不传 recHook(或传 no-op仅王蓝莓真玩取证脚本注入 rec。渠道产物 esbuild 入口不引取证注入点 → 探针代码 tree-shake 不进包。
体积ref 197KB→~540KB引擎真入包 P2 ~540KB raw minhost-dev entry 实测 544KB 量级gz 远在 B1 入场券 gz≤350KB 内LittleJS 完整版 raw≈55KB gz→ 不触红线。SIZES.md 增"游戏产物=引擎+包装层"口径(与插件增量分离)。勿引"13KB"(已废)。
受控面被动 P0 受控面 6 项契约零改是硬约束(engineFactory 是既有可选入参 api.d.ts:405);装载契约是 additive 新增,不改下层既冻 6 项。

4. 验收门概览arc §5.1 六门 + 韧性)

判据 口径来源
① 掌帧 引擎掌帧帧增量区间 [20,45]/500ms gate0 G0-2gate0-engine-takeover.cjs:75
② 同屏像素 同一张画布(#game-engine)上:游戏美术暖色命中(warmHits>200,色域 r>140 && rb>85 && g<r开工前坐实非占位·M3∧ 引擎粒子像素命中burst 白 maxCh>80brightHits>0);粒子渲到另一画布则游戏画布命中=0=fail a6 A6-5maxCh/brightHits+ 暖色域自 palette.js:23-41execution §8「门②(a)」)
③ 真接线门② window.__engineCallsparticles.spawnEmitter call-ID插件运行时真调引擎 a6 A6-2a6-engine-wiring.cjs:79
④ CDP 真玩 CDP 真触摸逐 zone 真玩(开店→进货→营业→结算→升级)+ 四件套 evidencedriver 范式=真触摸+轮询,禁 stepFrames 手摇·M2rect 选择器 #game-engine、就绪等 __refBootedRealzone 名以 main.js computeZones 为唯一真相 ref e2e 扩 real 通道execution 步骤 8+ harness §2-律4
⑤ stub 回归 ?engine=stub 逐像素确定性回归仍绿(双开哈希一致) ref e2e 门 B + harness §5-坑6
⑥ 提升回归 engine-caps 提升后门0 8/8 + real 门 6/6 逐项不变 gate0 + a6
⑦ console 干净 console 零未捕获错误 ref e2euncaught=[]

5. 回滚

  • 代码回滚?engine=stub 通道与今天 main.js 行为逐行等价RAF + #game 2D 画布 + mockNowMs 墙钟。real 通道过不了 → 用户/取证一律走 stubstub 既有交付T1b-α 49 单测 + e2e 四件套)零连累。
  • 契约回滚:装载契约 game-host.d.ts 是 additive 新增工件,受控面 6 项零改 → 移除新增不破存量。
  • 模块回滚engine-caps.js 提升若引入回归门0/real 门红),可临时让 host.js 退回内联闭包git revert 该文件 + host.js import 行),不连累 ref real 通道ref 自带 engineFactory 经 createEngineCaps

6. 爆炸半径

触及 文件 性质
ref 主循环重做 games/wanglanmei-ref/{entry.js:15, src/main.js:112/583-602/617-691/648} real 通道改造stub 保旧);含输入改挂 mainCanvasM4
装载契约新工件 game-runtime/src/core/game-host.d.ts(新增,复审改落点)+ contracts/README.md(仅指针登记) additive 第 9 类契约game-runtime 内部)
engine-caps 模块化 host-dev/engine-caps.js(新增,搬 host.js:197-318+ host-dev/host.js(改 import 搬运已有件,语义零变
ref 装载适配 games/wanglanmei-ref/src/game-shop.js新增·硬独立文件A3:拆出符合装载契约的 game 工厂(默认导出、零引擎 import禁 main.js 内联entry import 引擎 real 通道接线
ref 就绪标记 games/wanglanmei-ref/index.htmlreal 通道接 __refBootedRealM1 就绪信号stub 仍用 __refBooted
SIZES 口径 SIZES.md 增"游戏产物(引擎+包装层)"新表 文档(与插件增量分离)
受控面 6 项契约 api.d.ts:269/281/286/303 零改(硬约束)
harness driver e2e-input.cdp.cjs real 通道范式切换(真触摸+轮询/选择器 #game-engine/等 __refBootedReal/同屏暖色判据非「加断言」增量M1/M2/M3 取证范式重写

P1 不触及:后端发布主链 / game-studio / feed / 生成管线(那些是 P2/P3归母 spec

双通道全景real / stub

graph TB
  URL{"?engine="}
  URL -->|real新增·引擎掌帧| R0
  URL -->|stub保旧·回滚锚| S0
  subgraph R["real 通道"]
    R0["entry import LJS<br/>引擎真入 ref-bundle ~540KB"] --> R1["setGLEnable(false)<br/>engineInit 五回调"]
    R1 --> R2["createEngineCaps 建 engineFactory<br/>注入 createHostDevContext"]
    R2 --> R3["game.update←gameUpdate<br/>game.render(mainContext)←gameRender<br/>mockNowMs=逻辑帧钟·废 RAF"]
    R3 --> R4["验收:门①掌帧/②同屏/③call-ID/④真玩/⑦console"]
  end
  subgraph S["stub 通道"]
    S0["不 import 引擎<br/>ref-bundle 197KB"] --> S1["prepareCanvas(#game) 2D<br/>浏览器 RAF + mockNowMs 墙钟"]
    S1 --> S2["frame(dtReal) 原样<br/>drawFrame(g=#game)"]
    S2 --> S3["验收:门⑤逐像素双开哈希一致"]
  end
  R4 -.过不了回滚.-> S3

7. P2 / P3 归并O2 已拍)

  • P2泛化宿主装载任意 GamePackage合并进母 spec 2026-06-13-T1b-β引擎真接线与Runner-v2-execution.md Phase DO2 已拍。P1 验实的装载契约 = P2 的产品化基底:母 spec Phase D 的"ref 经发布主链入 feed"升级为"engine-aware 包(按装载契约 + packageUrl 加载,game-package.schema.json:66)入 feed 跑引擎真玩"。P2 不在本 spec 展开。
  • P3W-G1 生成游戏按同一装载契约产出) 归 W-G1 自身 specagent 写码于插件库、产装载契约 bundle、20 基准 bake-off。P1 的装载契约 IDL 是 P3 生成 agent 的唯一产出靶

8. 待确认 / 风险与假设(诚实标注)

对抗复审结论2026-06-13·deep·全程读真源码无致命项。装载契约 IDL 五件init/update/render/onInput/destroy形态合理、与引擎五回调对齐准确host.js:883-887additive 关系成立(受控面 6 项零改 api.d.ts:269/281/286/303/405);双通道回滚有 A0 实证。4 重大项已全部纳入 execution 版钉死real 就绪标记 M1 / driver 范式切换 M2 / 暖色阈值坐实 M3 / bundle 延后建 M4均为「会致执行 agent 临场返工或门假绿/假红的硬装配问题」,不翻盘但必须在 execution 钉死。5 项不明假设逐条处置(落点改 game-runtime/src·缺省分叉显式·game 工厂硬独立·部署 git-clone-scratch·步骤0 降常规核对)。逐条裁决见 execution §12。 唯一 open item非阻断:装载契约工件命名(见末条)。

缺省值分叉(复审 A2·显式声明此分叉有意为之host-dev 缺省 engine=realhost.js:340ref 缺省 engine=stubmain.js:82 play 模式走 stub 通道)。有意不统一:真玩取证显式带 &engine=real 更稳、stub 兜逐像素回归;但须知 P3 复制 ref 作模板时,缺省 stub=「新游戏默认不上引擎」是过渡现状,引擎是正路boot 注入装载契约)——这点 P3 spec 须承接。

  • [verified] 装载契约全仓不存在(grep GameModule/装载契约/engineInitcontracts/+src/ 零命中);既有契约 = PluginContextapi.d.ts:269/281/286/3036 受控面 + getEngine()+ packageUrlgame-package.schema.json:66)。
  • [verified] engine-caps.js 文件不存在,但内容已大半存在:makeEngineCaps 闭包(host.js:197-318)已包装 particles/audio.synth/math 三面并已注入 engineFactoryhost.js:359engine-math.js 已独立成模块(host.js:47 import。→ 提升=搬运。
  • [verified] ref 游戏层 core.updatemain.js:621/drawFrame(g)main.js:648g=prepareCanvas(#game)/RAFmain.js:668-691/mockNowMs 步进钟(main.js:618createHostDevContext 仅 4 键无 engineFactorymain.js:112)→ 粒子今天全 NOOP_HANDLE
  • [verified] real 通道引擎掌帧范式已在 host.js 跑通:engineInit 五回调(host.js:917+ setupAfterEngine(LJS.mainContext)host.js:919+ 就绪标记 __hostBootedReal 在 then 内 game 就绪后置(host.js:920,注释明告 CDP 须等它再驱动 :851/931+ bundle 在 then 内建(host.js:323-324/353-371/379-381+ 输入挂 mainCanvashost.js:536-538+ setGLEnable(false)host.js:904+ 双时钟 window.__enginehost.js:840+ mainCanvas.id='game-engine'host.js:830)。这些 host-dev 已踩并解决的坑M1/M4ref 必须继承execution §12
  • [verified] burst 粒子 colorStart 缺省白 {r:1,g:1,b:1,a:...}toEngineSpecparticles-juice/impl.js:217),故引擎粒子像素门判 brightHits/maxCh青蓝口径已对引擎渲染失效harness §6-2游戏美术暖色门另判暖色域M3palette.js:23-41)。
  • [verified·复审降级] ref 接引擎 import 能入包——host-dev/entry.js 末行已 import 引擎且 real 门 6/6 已过(同 monorepo node_modules常规构建 grep ParticleEmitter 核对A5非阻断门。
  • [inferred] ref ~540KB 不触 B1 入场券——基于 host-dev entry 544KB 实测 + gz 量级P1 用 size.mjs 实测游戏产物 gz 坐实。
  • [assumption] "好玩基线 v2 五要素"精确判据不在本 spec 范围P3 验收门事P1 只证装载契约可让真游戏真跑在引擎上同屏可玩。
  • [open item·留创始人·非阻断] 装载契约工件命名 game-host.d.ts暂名arc 称"游戏宿主装载契约");若创始人偏好其它名(如 game-loadable.d.ts),执行版改名即可,形态/落点game-runtime/src/core不变。改名零成本,不阻断执行。