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>
This commit is contained in:
zizi 2026-06-13 18:32:50 +00:00
parent db9b7b623e
commit 99b6c82dec
25 changed files with 3709 additions and 811 deletions

View File

@ -23,6 +23,8 @@
> **Wave1 脊柱 4 模块契约已补并锁**aigc/runtime/feed/telemetry2026-06-08经契约先行起草 + 一致性评审 + 收口§2.1+ 主 agent 校验YAML 语法/裸 select* 0/错误码段 101-104 独占/Flyway V2-5 唯一)。
> **Wave2 变现契约已补并锁**ad/trade2026-06-08`ad.yaml`+`trade.yaml`API #1+ `V6.0.0`/`V7.0.0`DB #2),消费既有 #7 ad-slot / #5 events 不重造;经主 agent 校验YAML 语法/错误码 111·106 独占/Flyway V6-7 唯一/金额用「分」/资金幂等键+状态机/ad↔trade Feign seam 一致ad 计费 `uk_trace` 由主 agent 收紧为 `(trace_id,event_type,tenant_id)` 防 reward 被 impression 静默吞掉。**pay=复用 yudao-pay 后置**(收单 P1
> **Wave3 跨主线收口(进行中)**:新增 `studio`(创作主链路最小版编排,错误码 112 / Flyway V8+ `compliance`(锁风门 Gate发布前合规扫描两模块契约。Phase A 已收口 project 侧publish 唯一化(返回 admitted+gates 门禁聚合、ProjectApi seam 定稿getCurrentVersionId GAP-3 / createProject 写类 RPC、GAP-4 把 `events.schema.json` 信封 envelope/user 由 snake_case 统一为 camelCase与 telemetry.yaml/前端 SDK 对齐。compliance gate 在 project.submitPublish 内仅留 Phase C 接入 seamTODO 注释),尚未接 ComplianceGateApi。其余模块community/ip/biz待后续波次补。
>
> **游戏宿主装载契约(指针登记,不折进上表跨仓 8 类)**Runner v2 P1 新立 additive 第 9 类契约,**落 `game-runtime/src/core/game-host.d.ts`**(与 `api.d.ts` 同目录,**非**本目录)。理由:它是 game-runtime 内部「host↔游戏工厂」接线面消费方只在 game-runtimehost boot + 生成 agent落同目录 import 方向顺(`./api.d.ts`);落本目录会让跨仓 SSOT 反向相对 import `../game-runtime/src`(拆仓即断)。消费方=host boot装游戏+ 生成 agent产出靶与受控面 6 项 additive受控面零改。详见 `docs/agent-specs/2026-06-13-P1-王蓝莓上引擎-execution.md` §5/§12-A1。
```
contracts/

View File

@ -0,0 +1,370 @@
# P1 · 王蓝莓上引擎spike 游戏宿主装载契约)· 执行版
> **状态**execution 版 · 2026-06-13 · 给执行 agent 实现。配套 review 版 [`2026-06-13-P1-王蓝莓上引擎-review.md`](./2026-06-13-P1-王蓝莓上引擎-review.md);母梁 [`2026-06-13-runner-v2-arc-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` + `destroy`boot 注入引擎 `mainContext` 给游戏层。
> **铁律**:受控面 6 项契约(`getContext2d/onFrame/getInput/getAudioContext/time/random``api.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` 回滚(逐像素确定性)。同时把 `makeEngineCaps``host.js:197-318`)提升为可 import 模块 `host-dev/engine-caps.js`host-dev / ref 共用。
### 1.2 范围内
- 新立装载契约工件 **`game-runtime/src/core/game-host.d.ts`**additive 第 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` 主循环挂引擎五回调、绘制面改 `mainContext``createHostDevContext``engineFactory``mockNowMs` 改逻辑帧钟、RAF 门控。
- ref 内拆出符合装载契约的 game 工厂(**硬约束:独立文件 `game-shop.js`,默认导出 `GameHostFactory`,零引擎 import禁 main.js 内联糊过**,详见步骤 3 + §12-A3
- `e2e-input.cdp.cjs``engine=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 通道范式可用) | verified`host.js:917` engineInit 五回调 + `host.js:919` setupAfterEngine(mainContext)门0 8/8`3e5cab4` |
| P-2 | A1-A4 引擎能力包装已落地makeEngineCaps 已接线) | verified`host.js:359` `engineFactory: makeEngineCaps``engine-math.js` 已独立模块 |
| P-3 | mini-desktop 具备 esbuild + chrome6c6g 禁 chrome | verified`a6-runbook.sh` / `ref-e2e-runbook.sh` 均跑 mini-desktop |
| P-4 | ref 现状49 单测绿 + e2e 四件套stub 基线) | verified`BUILD.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 `createEngineCaps``host.js:197-318` 删,`host.js:204/359` 改) |
| `games/wanglanmei-ref/entry.js` | 改 | `entry.js:15``import * as LJS from 'littlejsengine'` |
| `games/wanglanmei-ref/src/main.js` | 改real 通道) | `main.js:112`engineFactoryreal 须延后到 engineInit().then 内建·§12-M4/`617-661`frame→game.update/render/`648`drawFrame g→mainContext/`668-691`RAF 门控)/`618`mockNowMs/`583-602`real 输入改挂 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. 数据流
```mermaid
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/M4**`createHostDevContext`bundle`game.init`、输入桥挂 `mainCanvas`、就绪标记 `__refBootedReal=true` **全部在 `engineInit().then(setupReal)` 内**(上图 `.then() → mainContext 就绪` 之后)——引擎掌帧的必然结构:引擎先起、`mainContext`/`mainCanvas` 才有;顶层提前建 bundle 会快照错 ctx`host.js:323-324`)。
- stub与今天 `main.js` 逐行等价(回滚锚);就绪标记仍用 `index.html``__refBooted``index.html:81`)。
---
## 5. 装载契约 IDL新立 additive 第 9 类契约 · `game-runtime/src/core/game-host.d.ts`
> **设计原则**:游戏 = 纯模块工厂(零引擎 importQ4。host 经 `engineInit` 五回调驱动游戏生命周期;游戏绘制面 `render(g)``g` = 引擎 `mainContext`boot 注入);插件能力经下层已冻结 `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
```typescript
// 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. **绘制面唯一=引擎 `mainContext`**`render(g)` 的 g 必为 boot 注入的 `mainContext`;游戏美术与引擎粒子同屏。**推论§12-M4**real 通道 `bundle` 必须在 `engineInit().then` 内、以 `context2d=mainContext` 建(受控面 getContext2d 一次性快照闭包捕获值,`host.js:323-324`);不得在顶层先建再改 ctx。
3. **能力唯一面=下层 `ctx.getEngine()`**:粒子/音频/easing 经它(不在本契约重复定义引擎能力面)。
**additive 关系明确**`game-host.d.ts``import type { PluginContext } from './api.d.ts'`,不修改 `api.d.ts` 任何导出;受控面 6 项零改。
---
## 6. 关键实现步骤(有序)
### 步骤 0核前置执行首步read-only · 常规核对)
- mini-desktop scratch clone 后核 `games/wanglanmei-ref/entry.js``import * 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` 改:删内联 `makeEngineCaps``197-318`),改 `import { createEngineCaps } from './engine-caps.js'`;在 `buildBundle``host.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.js``export default createShopGame``GameHostFactory` 形态),**零引擎 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*1000``core.update(dtWorld)``bundle.tick(dt)``applyEvents``maybeReplayBufferedServe``fx.step`rush 联动闲时台词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 一次性快照闭包捕获的 ctx`host.js:323-324` 注释明确「读闭包捕获值,故必须就绪后建 bundle」**绝不可在顶层先用 `#game` ctx 建 bundle 再改**,否则 getContext2d 快照到 stub `#game` ctx受控面 context2d 与游戏 render 面分裂(= spec 要消除的渲染面分裂,会换形式复现)。可直接引 `host.js:353-371`buildBundle+ `host.js:379-381`setupAfterEngine 在 then 内建 bundle作样板。
- 建 game 工厂 → `game.init({ ctx: bundle.context, mainContext, canvas: LJS.mainCanvas, seed })`(异步,`init` 内 await loadAssets
- 挂 `window.__refHost``window.__refForensics``window.__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 `__hostBootedReal``host.js:920`)。**这是 real 通道唯一就绪信号**——必须在 engineInit().then 内、game.init/资产/受控面全就绪后置。**stub 通道照旧用 `index.html``__refBooted`bootRef().then 置,`index.html:81`real 通道额外置 `__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 范式)。
- **mockNowMs**real 下由 `game.update``mockNowMs += dt*1000` 累加(逻辑帧钟,非墙钟,同 `host.js:526`)。
- **RAF 门控**`startRaf/rafLoop``main.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 : canvas``toCanvasXY``getBoundingClientRect` 改读 `LJS.mainCanvas`。监听挂载 + `inputBridge` 取用同样移进 `setupReal`bundle 就绪后才有 `bundle.inputBridge`)。经 `bundle.inputBridge._emit` 灌受控面;游戏经 `boot.ctx.getInput().on` 订阅(同 `main.js:604-611`,订阅在 game 工厂 `init` 内)。**stub 通道输入逐行不变**(挂 `#game``main.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探针绝不入渠道产物物理保证
- `createEngineCaps``recHook` **默认 no-op**`engineFactory` 默认不传 recHook。
- **王蓝莓真玩取证**:取证脚本经 URL/全局开关让 main.js real 分支注入 `recHook = id => window.__engineCalls.push(id)`(仅取证路径)。
- **渠道产物(未来 P2/P3 出仓 bundle**esbuild 入口不引取证注入点 → recHook 恒 no-op → 探针代码无副作用、不挂 `window.__engineCalls`。**P1 阶段 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) **就绪标记M1**`waitRefBoot``engine=real` 时改等 **`window.__refBootedReal`**(而非 `__refBooted`。real 下 `__refBooted``index.html:81` bootRef().then 置)会在引擎/game 就绪前被置 truebootRef 顶层同步段先 resolvedriver 据它驱动会打空/读 undefined → 假失败或间歇绿。host-dev 正是为此引入 `__hostBootedReal``host.js:920`注释「CDP 取证须等 __hostBootedReal 再驱动」`host.js:851/931`)。新分支:`evaluate('({ b: !!window.__refBootedReal, e: window.__refBootError||null, h: !!window.__refHost })')`
(b) **坐标 rect 选择器M3**`initMapping``document.getElementById('game')``:127`)在 `engine=real` 时切到 **`document.getElementById('game-engine')`**(引擎自建可见画布,`host.js:830`real 下 `#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=evidence`real 取证不靠 stepFrames靠真触摸+轮询)。
(d) **同屏像素扫描**:合 gate0 G0-7 + a6 A6-5 像素扫描CANVAS=`#game-engine`,判据见 §8 门②(暖色阈值已开工前坐实,见门②)。
---
## 7. 边界失败路径
| 失败 | 触发 | 处置 |
|---|---|---|
| 引擎 engineInit 失败 | WebGL/资源异常 | 镜像 `window.__refBootError`(仿 `host.js:925`status 提示"可加 `?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-required`harness §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-A4**`git clone --depth 50 --branch dev/2.0.0 ssh://git@101.200.34.71:2222/...``git checkout <精确SHA>` 到 scratch`ref-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-required``ref-e2e-runbook.sh:48`harness §5。**real 版只需在 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
```
**真玩 URL**`http://localhost:4320/?engine=real&seed=305441741`(取证模式 `&mode=evidence``__refHost` 确定性驱动;真人交互去掉 mode
| 门 | 判据(可执行阈值) | driver / 口径 |
|---|---|---|
| **① 掌帧** | `window.__engine.snapshot()` 两采样间隔 500ms`frame` 增量 ∈ `[20,45]`@60fps ~30排单步假推进+失控空转) | 复用 gate0 G0-2`gate0-engine-takeover.cjs:75`ref driver 加同款断言ref 暴露 `window.__engine` |
| **② 同屏像素(堵假阳性)** | 截 `#game-engine`(引擎接管后 DOM 可见、=mainContext 的 canvas`host.js:830`)。**两类命中均 >0**(a) **游戏美术·暖色(开工前已坐实,非占位)**:店面暖色主色像素命中数 `warmHits > N`N=200按 780×1688 抽样网格调,详见下「门②(a) 暖色判据」);(b) **引擎粒子·亮色**burst 白 `brightHits>0``maxCh>80`a6 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.__engineCalls``particles.spawnEmitter` call-ID经 particles-juice 真实 spawnEmitter非直调引擎 | 复用 a6 A6-2`a6-engine-wiring.cjs:79`ref driver 触发游戏内真粒子serve_ok 飞行/burst后读 `__engineCalls` |
| **④ CDP 真玩 + 四件套** | CDP 真触摸逐 zone 真玩闭环(开店→进货→营业→结算→升级),首日接待数>0、serve_ok>0、到达结算、升级 `upgrades.speed1=true`;四件套 evidence输入轨迹 JSONL/截图+canvas 哈希/probe JSONL/checklist JSON`evidence/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 driver`main.js:90-96` uncaught 归集) |
> **门④ driver 范式(复审 M2**real 通道 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+30``gate0-engine-takeover.cjs:111`a6 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:line`main.js:112/583-602/618/621/648/668-691``stepFrames` 手摇 `:664/:109`host.js real 范式(`host.js:323-324/353-371/379-381/488-490/536-538/830/840/904/917-920/930-932`,缺省 real `:340`index.html `__refBooted` 置位时机(`index.html:80-81`driver 现状(`e2e-input.cdp.cjs:76/109/127/144/236/277`makeEngineCaps 闭包(`host.js:197-318`+ engineFactory 注入(`host.js:359`burst colorStart 白(`particles-juice/impl.js:217`gate0 青蓝 `:111` / a6 亮度 `:111`palette 暖色 BASE`palette.js:23-41`rush shiftHsl `:57`zone kind 枚举(`render.js:967/975/981`ref runbook git-clone-scratch`ref-e2e-runbook.sh:20-21/44/47/48`contracts 跨仓 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/57``gate0:111``a6: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-538``main.js:112/584/590-602` |
| **A1** | IDL 落 contracts/ 并反向相对 import `../game-runtime/src/api.d.ts`import 方向/落点合理性) | **改落点**:落 `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:340`host-dev realvs `main.js:82`ref 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-scratch**real 版只在 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 已过 |

View File

@ -0,0 +1,171 @@
# 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`](./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` + `destroy`boot 时把引擎 `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 双通道范式)。
**同时把已存在但仍是闭包的 `makeEngineCaps``host.js:197-318`)提升为可 import 模块 `host-dev/engine-caps.js`**host-dev / ref / 未来生成游戏共用同一份引擎能力包装层——**这是"模块化已有件"(搬运),非从零造**arc §0-F2 已订正原稿"engine-caps.js 不存在=真新增工作")。
**装载契约的本质O1 已拍形态)**:一款游戏 = 一个**纯模块工厂**(零引擎 importQ4 铁律),暴露稳定生命周期 `init(ctx, mainCtx)` / `update(dt)` / `render(g)` / `onInput(ev)` / `destroy()`;其中 `render(g)``g` = 引擎 `mainContext`boot 时注入),插件能力仍经下层已冻结的 `ctx.getEngine()` 拿。该契约落 **`game-runtime/src/core/game-host.d.ts`**(与既有 `api.d.ts` 同目录,复审改点;理由见上「复审改点」),与 `api.d.ts`**additive 关系**——**受控面 6 项契约零改**`getContext2d/getInput/getAudioContext/getEngine``api.d.ts:269/281/286/303`)。**game 工厂必须是独立、可被 P3 复制的实现**(独立文件默认导出、零引擎 import禁 main.js 内联糊过——这是 P3 生成 agent 的真实参照靶,复审钉死的硬边界)。
> ⚠️ **为何必须新立、不能借 `PluginContext`**ref 的游戏层(`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/ 是跨三仓五工位的**业务契约** SSOT`contracts/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/Render``host.js:883-887``destroy` 补卸载卫生P2 装载多游戏时复位)。其余(`resize/onVisibility`additive 后补。 |
| **绘制面约定** | `render(g)``g` = 引擎 `mainContext`**boot 时注入游戏层** | A0 已证 `setGLEnable(false)` 后引擎纯 2D 渲进 `mainContext``host.js:904/919`);游戏绘制面改它即与引擎粒子同屏(消除渲染面分裂)。**不借 `PluginContext`**。 |
| **能力获取** | 插件能力经下层已冻结 `ctx.getEngine()`;游戏绝不 `import littlejsengine` | Q4 铁律:引擎 import 只活 entry/host`entry.js:18-21`/`host.js:44`)。 |
**装载关系图(游戏 ↔ host ↔ 引擎)**
```mermaid
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=引擎 `mainContext`boot 注入),由 `gameRender` 回调驱动 |
| **插件受控面 context2d** | host `prepareCanvas(#game)` 的 ctx | 快照引擎 `mainContext``createHostDevContext``context2d` 改它) |
| **粒子能力** | `engineFactory` 未传 → `getEngine()`=null → 粒子 `NOOP_HANDLE`(无粒子) | 经 `engine-caps.js``engineFactory` → 粒子真经引擎 `ParticleEmitter` 自渲落 `mainContext` |
### 3.2 最大风险(先标在最前)
| 风险 | 等级 | 缓解 / 回滚 |
|---|---|---|
| **真渲同屏过不了 / 假阳性(粒子渲到另一张分离画布,游戏画布无粒子命中)** | **P0** | 这是 P1 第一道真门,也是全弧第一道门。判据必须堵死假阳性:**截取引擎接管后 DOM 可见那张 canvas=`mainContext` 的 canvasA0 已给它打 id `#game-engine``host.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 下 `__refBooted``index.html:81` bootRef().then 置)早于引擎就绪;补 real 专属 `__refBootedReal`(只在 engineInit().then 内 game.init 完成后置,仿 host-dev `__hostBootedReal` `host.js:920`driver 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=mainContext`getContext2d 一次性快照闭包捕获值,`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-op`ref-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-2`gate0-engine-takeover.cjs:75` |
| **② 同屏像素** | **同一张画布(`#game-engine`)上**:游戏美术暖色命中(`warmHits>200`,色域 `r>140 && rb>85 && g<r`**开工前坐实非占位**·M3∧ 引擎粒子像素命中burst 白 `maxCh>80``brightHits>0`);粒子渲到另一画布则游戏画布命中=0=fail | a6 A6-5`maxCh/brightHits`+ 暖色域自 `palette.js:23-41`execution §8「门②(a)」) |
| **③ 真接线门②** | `window.__engineCalls``particles.spawnEmitter` call-ID插件运行时真调引擎 | a6 A6-2`a6-engine-wiring.cjs:79` |
| **④ CDP 真玩** | CDP 真触摸逐 zone 真玩(开店→进货→营业→结算→升级)+ 四件套 evidence**driver 范式=真触摸+轮询,禁 stepFrames 手摇·M2**rect 选择器 `#game-engine`、就绪等 `__refBootedReal`zone 名以 `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 e2e`uncaught=[]` |
---
## 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.html`real 通道接 `__refBootedReal`M1 | 就绪信号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
```mermaid
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 D**O2 已拍。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-887`additive 关系成立(受控面 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=real``host.js:340`ref 缺省 `engine=stub``main.js:82` play 模式走 stub 通道)。**有意不统一**:真玩取证显式带 `&engine=real` 更稳、stub 兜逐像素回归;但须知 P3 复制 ref 作模板时,缺省 stub=「新游戏默认不上引擎」是过渡现状,**引擎是正路**boot 注入装载契约)——这点 P3 spec 须承接。
- **[verified]** 装载契约全仓不存在(`grep GameModule/装载契约/engineInit``contracts/`+`src/` 零命中);既有契约 = `PluginContext``api.d.ts:269/281/286/303`6 受控面 + `getEngine()`+ `packageUrl``game-package.schema.json:66`)。
- **[verified]** `engine-caps.js` 文件不存在,但内容已大半存在:`makeEngineCaps` 闭包(`host.js:197-318`)已包装 particles/audio.synth/math 三面并已注入 `engineFactory``host.js:359``engine-math.js` 已独立成模块(`host.js:47` import。→ 提升=搬运。
- **[verified]** ref 游戏层 `core.update``main.js:621`/`drawFrame(g)``main.js:648`g=`prepareCanvas(#game)`/RAF`main.js:668-691`/`mockNowMs` 步进钟(`main.js:618``createHostDevContext` 仅 4 键无 `engineFactory``main.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`+ 输入挂 mainCanvas`host.js:536-538`+ `setGLEnable(false)``host.js:904`+ 双时钟 `window.__engine``host.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:...}``toEngineSpec``particles-juice/impl.js:217`),故引擎粒子像素门判 `brightHits/maxCh`青蓝口径已对引擎渲染失效harness §6-2游戏美术暖色门另判暖色域M3`palette.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不变**。改名零成本,不阻断执行。

View File

@ -0,0 +1,184 @@
# 《W-G1 · 20 经典轻游戏基准》设计稿(review 版)
> **状态**:review 版 · 2026-06-13 · 只读调研产出(未改任何运行时代码)。
> **定位**:本基准是"**便宜模型(M3/M2.7/DS-V4)+ 当前引擎(LittleJS)/插件库**"能产出什么的**实测靶**。20 款经典轻游戏铺满机制谱系,既验生成管线打通,又压测能力上限、**暴露引擎/插件库短板**。
> **调研口径**:覆盖判定**只认源码导出**(api.d.ts 手写门面 + impl.js 实际 return 面 + host.js `makeEngineCaps()`),不认"我以为有"。证据锚点全部带 file:line。
> **关联**:边界模型见 memory `engine-plugin-boundary-model`;本基准是 Runner v2 全弧 P3(W-G1)的靶集,见 `2026-06-13-runner-v2-arc-review.md`
---
## 结论先行(给创始人的三句话)
1. **能打的多数**:20 款里约 **12 款 ✅当前插件/引擎已直接覆盖**(井字棋/2048/Flappy/打砖块/贪吃蛇/Pong/扫雷/Simon/Snake 类…),便宜模型在现有能力面上应能产出可玩件——这批先做冒烟,验管线。
2. **缺口集中在 4 类能力**(本基准核心产出):**① 文本/分数 HUD 渲染**(无插件门面)、**② 网格/棋盘状态抽象**(无 grid/tilemap)、**③ 连续碰撞 CCD**(collision 仅离散 MTV,高速小球会穿透)、**④ 通用状态机 FSM**(gamefeel 仅 3 个特化计时器)。这 4 类决定 Tetris/Asteroids/Breakout-高速/Match-3 的产出质量。
3. **真短板不在"造不出",在"agent 要自己补层"**:上述①②④引擎/插件其实有底层原语或属 agent 生成域(LJS.drawText 存在但未包装、网格状态属 agent 生成域),但**没有插件门面=每款游戏的便宜模型都要重造一遍轮子**→ 一致性差、token 贵、易错。**建议:W-G1 实测后,把高频缺口(文本 HUD / 网格)补成插件门面**,而非让 20 个 prompt 各自硬写。
---
## A. 事实基:当前能力面盘点(verified,带证据锚点)
### A.1 插件库实际导出(7 个业务插件 + 1 example)
| 插件 | 公开能力(源码实际 return 面) | 证据锚点 |
|---|---|---|
| **collision** | 离散几何相交:`circleVsCircle`/`circleVsAabb`/`aabbVsAabb`/**`satVsPolygon`(凸多边形+MTV)**;射线 `raycastCircle`/`raycastAabb`(RayHit);**`SpatialHash`(均匀网格 broadphase 粗筛)** | `collision/impl.js:268/295/337/395/462/500``SpatialHash:112-264`;api.d.ts:`Manifold`/`RayHit:80/97` |
| **physics-lite** | 离散运动数学:`integrateProjectile`/`Projectile`(抛体+重力)、`springStep`/`criticalDamping``moveWithConstraint``clampSpeed`/`applyFriction` | `physics-lite/impl.js:104/127/181/194/241/268/286` |
| **particles-juice** | `spawnEmitter`(preset/config)、`hitStop``shakeScreen``flashScreen``pulseScale``getPreset`;渲染输出经受控面偏移 | `particles-juice/impl.js:438/504/524/544/562``getPreset:163`;api.d.ts façade `:204-240` |
| **gamefeel** | `InputBuffer``CoyoteTimer``ComboWindow`、**easing 标准库 11 条**(纯函数,经引擎门面) | `gamefeel/impl.js: InputBuffer:232/CoyoteTimer:319/ComboWindow:390/makeEasingFacade:205`;api.d.ts `:108/154/196` |
| **audio-music** | `playSfx`(blip/thud/chime)、`play`/`loop`/`stop`(BGM)、`setIntensity`/`setIntensityLayering``loadSong` | `audio-music/impl.js:438/455/463/467/471/481`;preset `:71-75` |
| **palette-post** | 调色板映射 `applyPaletteMap`/`lerpPaletteMap`、HSL `shiftHsl`/`rgbToHsl`/`hslToRgb`、后处理 `setPost`(vignette/dither/scanline)/`renderPost` | `palette-post/impl.js:142/210/238/62/90/392/412`;dither `:435` |
| **save-progress** | KV 持久化 `get`/`set`/`remove`/`clear`(命名空间隔离+损坏容错+单条上限) | `save-progress/impl.js:219/243/285/294` |
| **runtime-probe** | 取证埋点 `getRecords`/`toJSONL`/`verify`(哈希链)— 非玩法能力,e2e 证据用 | `runtime-probe/impl.js:330/335/340` |
### A.2 `makeEngineCaps()` 引擎透传真实粒度(host.js,只此三面)
| 面 | 真实粒度 | 证据锚点 |
|---|---|---|
| **particles** | `spawnEmitter(spec)` 薄包装引擎 `ParticleEmitter`(27 参全映射),像素↔世界阻抗换算 | `host.js:213-273`(构造 `:243-266`) |
| **audio.synth** | `synthSfx`(包 `zzfxG`)/`synthSong`(包 `zzfxM`)→ PCM 样本(不播放,×0.3 播放层补) | `host.js:279-316` |
| **math** | `lerp`/`smoothStep` + `easing` 11 条等价曲线(POWER/BACK/ELASTIC 族),纯函数 | `host.js:204-209` + `engine-math.js:56-87` |
> 引擎本体(LittleJS)远不止这三面(有 `drawText`/`drawTile`/`tileCollision`/相机等),但**插件墙内只透出这三面**;其余引擎能力**不构成 agent 的能力词汇**(一职一路,见边界 memory)。
### A.3 受控面 6 项(api.d.ts:264-304,引擎可换边界)
`getContext2d()` / `onFrame()` / `getInput()`(归一化输入:pointer 三态+key 两态)/ `getAudioContext()` / `time`(受控时钟)/ `random`(确定性随机)。**输入只有 5 类事件**(`api.d.ts:218`),**无手势识别**(swipe/拖拽需 agent 在生成域自合成 pointer 序列)。
### A.4 模板哲学(决定"什么算覆盖")
`tech-decisions.md:38` 终裁:**玩法/美术/关卡/UI = agent 写码生成域**;插件=能力 API。故"覆盖判定"口径:**该机制所需能力,是否有插件门面或引擎透传面兜底**;纯玩法逻辑(回合制、计分、棋盘状态机)是 agent 自写域,**不算缺口**——除非它需要的底层原语(文本渲染、网格抽象)无门面。
---
## B. 20 款经典轻游戏基准表
> 覆盖判定:**✅** 插件/引擎透传面已覆盖 | **🟡** 引擎有底层原语/属生成域但无门面(agent 须自写补层,高频则建议补门面)| **❌** 缺口(无门面且 agent 自写成本高/易错)
> 难度档:**易**=单机制+静态/网格 | **中**=2-3 机制叠加+实时 | **难**=多机制+连续物理/程序化/高频碰撞
| # | 游戏 | 核心机制 | 所需能力清单 | 当前覆盖判定 | 难度 | 一句话生成种子 |
|---|---|---|---|---|---|---|
| 1 | **井字棋** | 3×3 落子棋/胜负判定 | 网格状态、tap、文本"X/O"、胜负 AI | 🟡 网格/文本🟡、输入✅、AI=纯逻辑 | 易 | 3×3 棋盘点击落子 X,AI 落 O,三连判胜 |
| 2 | **2048** | 网格滑动合并/数字 | 4×4 网格、swipe、文本数字、合并逻辑 | 🟡 网格+文本🟡(高频缺口)、swipe=合成、合并=纯逻辑 | 中 | 4×4 数字方块,滑动合并同值,生成新块,显分 |
| 3 | **贪吃蛇** | 网格移动/自增长/自碰撞 | 网格、方向键、自碰撞(网格等值)、计分 | 🟡 网格=agent 自管、输入✅、碰撞=网格逻辑、计分文本🟡 | 易 | 蛇沿网格吃食物变长,撞墙/撞自己结束 |
| 4 | **Flappy Bird** | 重力跳跃/管道避障/无尽 | 重力(抛体)、tap、AABB 碰撞、程序化管道、计分 | ✅ physics 抛体`:104`、collision `aabbVsAabb:337`、输入✅;文本🟡 | 中 | 点击让小鸟上跳,重力下落,穿随机高度管道 |
| 5 | **打砖块 Breakout** | 弹球反弹/挡板/砖块网格 | 弹球物理、AABB/圆碰撞、挡板输入、砖块网格、反弹角 | ✅ collision `circleVsAabb:295`+MTV 反弹、physics 限速;高速球🟡 | 中 | 挡板接弹球击碎砖阵,球随挡板位置改反弹角 |
| 6 | **Pong** | 双挡板/弹球/简单 AI | 弹球、AABB 碰撞、挡板输入、AI 追球 | ✅ collision/physics/输入✅;AI=纯逻辑 | 易 | 左玩家右 AI 各控挡板,漏球对方得分 |
| 7 | **扫雷** | 网格揭示/数字提示/标记 | 网格、左右键 tap、文本数字、洪水填充 | 🟡 网格+文本🟡、输入✅、洪水填充=纯逻辑 | 中 | 点击揭示显周围雷数,右键标记,踩雷结束 |
| 8 | **Simon 记忆** | 序列记忆/颜色按钮/渐进 | 序列状态机、tap、音效反馈、计时 | ✅ audio `playSfx:481`、gamefeel 计时、输入✅;序列=纯逻辑 | 易 | 四色按钮按序闪烁播音,玩家复现,逐轮加长 |
| 9 | **记忆翻牌** | 翻牌配对/网格 | 网格、tap、翻转动画(easing)、配对逻辑 | ✅ easing`:205`/输入✅;网格🟡、图案=agent 绘 | 易 | 牌面朝下,翻两张配对,全配对获胜 |
| 10 | **太空侵略者** | 波次敌阵/射击/碰撞 | 敌阵网格移动、射击 tap、子弹 AABB、波次、HUD | ✅ collision AABB+SpatialHash`:112`、particles 爆炸;文本🟡 | 中 | 炮台左右移动射击,敌阵整体平移下压,清屏过关 |
| 11 | **俄罗斯方块 Tetris** | 下落方块/网格堆叠/消行 | 网格(堆叠态)、方向键旋转移动、消行、计时下落、文本 | 🟡 网格堆叠=agent 自管(**高频痛点**)、输入✅、文本🟡 | 难 | 七种方块下落,旋转堆叠,满行消除,逐级加速 |
| 12 | **小行星 Asteroids** | 惯性飞船/任意角度/程序化碎裂 | 惯性物理、旋转射击、多边形碰撞、程序化分裂、环绕 | ✅ collision `satVsPolygon:395`+RayHit、physics `applyFriction:286`;高速子弹 CCD❌ | 难 | 飞船惯性漂移射击,大陨石击中裂小块,屏幕环绕 |
| 13 | **消消乐 Match-3** | 网格交换/三连消除/下落填充 | 网格、swipe 交换、三连检测、重力下落、连锁、HUD | 🟡 网格+文本🟡(**高频痛点**)、swipe=合成、消除=逻辑、easing✅ | 难 | 8×8 宝石网格,交换凑三连消除,上方填充连锁 |
| 14 | **跳跃者 Doodle Jump** | 垂直无尽/平台跳/重力 | 重力跳(抛体)、左右倾斜、平台 AABB、程序化平台、相机 | ✅ physics 抛体`:104`、collision AABB、coyote`:319`;相机=受控面、文本🟡 | 中 | 持续上跳踩平台,左右移动,平台程序化生成 |
| 15 | **愤怒小鸟(简版)** | 抛射弹道/拖拽蓄力/碰撞倒塌 | 抛体弹道、**拖拽手势**、多体碰撞、弹簧/冲量 | 🟡 physics 抛体✅/弹簧✅、collision MTV✅;**拖拽=agent 合成 pointer 链** | 难 | 拖拽弹弓蓄力发射,抛物线击中结构使其倒塌 |
| 16 | **跑酷 Runner** | 横向无尽/跳跃滑铲/障碍 | 自动横移、tap/swipe 跳铲、AABB、程序化障碍、加速、HUD | ✅ physics 跳、collision AABB、buffer`:232`+coyote;swipe=合成、文本🟡 | 中 | 自动向右跑,点击跳跃避障,距离计分,逐渐加速 |
| 17 | **打地鼠** | 随机出现/限时 tap/计分 | 随机定时生成、tap 命中(点 vs 圆)、计时、计分、反馈 | ✅ random✅、collision 点/圆判、particles 命中;文本🟡 | 易 | 地鼠随机洞口冒出,限时点中得分,超时消失 |
| 18 | **见缝插针** | 旋转/时机 tap/碰撞检测 | 旋转角积分、tap 时机、角度/扇区碰撞、渐进 | ✅ math 角度、collision `raycastCircle:462`、particles;文本🟡 | 中 | 中心圆持续转,点击射针,针不能相撞,逐根加针 |
| 19 | **节奏点击** | 时间窗判定/节奏/连击 | 时间窗(combo)、tap 精度、音效、连击计分、easing | ✅ `ComboWindow:390`+`InputBuffer`、audio、easing;文本🟡 | 易 | 音符落判定线时点击,perfect/good 分级,连击加成 |
| 20 | **弹球变体(多球)** | 多球/道具掉落/物理反弹 | 多体弹球、圆/AABB 碰撞、道具 AABB、屏震闪白 juice、HUD | ✅ collision 圆+AABB、physics 限速、particles `shakeScreen:524`/`flashScreen:544`;高速多球 CCD🟡 | 难 | 挡板接多球击砖,砖落道具(增球/扩板),命中屏震闪白 |
---
## C. 能力覆盖审计(本基准核心价值)
### C.1 20 款合计考到的能力维度 × 谁兜
| 能力维度 | 由谁兜 | 覆盖状态 | 涉及款数 |
|---|---|---|---|
| 离散几何碰撞(圆/AABB/多边形 MTV) | **collision** `:268-431` | ✅ | 4,5,6,10,12,15,17,18,20 |
| 射线命中(RayHit) | **collision** `:462/500` | ✅ | 12,18 |
| 空间粗筛(broadphase) | **collision** `SpatialHash:112` | ✅ | 10,20 |
| 抛体/重力 | **physics-lite** `:104` | ✅ | 4,12,14,15,16 |
| 摩擦/限速/惯性 | **physics-lite** `:286/268` | ✅ | 12,16 |
| 弹簧阻尼 | **physics-lite** `:194` | ✅ | 15 |
| 输入缓冲/郊狼/连击窗 | **gamefeel** `:232/319/390` | ✅ | 8,14,16,19 |
| easing 标准库(11 条) | **gamefeel/math 门面** `:205` | ✅ | 9,11,13,18,19 |
| 粒子/屏震/闪白/命中停顿 | **particles-juice** `:438/504/524/544`+引擎透传 | ✅ | 10,12,17,20 |
| 音效/BGM/强度分层 | **audio-music** `:455-481`+引擎透传 | ✅ | 8,19,20 |
| 调色/后处理 | **palette-post** `:392/412` | ✅(可选) | 全部可选 |
| 存档/进度 | **save-progress** `:219-294` | ✅ | 高分榜可选 |
| tap/方向键输入 | **受控面 getInput** `:218` | ✅ | 全部 |
| 确定性随机/程序化 | **受控面 random** `:35` | ✅ | 4,7,10,11,13,14,16,17 |
| **文本/分数 HUD 渲染** | **无插件门面**(引擎 drawText 未透出) | **🟡 缺门面** | 几乎全部 |
| **网格/棋盘状态抽象** | **无 grid/tilemap 门面**(agent 生成域自管) | **🟡 缺门面** | 1,2,3,7,9,11,13 |
| **连续碰撞 CCD(swept)** | **collision 仅离散 MTV**(grep swept/ccd 全空) | **❌ 缺口** | 5,12,20(高速小目标) |
| **通用状态机 FSM** | **无**(gamefeel 仅 3 特化计时器) | **🟡 agent 自写** | 8,11、回合制类 |
| **手势识别(swipe/拖拽)** | **无门面**(agent 合成 pointer 序列) | **🟡 自合成** | 2,13,15,16 |
| **寻路 A*/导航** | **无**(grep astar/pathfind 全空) | **❌ 缺口(已规避)** | (完整 Pacman 鬼 AI——故未入选) |
### C.2 点名"会暴露能力缺口"的款 + 缺口性质
**❌ 真缺口(无门面+agent 自写成本高/易错):**
1. **连续碰撞 CCD(swept)** — 暴露款 **#5 高速球/#12 子弹/#20 多球**。
- 性质:collision 只有离散 MTV/穿透判定(`circleVsAabb:295`),无 swept/toi(grep `swept|ccd|continuous|toi|tunnel` 全空;`impl.js:21` 自述能力止于"MTV/穿透/RayHit")。
- 后果:**高速小球穿砖/穿墙(tunneling)**,便宜模型多半产出"球偶尔卡进/穿透"的 bug 件。
- 绕法:agent 用 `raycastAabb:500` 沿速度向量手做 toi(可行但每款重写、易错)→ 高频则补 collision swept 门面。
2. **寻路 A*/导航网格** — 本基准**已主动规避**(完整 Pacman 鬼寻路未入选)。
- 性质:grep `astar|pathfind|navmesh` 全空;`SpatialHash` 只做粗筛非寻路。
- 结论:**任何需要敌人智能寻路的品类当前造不出**=引擎/插件库明确边界。需要时须新增寻路插件。
**🟡 半缺口(底层原语存在/属生成域,但无门面→每款重造轮子):**
3. **文本/分数 HUD 渲染** — 暴露款 **几乎全部**(2/3/7/10/11/13/16…)。
- 性质:无任何文本渲染门面(grep `drawText|renderText` 全空);引擎 `LJS.drawText` 存在但未经插件透出,`makeEngineCaps()` 三面不含文本(`host.js:210-317`)。
- 后果:每个游戏的便宜模型都得自己 `ctx.getContext2d().fillText` 手画 HUD——风格各异、字体/对齐易错。
- **最高频缺口**:20 款里 ~15 款需文本 → **强烈建议补 text/HUD 门面插件(W-G1 实测后第一优先)**
4. **网格/棋盘状态抽象** — 暴露款 **#1/2/3/7/9/11/13**。
- 性质:无 grid/tilemap 门面(grep `tilemap|gridMap` 全空);属 agent 生成域,但坐标↔索引/邻接/遍历/合并每款重写。
- 后果:2048/Tetris/Match-3 网格状态机是核心难度,便宜模型自管易出"合并方向/消行/连锁"bug。
- 建议:W-G1 观察便宜模型自写网格出错率;若高,补 grid 工具门面。
5. **手势识别(swipe/拖拽)** — 暴露款 **#2/13(swipe)/#15(拖拽)/#16**。
- 性质:受控面 `getInput` 只给 5 类原始事件(`api.d.ts:218`),无 swipe/drag 合成。
- 后果:agent 须自缓存 pointerdown→move→up 算方向/距离;愤怒小鸟拖拽蓄力尤其易错。
- 建议:swipe/drag 是触屏轻游戏刚需 → 可补进 gamefeel(已管输入缓冲,自然归属)。
6. **通用状态机 FSM** — 暴露款 **#8 Simon/#11 Tetris** 及回合制类。
- 性质:gamefeel 仅 3 个特化计时器,无通用 FSM 原语。
- 后果:多状态玩法自写状态切换易乱;属生成域,优先级低于文本/网格(纯逻辑模型相对擅长)。
### C.3 一句话审计结论
**20 款里 ~12 款能力面齐备(✅),可立即冒烟;余 8 款集中暴露 4 类短板。最痛的不是"造不出",而是"文本 HUD + 网格状态"两个高频能力没有门面——便宜模型 20 个 prompt 各造一遍轮子,一致性、token、出错率三重受损。真·造不出的硬边界只有两条:连续碰撞 CCD(高速穿透)与寻路(已规避)。**
---
## D. 分批/分档执行建议
> 原则:先用能力面齐备的易档验管线打通(冒烟),再用中/难档+缺口款压测能力上限、量化便宜模型在缺口处的产出质量。
### 批次 1 · 冒烟批(验管线打通)— 6 款 · 全 ✅ · 难度易
**#6 Pong / #8 Simon / #17 打地鼠 / #9 记忆翻牌 / #3 贪吃蛇 / #19 节奏点击**
- 理由:能力面 100% 齐备,机制单一。目标=证明"便宜模型+插件库+new-api+harness 门"端到端跑通,产可玩件。
- 验收:管线零阻塞 + ≥4/6 过好玩基线评估门。
### 批次 2 · 主力批(验常见机制覆盖)— 8 款 · ✅ 为主 · 难度中
**#4 Flappy / #5 打砖块 / #10 太空侵略者 / #14 Doodle Jump / #16 跑酷 / #7 扫雷 / #18 见缝插针 / #1 井字棋**
- 理由:覆盖物理/碰撞/波次/程序化/网格主流机制,带少量 🟡(文本/网格/swipe)。
- 核心产出:**量化文本 HUD/网格/swipe 三个高频 🟡 的便宜模型自写出错率**——"是否要补门面"的决策数据。
- 验收:≥6/8 可玩;逐款记录 agent 在 🟡 缺口处的补层方式与 bug。
### 批次 3 · 压测批(测能力上限+暴露缺口)— 6 款 · 含 ❌/难 🟡 · 难度难
**#11 Tetris / #13 Match-3 / #12 Asteroids / #15 愤怒小鸟 / #20 多球弹球 / #2 2048**
- 理由:每款压一/多个短板——Tetris/Match-3/2048=网格状态机上限;Asteroids/多球弹球=CCD 缺口;愤怒小鸟=拖拽手势缺口。
- 核心产出:**证实 C.2 缺口判定**——便宜模型在 CCD/网格/拖拽处的产出质量(穿透 bug?消行 bug?蓄力 bug?),给创始人"引擎短板实测画像"。
- 验收:**不强求高通过率**(本批测上限);**重点产出缺口实证报告**→ 喂"是否补 text/grid/swipe/swept 门面"的决策。
### 跨批观测项(W-G1 真正要回答的问题)
1. 便宜模型(M3/M2.7/DS-V4)在三档的产出质量梯度 → 哪档开始崩,定能力天花板。
2. 四类短板的实测影响:文本/网格/CCD/拖拽各拖垮多少款 → 排补门面优先级(预判:文本 HUD > 网格 > swipe/拖拽 > CCD swept)。
3. harness 门的判定有效性:能否稳定逮住"穿透/消行错/不可玩"——门兜底是便宜模型路线的命门。
---
## 风险与假设(诚实标注)
- **[verified]** 全部覆盖判定基于源码导出(api.d.ts + impl.js return 面 + host.js makeEngineCaps),证据锚点见 A/B/C 各 file:line。缺口判定用"源码证伪法"(grep 关键原语全空+impl.js 自述能力边界),非按预期名臆断(遵 memory 红线 `engine-plugin-boundary-model` §1.5)。
- **[inferred]** "便宜模型能产出 ✅ 款的可玩件"是推断非实测——这正是 W-G1 要验的;本基准只确证"能力面齐备",不预断模型产出质量。
- **[inferred]** 难度档(易/中/难)是按"机制数 × 实时性 × 缺口数"的工程推断,非用户体感难度。
- **[assumption]** 好玩基线 v2 的具体五要素未在本次检索中定位到原文(`add-game-template.md:7` 仅言"改挂评估门"),验收门精确判据需在 W-G1 开工前从评估门 spec 取齐。
- **[未决]** 20 款最终取舍可由创始人按"想优先验证的机制谱系"调整;CCD/寻路两条硬边界是否本期补门面,建议**等批次 3 实测数据**再拍,不预先投入。

View File

@ -0,0 +1,296 @@
# Runner v2 全弧 · 顶层评审版HJ-RUNNER-ARC-001
> **状态**review 版 · 2026-06-13**对抗复审整改稿 r2**)· 给创始人决策(结论先行,不写代码级执行细节,多用 Mermaid
> **定位**:本文不是执行 spec。它确立 **一块基石("游戏挂引擎"装载契约解锁三相P1 spike → P2 泛化 → P3 生成)** 的顶层主梁,并给方案 A/B 收敛裁决。落地时各相另备 review+execution 双 spec。
> **前提(创始人已拍)**:① 推进范围 = **P1→P2→P3 全弧**,顺序解锁、我连续驱动;② **W-G1 前置 DeepSeek key 已就绪**W-G1 排进 P3。
> **关联**A0 地基 [[a0-engine-takeover-done]]、边界模型 [[engine-plugin-boundary-model]]、20 经典基准 `2026-06-13-WG1-20经典轻游戏基准-review.md`、母 spec `2026-06-13-T1b-β引擎真接线与Runner-v2-execution.md`、三契约冻结提交 `28a57b8`
> ### 复审整改说明r22026-06-13
> 本轮收到的对抗复审结论体为空JSON=null。为不空转主笔架构师**亲自对照 spec 所引源码做了一轮对抗核验**(全 verified证据带 file:line逮出 3 条致命 + 2 条重大并就地整改;下列整改均可被源码复证,整改清单见 §0。**这不替代一轮独立外部对抗复审**——本稿主梁已重锚,建议仍走一次外部复审确认再冻结分相 execution。
>
> **整改的主干裁决(一句话)**:原稿把"统一基石"命名为一份**尚不存在的 `GameModule` 契约**,且与既有 `PluginContext` 混为一谈——这本身就是它声称要避免的"双词汇/缝合"。整改把主梁**重锚到已冻结的真契约**`PluginContext.getEngine()` + `packageUrl`,母 spec P2 三契约,`28a57b8`),把"游戏怎么挂引擎"明确为**一份仍待起草的 additive 装载契约**(暂名"游戏宿主装载契约"),升级为创始人待拍 open item**不再当作既成事实断言**。
---
## 0. 对抗复审整改清单r2
> 自核 5 项3 致命 F1-F3 + 2 重大 M1-M2全部纳入。证据列均为可复证 file:line路径相对 `game-runtime/`,契约相对仓根 `contracts/`)。
| # | 级别 | 原稿问题 | 整改 | 证据(已复证) |
|---|---|---|---|---|
| **F1** | 致命 | 把统一基石命名为 `GameModule` 契约并当作"已统一的单一事实源",但**全仓不存在该契约工件**,且与既有 `PluginContext`(插件契约)混淆 → 实为"另造一套词汇",违背它自己声称的"勿缝合/同一职不留两条路"。 | 主梁重锚:**统一基石 = 母 spec P2 已冻结的真契约**`PluginContext.getEngine()` + GamePackage `packageUrl`)。"游戏如何挂引擎"作为**仍缺的一块 additive 装载契约**"游戏宿主装载契约")显式立项,**升级为 open item O1不再断言已成**。改 §1/§4。 | `grep GameModule``contracts/`+`src/core/`+`host-dev/` **零命中**;既有契约 = `api.d.ts:264-303 PluginContext`6 受控面 + `getEngine()`);母 spec L163「P2 三契约✅冻结」(`28a57b8`) |
| **F2** | 致命 | 断言"`engine-caps.js` 当前不存在=真新增工作"、"`makeEngineCaps` 仍是闭包待抽"。**已过时**A1/A4 已落地共享数学门面 + caps 工厂 + 注入。 | 修正基线:`engine-math.js` 已是共享模块、`makeEngineCaps` 已包装 particles/audio/math 且已注入 `engineFactory`。真实剩余工作**收窄为**"把 `makeEngineCaps` 闭包**提升为可 import 模块**供 ref/生成游戏共用",非从零造。改 §3.1/§4.3/§5.1/§7。 | `host-dev/engine-math.js` 存在(`buildEngineMath``host.js:47` import`makeEngineCaps` 已包装三面(`host.js:197-318`)且已注入(`host.js:359 engineFactory: makeEngineCaps`);提交 `e591e9b`/`3e5cab4` |
| **F3** | 致命 | "ref 与引擎同屏"被描述为"改主循环+换画布"。**漏了契约缺口**ref 的 `core.update`/`drawFrame(g)` 是**游戏层**(非插件),不经 `PluginContext`,今天没有"游戏层↔引擎 mainContext"的约定面——这正是 F1 缺的那块装载契约。 | 把"同屏"明确为**装载契约的核心待解项**:游戏层取引擎 `mainContext` 的方式必须由 O1 的装载契约定义,**不能借 `PluginContext`**那是插件面。P1 的真正产出 = **把这块装载契约 spike 实**,不止"换几行画布"。改 §1 风险/§4.1/§5.1。 | ref 插件经 `createHostDevContext``main.js:112`=PluginContext`drawFrame(g,…)`(`main.js:648`)/`core.update` 是游戏层,画在 `#game`(`main.js:83 prepareCanvas`),在契约面之外 |
| **M1** | 重大 | A/B"收敛"裁决建立在 F1 的虚构契约上,收敛性无法验证。 | 收敛裁决重新落在真契约A=P1 把"装载契约 spike 实(含 caps 模块化)"B=P2 复用同一装载契约 + `packageUrl` 加载链。收敛=共用 caps 模块 + 同一装载契约 + 同一 `packageUrl` 端点,**可被 import 图与端点验证**。改 §4.3。 | §4.3 重写 |
| **M2** | 重大 | O1生命周期粒度默认了"GameModule 是新契约"这一未明示假设。 | 把"装载契约是新立 additive 契约 / 还是折进既有面"本身列为 O1 主问题;生命周期粒度降为 O1 子问题。改 §8。 | §8 O1 重写 |
---
## 1. 结论先行
**一块基石 = 一套"游戏挂引擎"的统一约定,由两层构成**
- **下层(已冻结、复用)= 母 spec P2 三契约**`PluginContext.getEngine()`(受控引擎能力面,`api.d.ts:264-303`+ GamePackage `manifest.packageUrl`(交付/加载锚,`game-package.schema.json:66`+ SDK storage 根契约。这层是**插件如何拿引擎能力 + 包如何交付**的单一事实源,`28a57b8` 已冻结。
- **上层(仍缺、本弧核心)= "游戏宿主装载契约"**:定义"一款**游戏整体**(不止插件)如何挂到 LittleJS 引擎宿主上"——游戏暴露稳定生命周期、绘制面取引擎 `mainContext`、经 `engineInit` 五回调驱动。**这层契约今天全仓不存在**§0-F1 复证),是 P1 要 spike 实的真东西。其形态(新立 additive 契约 / 折进既有面 / 生命周期粒度)是创始人待拍项 **O1**,本稿不预断。
> ⚠️ **不要把上层叫成已存在的契约**。原稿用 `GameModule` 命名它并当作"已统一",但它既不在 `contracts/` 也不在 `api.d.ts`;而 `PluginContext` 是**插件**契约,不是**游戏宿主**契约。两者一个都不能替另一个。Q4 铁律(引擎 `import` 只活集成段 host禁泄进游戏/插件)对两层都成立。
这套约定一次设计P1/P2/P3 三相共用:
1. **P1 用王蓝莓把上层装载契约 spike 出来**(方案 A`wanglanmei-ref``?engine=real` 通道,主循环从浏览器 RAF 改挂 `engineInit` 五回调,**游戏层绘制面**从 `#game` 改引擎 `mainContext`,保 `?engine=stub` 回滚=A0 双通道);并把**已存在但仍是闭包**的 `makeEngineCaps` 提升为可 import 模块§0-F2
2. **P2 把宿主泛化成"装载任意 GamePackage"**(同一装载契约 + 复用 `packageUrl` 加载链:`packageUrl→iife bundle`;后端写包、`game-studio` 增"引擎包"分支取代掏空的 `startRuntime`)。
3. **P3 W-G1 生成游戏按同一装载契约产出**agent 写码于插件库20 基准做 bake-off经 P1 宿主 + e2e harness 评测)。
**为何"三相同一基石"**:四路侦察确证地面有一道结构性裂缝——真引擎宿主(`host-dev` real**有掌帧无游戏**,真游戏(`wanglanmei-ref`**有游戏无引擎**,生成管线产物喂给**已掏空的 1.x runtime**(既不接引擎、也被 W-CLEAN 停用)。三处断口的**同一个根**是"缺上层那份'游戏整体↔引擎'的装载约定"(下层 `getEngine()` 已解决'插件↔引擎',但游戏的 `update/render` 主体仍在契约面之外)。先把这份装载约定 spike 实、再泛化、再让生成按它产出,是唯一不缝合、不返工的收敛路径。**反面**:若 P1/P2/P3 各自造一套接引擎方式ref 一套、Runner 一套、生成 agent 一套),三套漂移 = 边界模型"同一职不留两条并行路"被破agent 在多套词汇间犯迷糊,历史游戏兼容性失锚。
**最大风险(先标在最前)****P1 的"真渲同屏"= 把上层装载契约从零 spike 实,不是补几行**——关键难点是:① ref 的游戏层(`core.update`/`drawFrame(g)`**非插件**)今天没有"取引擎 `mainContext`"的约定面,不能借 `PluginContext`§0-F3② 游戏美术画在自带 `#game` 2D 画布、引擎粒子自渲进引擎 `mainContext`**两面分裂**待合并;③ `main.js` 用浏览器 RAF + `mockNowMs` 自掌帧,与 `engineInit` 掌帧**直接冲突**。这是全弧的第一道真门,过不了则 P2/P3 全部悬空。
---
## 2. 背景与问题(四路侦察的地面真相)
四路只读侦察(全 verified证据带 file:line拼出的真相**"N 款游戏跑在引擎上、人在浏览器真玩、进 feed"这条链今天是断的,断在四处**。
### 2.1 四处断口
| 侦察 | 断口 | 关键证据file:line |
|---|---|---|
| **a1/a4** | **王蓝莓上引擎≠补几行**:渲染面分裂 + 掌帧冲突 | `main.js:112` `createHostDevContext` 未传 `engineFactory`(仅 4 键 context2d/seed/audioFactory/clock`plugin.js` 返 null → 粒子全 `NOOP_HANDLE`;游戏美术画在 `main.js:83` `prepareCanvas(#game)` 的 2D `g` 上(`drawFrame(g,…):648`),引擎粒子自渲进 `mainContext``host.js:243` `new ParticleEmitter`=两张分离画布;`main.js:617-691` 自管 `frame(dtReal)`+RAF`host.js:884` 引擎掌帧不兼容 |
| **a4** | **host-dev real 有掌帧无游戏**:只挂九插件演示能力 | `host.js:428/874` 留痕"host-dev 无真游戏"`bootHostDev` 只收 `{canvas,statusEl}``host.js:150-156`**零"装载任意游戏"约定**;引擎五回调 `engGameInit/Update/Render` 转发到插件演示(`host.js:883-887` |
| **a2** | **生成管线是断的**:执行器被停用 + 产物喂掏空 host | `AigcTemplateConstants.java:46` `SUPPORTED_TEMPLATE_IDS = List.of()`W-CLEAN 主动停用)→ 任何任务判 `NO_TEMPLATE_MATCH` 零产出M-b 产 `GameConfig` JSON → 喂 `game-studio/src/host/runtime/index.ts``startRuntime`,该 host W-CLEAN 后只画"模板体系重构中"+发 `game_error``index.ts:56-113`**完全非 engine-aware** |
| **a3** | **feed 真空 + studio 是 mock** | 真后端 `feed/stream``list:[]`W-CLEAN 清掉存量游戏);`:4173` 是 mock 构建(取到 mock "霓虹躲避"假数据,未接真 staging基建全活new-api `:3000` healthy / MySQL / cloud `:48080` 跑 1 天+ / admin `:4174` |
### 2.2 母 specT1b-β execution已覆盖什么、缺什么
母 spec `2026-06-13-T1b-β…execution.md` 已**冻结开工**并覆盖Phase AA0-A6 引擎真接线,**已收口** `e591e9b`+`3e5cab4`、Phase BRunner v2 = 复用后端 `packageUrl` 走 DB-manifest 降级、Phase C1.x 退役 srcdoc→URL、Phase D参考件经发布主链入 feed
**但母 spec 的 Phase D = "ref 经发布主链入 feed",其产物仍是跑在 stub 插件上的 ref`main.js:15` 不 import 引擎)**——**它不含"ref 跑在引擎上"**。a4 把这道缝明确标为"结构缺口、归属波次待确认"。**Runner v2 全弧正是补这道缝、并把它与生成主线收敛成一条主梁的波次**:母 spec 完成"引擎能 spike / 包能交付 / feed 能翻态"的零件,本弧负责"用一份契约把这些零件串成'游戏真跑在引擎上、可泛化、可生成'的整链"。
```mermaid
graph LR
subgraph 今天["地面真相(四处断口)"]
E["引擎宿主<br/>host-dev real<br/>有掌帧·无游戏"]
G["真游戏<br/>wanglanmei-ref<br/>有游戏·无引擎"]
P["生成管线 M-b<br/>停用·产物喂掏空 host"]
F["feed 真空<br/>studio=mock"]
E -. 不交汇 .- G
P -. 喂 .-> X["1.x runtime<br/>掏空·非 engine-aware"]
F -. 接 .-> M["mock 假数据"]
end
今天 ==Runner v2 全弧:两层基石==> 明天
subgraph 明天["收敛后"]
L0["下层·已冻结(复用)<br/>PluginContext.getEngine()<br/>+ packageUrl"]
C["上层·仍缺(本弧 spike)<br/>游戏宿主装载契约<br/>形态待拍 O1"]
L0 -.托底.-> C
C --> A1["P1 ref spike<br/>装载契约验实·真渲同屏"]
C --> A2["P2 泛化宿主<br/>装载任意 GamePackage"]
C --> A3["P3 生成游戏<br/>按同一装载契约产出"]
end
```
> **图读法**:下层(`getEngine()`/`packageUrl`)已冻结、三相直接复用;上层"游戏宿主装载契约"是本弧 P1 要 spike 实的真新增物,其形态由 O1 拍定后才进 `contracts/`
---
## 3. 目标 / 非目标
### 3.1 目标
1. **钉死一份"游戏宿主装载契约"(上层)**,三相共用、单一事实源;**复用**下层已冻结的 `PluginContext.getEngine()` + `packageUrl`,不另造一套引擎能力面。装载契约形态(新立 additive 契约 / 折进既有面)按 **O1** 拍定。
2. **把 `makeEngineCaps` 从闭包提升为可 import 模块**(暂名 `host-dev/engine-caps.js`host-dev / ref / 未来生成游戏共用同一份引擎能力包装层,物理上杜绝"两份漂移"。**注意§0-F2**:数学门面 `engine-math.js`、caps 工厂 `makeEngineCaps``engineFactory` 注入**均已存在并已接线**A1/A4`e591e9b`/`3e5cab4`);本目标剩余工作只是把闭包搬成独立文件,非从零造。
3. **P1**:用 `wanglanmei-ref` 把该装载契约 spike 到"**引擎真掌帧 + 游戏美术与引擎粒子同屏可玩**",人在浏览器真玩,保 `?engine=stub` 回滚。
4. **P2**:把宿主泛化为"**装载任意 GamePackage 跑引擎**",复用 `packageUrl` 打通 `packageUrl→bundle→GamePlayer` 链,一款 engine-aware 游戏经发布主链进 feed、真人试玩。
5. **P3**W-G1 用便宜模型M3/M2.7/DS-V4按同一装载契约 **agent 写码于插件库**产出游戏20 基准 bake-off经 P1 宿主 + e2e harness 评测、金丝雀入 feed N 款。
### 3.2 非目标
- **不动受控面 6 项契约**`getContext2d/onFrame/getInput/getAudioContext/time/random``api.d.ts:264-304`)——引擎可换边界守住。
- **不动 feed 终裁引擎选型**LittleJS feed 侧不动渠道线小游戏引擎另行竞标HJ-CH-001
- **不重做 M-b 执行器的全部范式细节**P3 只定"生成按上层装载契约产 bundle"的接口边界,模型竞标/评估门判据细节归 W-G1 自身 spec
- **本稿不冻结上层装载契约的精确 IDL**(仅给 §4.1 草案形态供 O1 决策;拍定后由分相 execution spec 落进 `contracts/`,本稿不预冻字段)。
- **不接 OSS/MinIO**Runner v2 沿母 spec 默认降级 = DB-manifest 带版本 + immutable 头;切 OSS 留上线前)。
- **不新增寻路/CCD swept 等引擎缺件**20 基准已标两条硬边界;是否补门面等 P3 实测数据再拍)。
---
## 4. 推荐方案:两层契约(主梁)
> **主梁 = 下层复用 + 上层 spike**。下层(`getEngine()`/`packageUrl`)已冻结,照搬;上层"游戏宿主装载契约"是本弧真正要立的东西,形态待 O1 拍。本节给出装载契约的**草案形态**(仅供 O1 决策参考,未拍前不进 `contracts/`)。
### 4.1 上层装载契约的草案形态(待 O1 拍)
一款游戏 = 一个**纯模块工厂**(不 import 引擎、不碰裸 DOM暴露稳定生命周期由集成段 host 经 `engineInit` 五回调驱动;**插件能力**经下层 `ctx.getEngine()` 拿,**游戏整体的绘制面**经装载契约约定为引擎 `mainContext`
> ⚠️ **下层 vs 上层的职责切割F1/F3 的解法)**`getEngine()``api.d.ts:303`)是**插件**拿引擎能力的面,已冻结;它**解决不了**"游戏的 `update/render` 主体怎么挂引擎、怎么拿 `mainContext`"——因为游戏层ref 的 `core.update`/`drawFrame`)不是插件、不走 `PluginContext`。这正是上层装载契约要补的缺口,**不能拿 `getEngine()` 硬凑**。下图的 `game.*` 生命周期与"`render(g)``g` 来自引擎 `mainContext`"都属待拍的装载契约草案,非既有契约。
```mermaid
flowchart TB
subgraph host["集成段 host唯一 import 引擎处·Q4 铁律)"]
EI["LJS.engineInit(五回调)"]
EC["engine-caps.js 模块(提升自 host.js:197 闭包)<br/>particles/audio.synth/math 三面薄包装<br/>=复用下层 getEngine() 背书"]
CTX["createHostDevContext<br/>受控面 6 项 + engineFactory均已存在"]
end
subgraph game["游戏侧(装载契约·零引擎 import·形态待 O1"]
GM["game.init(ctx)<br/>game.update(dt)<br/>game.render(g)<br/>game.onInput(ev)"]
end
EI -->|gameUpdate 回调| GM
EI -->|gameRender 回调→引擎 mainContext| GM
EC -->|插件能力经 ctx.getEngine()【下层·已冻】| GM
CTX -. 游戏绘制面=引擎 mainContext【上层·待 O1】 .-> GM
GM -. 禁 .->|直接 import littlejsengine| X1["❌ Q4 违例"]
```
**四条不可破的约定**
1. **掌帧唯一源 = 引擎**:游戏不自起 RAF`update/render``engineInit` 五回调驱动(消除 a1/a4 的掌帧冲突)。
2. **绘制面唯一 = 引擎 `mainContext`**`setGLEnable(false)` 纯 2D游戏美术与引擎粒子渲到**同一张画布**(消除 a1 的渲染面分裂)。**此条是装载契约的核心待解项**F3
3. **能力唯一面(插件)= `ctx.getEngine()` 受控面【下层已冻】**:粒子/音频/easing 经包装层(边界模型"引擎有→薄包装"引擎缺件collision/gamefeel经插件补层。
4. **引擎 import 唯一活点 = host**:游戏/插件零直接 importQ4 铁律,`entry.js:19-20`)。
### 4.2 三相复用同一套契约
```mermaid
graph TD
L0["下层契约【已冻·复用】<br/>PluginContext.getEngine() + packageUrl"]
C["上层装载契约【本弧 spike·形态待 O1】"]
EC["engine-caps.js 模块<br/>(提升自 host.js:197 闭包·非从零)"]
L0 -.托底.-> C
L0 -.托底.-> EC
C --> P1
C --> P2
C --> P3
EC -.共用.-> P1
EC -.共用.-> P2
EC -.共用.-> P3
subgraph P1["P1·ref spike 装载契约"]
direction LR
P1a["?engine=real 通道<br/>main.js RAF→engineInit 五回调<br/>drawFrame(g)→mainContext"]
end
subgraph P2["P2·泛化宿主同契约产品化"]
direction LR
P2a["GamePlayer 增'引擎包'分支<br/>复用 packageUrl→iife bundle<br/>取代掏空的 startRuntime"]
end
subgraph P3["P3·生成游戏同契约产出"]
direction LR
P3a["agent 写码于插件库<br/>产装载契约 bundle<br/>20 基准 bake-off"]
end
```
### 4.3 方案 A / B 收敛裁决(重锚到真契约)
侦察给了两条缝合方向:**A = 给 ref 加 `?engine=real` 通道**(改 `main.js` 主循环挂 `engineInit`**B = 把 `host-dev` 泛化为"游戏宿主壳"**`bootHostDev` 加 game 入参,游戏作被装载模块)。
**裁决A 与 B 不是两套并行入口而是同一主梁的两个相位——A=P1 用王蓝莓 spike 上层装载契约B 的"泛化宿主"=P2 对同一装载契约的产品化。** 收敛性**可被验证**非口头断言A 与 P2 必须 ① import 同一个 `engine-caps.js` 模块、② 实现同一份装载契约生命周期、③ 经同一个 `packageUrl` 端点交付/加载——这三点都是 import 图 + 端点可查的硬约束。理由:
| 维度 | 方案 AP1 先做) | 方案 BP2 承接) |
|---|---|---|
| **改动面** | 小:动 ref `main.js` 主循环 + entry import 引擎 + 复用既有 `engineFactory` 键;保 stub 回滚 | 大:动 host 装载约定 + studio GamePlayer/inject + 后端写包 + 发布主链 |
| **取证链** | 复用 ref 现成 CDP 真触摸 + 四件套 evidence`e2e-input.cdp.cjs` | 复用 staging 发布链 + feed 走查 |
| **风险** | 验"装载契约能不能让一款真游戏真跑在引擎上同屏" | 验"装载契约能不能装载任意包 + 经产品链入 feed" |
| **依赖** | A0 地基(已收口)+ O1 装载契约形态拍定 | 依赖 P1 装载契约定稿 + 母 spec Phase B/C/D 零件 |
**结论**:先做 AP1用最小改动、最强取证链把装载契约**验实**;再把验实的契约做成 BP2的"泛化宿主"产品化。**装载契约=主干A/B 是它的两个相位,不是两个竞争方案。** 这正是"勿另造一套"的兑现A 与 P2 共用 `engine-caps.js` 与同一份装载契约接口B 只是把"单一 ref 游戏"换成"任意 GamePackage 包加载"。
**共享模块的真实状态§0-F2 纠正)**`engine-math.js`(数学门面)已是独立共享模块,`makeEngineCaps`(粒子/音频/math 三面包装)已实现并已经 `engineFactory: makeEngineCaps` 注入(`host.js:359`)——**但 `makeEngineCaps` 仍是 `host.js:197` `bootHostDev` 内的闭包**(依赖 `engineMode`/顶层 `LJS` import/`window.__engineCalls` 探针ref/生成游戏**无法直接 import**。剩余工作 = **把这个已存在的闭包提升为可 import 模块** `host-dev/engine-caps.js`(保 `engineMode` 短路 / `__engineCalls` 探针 / null-engine 回归语义逐行不变host-dev / ref / 生成游戏共用——否则三相各自重建一份等价工厂 = 与"勿另造"直接冲突。**这是"模块化已有件",不是"从零造新件"**(原稿"engine-caps.js 不存在=真新增工作"已订正)。
---
## 5. 分相 P1 / P2 / P3
### 5.1 P1 — 用王蓝莓 spike 装载契约(方案 A
| 项 | 内容 |
|---|---|
| **目标** | `wanglanmei-ref``?engine=real` 通道:引擎真掌帧 + 游戏美术与引擎粒子**同屏可玩**,人在浏览器真玩;保 `?engine=stub` 回滚。同时把已存在的 `makeEngineCaps` 闭包**提升为可 import 模块** `engine-caps.js`§0-F2非从零造。**前置依赖 O1**:装载契约形态(游戏层怎么取 `mainContext`)须先拍定。 |
| **关键设计** | ① `entry.js``import * as LJS`(引擎真入 ref-bundleQ4 只活 entry/host`main.js``frame(dtReal)` 拆成逻辑段(`core.update/bundle.tick/applyEvents/fx.step`)挂 `gameUpdate`、绘制段(`drawFrame/palettePost.renderPost`)挂 `gameRender`**游戏层目标画布从 `#game` 改引擎 `mainContext`**`setGLEnable(false)` 纯 2D此口径由 O1 装载契约定义,非借 `PluginContext`);③ 插件受控面 `context2d` 改快照引擎 `mainContext`;④ 粒子/音频/easing 经**提升后的** `engine-caps.js` 包装层(复用下层 `getEngine()` 背书);⑤ `mockNowMs` 在 real 下改逻辑帧钟语义,删/旁置 `main.js:668-690` 浏览器 RAF对齐 A0 双通道)。 |
| **验收门** | ① mini-desktop 真渲门:引擎掌帧帧增量区间断言(对齐 G0-2+ **游戏层美术与引擎粒子同屏**像素命中(非两张分离画布,即装载契约的 `mainContext` 约定真生效);② 真接线门②:`window.__engineCalls``particles.spawnEmitter` 等 call-ID插件运行时真调引擎③ CDP 真触摸逐 zone 真玩(开店→进货→营业→结算→升级)+ 四件套 evidence`?engine=stub` 回滚通道逐像素确定性回归仍绿;⑤ `engine-caps.js` 提升后既有回归门0 8/8 + real 门 6/6逐项不变⑥ console 零未捕获错误。 |
| **爆炸半径** | `games/wanglanmei-ref/{entry.js:15, src/main.js:112/617-691/648}`(主循环重做);**模块化已有件**:把 `host.js:197-318` `makeEngineCaps` 提升为 `host-dev/engine-caps.js` + `host-dev/host.js` 改为 import 它(保 `engineMode` 短路/探针语义不变;`engine-math.js` 已是独立模块无需动);`SIZES.md` 增"游戏产物(引擎+包装层)"新口径ref 197KB→~540KB见 §6。受控面 6 项契约**零改**`engineFactory` 是既有可选入参 `api.d.ts:405`**新增的是上层装载契约工件**(形态待 O1。 |
### 5.2 P2 — 泛化宿主:装载任意 GamePackage方案 B
| 项 | 内容 |
|---|---|
| **目标** | 同一装载契约 + 包加载:复用 `packageUrl→iife bundle`;后端写包;`game-studio` GamePlayer/inject 增"引擎包"分支取代掏空的 `startRuntime`studio 改 staging 重建。产出≥1 款 engine-aware 游戏(先用 ref 级)经发布主链入 feed、真人试玩。 |
| **关键设计** | ① **包加载形态**GamePackage `manifest.packageUrl` 指向 iife bundle沿母 spec §5 = 复用后端 `packageUrl` 走 DB-manifest 端点 `/app-api/runtime/package/{versionId}/manifest`,天然含 versionId 版本化);② **GamePlayer 分支**`GamePlayer.vue:128-168` 取包后inject 不再硬绑掏空的 `startRuntime``inject.ts:104`),改"加载引擎 bundle + 经装载契约 boot"分支——**这是取代 `runtime/index.ts` 掏空壳的关键**;③ CSP 须放行 script-src(Runner origin)+connect-src(bundle 取源)(母 spec §8 四项兼容矩阵);④ studio `npm run build -- --mode staging` 重建,消除 mock 中间件掩盖a3 阻断 B。 |
| **验收门** | ① 后端 feed/stream 出现该游戏(`status=PUBLISHED(4)`);② 真人浏览器开 feed→点卡→**引擎 bundle 真渲非"重构中"**+`game_loaded`+`game_play_end`;③ 防 demo 兜底假阳性(母 spec 门4checksum=发布版本 ∧ gameId/versionId 匹配 ∧ 兜底触发即 fail④ 链路③⑤真走查解封。 |
| **爆炸半径** | `game-studio/src/host/{GamePlayer.vue, inject.ts, runtime/index.ts(掏空壳→引擎包分支), contract.ts}`;后端发布主链(`ProjectServiceImpl/PublishOrchestrationServiceImpl/FeedServiceImpl`,被动验证);`contracts/game-package.schema.json:66` `packageUrl`契约已留位additive。**与母 spec Phase B/C/D 高度重叠**——P2 是把母 spec 的"ref 入 feed跑 stub"升级为"engine-aware 包入 feed跑引擎",须与母 spec owner 对齐避免双写。 |
### 5.3 P3 — W-G1 生成游戏按同一装载契约产出
| 项 | 内容 |
|---|---|
| **目标** | 便宜模型M3/M2.7/DS-V4按上层装载契约 **agent 写码于插件库**产出游戏 bundle非 GameConfig JSON20 基准做 bake-off经 P1 宿主 + e2e harness 评测、过门金丝雀入 feed N 款。 |
| **关键设计** | ① **范式切换**:从"模板填参出 GameConfig JSON"M-b 现状)改为"agent 写装载契约代码→esbuild 打 iife bundle"HJ-GEN-001 终裁);② **白名单/契约前置**:新模板体系(插件库 templateId 或新生成包类型)注册进 `AigcTemplateConstants.SUPPORTED_TEMPLATE_IDS`(解 a2 阻断①)+ prompt registry 适配(`contracts/prompts`);③ **靶集**20 经典基准三批(冒烟 6 易 / 主力 8 中 / 压测 6 难),暴露 4 类能力短板(文本 HUD / 网格 / CCD / 拖拽);④ **评测**:复用 agent-loop-v1 工具箱judge/ledger/预算闸/CDP 真玩),过 P1 宿主真渲 + harness 门兜底。 |
| **验收门** | ① 黄金集批跑 acceptRate≥0.8 分模板HJ-GEN-001 口径);② 金丝雀 N 款 feedVisible 真渲;③ new-api `logs.quota` 成本分解对照 L1 门¥0.15);④ harness 门稳定逮"穿透/消行错/不可玩"(便宜模型路线命门)。 |
| **爆炸半径** | `game-cloud` aigc`AigcGenerateExecutor`/`AigcTemplateConstants`/生成-构建-取证管线);`contracts/{prompts, templates 或新包类型}`agent-loop-v1 工具箱。**前置硬卡已解DeepSeek key 就绪)**;生成范式从 JSON→bundle 是 P3 最大改动面(产物形态变 = host 消费方式变,靠 P2 的引擎包分支兜)。 |
### 5.4 三相依赖与解锁顺序
```mermaid
graph LR
A0["A0 地基<br/>✅ 已收口"] --> P1
O1["O1 装载契约形态<br/>⚠️ 待创始人拍"] --> P1
EC["engine-caps.js<br/>模块P1 提升·非从零)"] --> P1
P1["P1 ref spike<br/>装载契约验实"] --> P2["P2 泛化宿主<br/>包加载+入 feed"]
P1 --> P3["P3 W-G1 生成<br/>按契约产出"]
P2 --> P3
KEY["DeepSeek key<br/>✅ 就绪"] --> P3
style P1 fill:#fff3cd
style P2 fill:#d1ecf1
style P3 fill:#d4edda
style O1 fill:#f8d7da
```
> P1 解锁前先解 **O1**装载契约形态拍定。P3 双依赖 P1契约验实+ P2引擎包加载链生成的 bundle 要既"按装载契约挂引擎"P1 验实)、又"经包加载进 feed 试玩"P2 打通)。
---
## 6. 关键权衡
### 6.1 体积ref 197KB→~540KB 是否触 Tier1 体积口径?
- **事实**ref 现 197KB不打引擎esbuild dry-check 实测 `has zzfxG:false`);接引擎后 ~540KBhost-dev entry 实测 544KB 含 `engineInit+ParticleEmitter`。SIZES.md 当前是**插件增量法**(合计插件 gz≈12KB自律线 60K**不覆盖"游戏产物=引擎+包装层"口径**。
- **判定**~540KB raw 是 **min 未 gz** 量级LittleJS 完整版 raw≈55KB gzv1.18.19),加游戏代码后游戏产物 gz 远在 B1 入场券 **gz≤350KB / raw≤1.5MB**tiered-engine 记忆)之内——**不触红线**。**但须给 SIZES.md 增"游戏产物口径"新表**(与"插件增量"分离),并按"引擎+包装层"重锚(母 spec 裁决①已要求重锚,但仅针对 host-dev未覆盖游戏产物——本弧补上。**勿再引"13KB"**js13k 极限分支已废tiered-engine 记忆)。
### 6.2 确定性逐像素如何与引擎 RAF 共存?
- **事实**:引擎一个 RAF 内追帧步数 N 由墙钟抖动决定(`littlejs.esm.js:256` `frameTimeBufferMS` 循环),外部轮询 `frameCount>=startFrame+frames` 会 OVERSHOOT → same-seed 重跑像素不一致A0 已核验)。
- **判定****real 通道不承担逐像素确定性,由 `?engine=stub` 通道独占兜底**A0 既定双通道策略。real 通道只证"引擎掌帧 + 同屏真渲 + call-ID 真接线";逐像素哈希回归走 stub。P1 验收门据此分两套real=同屏可玩+call-ID / stub=逐像素回归),不强求 real same-pixel。
### 6.3 契约形态对生成 agent 的约束?
- **正向**:上层装载契约是 agent 的**唯一产出靶**(写 `init/update/render/onInput`,插件能力经下层 `ctx.getEngine()`一职一路、词汇收敛边界模型20 基准里 ~12 款能力面齐备可直接产出。**前提是 O1 把装载契约形态拍死**,否则 agent 没有稳定产出靶。
- **代价/风险**4 类高频短板(文本 HUD / 网格 / CCD swept / 拖拽手势)**当前无插件门面**——便宜模型 20 个 prompt 各造一遍轮子,一致性/token/出错率三重受损20 基准 C.2)。**这是 P3 实测要量化的核心数据**,据此决定"是否把高频缺口(文本 HUD/网格)补成插件门面"——**不预先投入,等批次 3 实测再拍**。
---
## 7. 风险 / 兼容性
| 风险 | 等级 | 缓解 / 回滚 |
|---|---|---|
| **装载契约缺口F1/F3未补就开工** | **P0** | 全仓无"游戏整体↔引擎"装载契约,`getEngine()` 是插件面凑不上§0-F1/F3。**必须 O1 先拍装载契约形态、再开 P1**;否则 P1 边做边发明接口 = 又一条平行路。本稿已把它从"既成事实"降为待拍 open item。 |
| **P1 真渲同屏过不了**(渲染面合并 + 掌帧重做难度被低估) | **P0** | 这是全弧第一道真门A0 双通道已证引擎掌帧可行ref 需把"自管画布+RAF"换成"引擎 mainContext+五回调"+按装载契约接游戏层绘制面。**回滚 = `?engine=stub`**(行为与 α 逐行等价A0 已保留)。过不了则全弧暂停、不连累 stub 既有交付。 |
| **受控面被动了** | P0 | 受控面 6 项契约**零改**是硬约束(`engineFactory` 是既有可选入参P1/P2/P3 任何相若需改受控面=触发架构复议,不得自行扩。**上层装载契约是 additive 新增,不得改下层既冻 6 项**。 |
| **P2 与母 spec Phase B/C/D 双写** | P1 | P2 高度重叠母 spec须与母 spec owner 对齐——P2 = 把母 spec"ref 入 feedstub"升级为"engine-aware 包入 feed引擎",是承接非另起。 |
| **历史游戏兼容** | P1 | 插件公开 APIEmitterConfig 等)冻结不变 → 老游戏照跑(包装/补层是内部事,边界模型);引擎包装的能力改走引擎渲染、像素可能微异=可移植性提升(创始人最初诉求)。上层装载契约对存量 ref 而言是 additive 通道(`?engine=stub` 即旧行为)。 |
| **engine-caps 模块化引入回归** | P1 | 把**已存在的** `host.js:197-318` `makeEngineCaps` 闭包提升为模块须保 `engineMode` 短路 / `__engineCalls` 探针 / null-engine 回归语义逐行不变门0 8/8 既有回归 + real 引擎门 6/6 作回归基线§0-F2这是搬运已有件非新写。 |
| **P3 生成范式 JSON→bundle 改动大** | P1 | 产物形态变 = host 消费方式变,靠 P2 引擎包分支兜底harness 门兜底是便宜模型路线命门。P3 评估门判据细节归 W-G1 自身 spec不在本弧预烤。 |
| **契约回滚** | — | 上层装载契约走 `?engine=stub` 通道 / Runner URL 复用 packageUrl / storage 均 additive回滚=移除新增不破存量。 |
---
## 8. 待创始人拍的 Open Items
| # | 待拍项 | 选项 / 默认建议 |
|---|---|---|
| **O1**升级·F1/M2 | **上层"游戏宿主装载契约"的归属与形态**——这是本弧第一待拍:①**归属**:新立第 9 类契约(`contracts/` 下独立工件)/ 还是折进既有 `core/api.d.ts``GameHostContract` additive 段?②**生命周期粒度**`init/update/render/onInput` 四件够不够?是否要 `resize/destroy/onVisibility`?③**绘制面约定**:游戏层取引擎 `mainContext` 的口径(不能借 `PluginContext`)。 | 建议:①新立 additive 契约(避免污染插件契约语义,与边界模型"一职一路"一致);②取 `init/update/render/onInput`+`destroy`(卸载卫生),其余 additive 后补;③`mainContext` 经装载契约 boot 时注入游戏层。**P1 开工前必须拍 ①③,否则 spike 无稳定靶。** |
| **O2** | **P2 与母 spec T1b-β Phase D 的边界**:是把母 spec Phase D 直接升级为"engine-aware 包入 feed"(合并),还是 P2 作为 Phase D 之后的独立增量波次? | 建议:合并——母 spec Phase D 的"ref 入 feed"本就该是 engine-aware 才有意义(否则 feed 里跑 stub 无产品价值)。**待拍是否改母 spec 范围。** |
| **O3** | **P3 生成产物白名单注册口径**:新模板体系用"单一通用装载契约 templateId"还是"按品类多 templateId"注册进 `SUPPORTED_TEMPLATE_IDS` | 建议:先单一通用 templateId 验管线,品类区分留 prompt 层;**待拍。** |
| **O4** | **高频缺口补门面时机**:文本 HUD / 网格门面是否在 P3 前预补,还是等批次 3 实测出错率再补? | 建议20 基准既定):**不预补,等实测数据**;但若创始人想压 P3 一致性,可预补文本 HUD最高频~15/20 款需要)。**待拍。** |
| **O5** | **N首批入 feed 款数)**P2 先 1 款 ref 验链P3 金丝雀 N 款——N 定多少、品类怎么选? | 建议P3 先冒烟批 6 款过门、取 ≥3 款金丝雀入 feed**待拍。** |
| **O6**(流程) | **本稿是否需补一轮独立外部对抗复审再冻结分相 execution**(本轮对抗 JSON 为空整改由主笔自核完成§0 | 建议:主梁已重锚且自核证据可复证,但 F1 触主干契约裁决,**建议补一轮外部对抗复审确认 O1 归属裁决后再开 P1 execution**。**待拍。** |
---
## 风险与假设诚实标注·r2 已订正)
- **[verified·订正]** **上层"游戏宿主装载契约"全仓不存在**`grep GameModule``contracts/`+`src/core/`+`host-dev/` 零命中)——这是 §0-F1 的根据。既有契约只有 `PluginContext`(插件面,`api.d.ts:264-303`+ `packageUrl``game-package.schema.json:66`),二者均已冻结(母 spec L163 / `28a57b8`)。
- **[verified·订正]** **`engine-caps.js` 文件确不存在,但其内容已大半存在并接线**`engine-math.js`(数学门面)已独立成模块(`host.js:47` import`makeEngineCaps`(粒子/音频/math 三面包装)已实现于 `host.js:197-318` 并已注入 `engineFactory: makeEngineCaps``host.js:359`)。故剩余工作 = 把闭包提升为可 import 模块(搬运),**非"从零造"**(原稿"真新增工作"已订正为 §0-F2
- **[verified]** 四处断口、SIZES 口径、母 spec 覆盖范围——源码/spec 实证,证据锚点见 §2/§4/§5 各 file:line。
- **[verified]** ref 游戏层(`core.update`/`drawFrame(g,648)`)画在 `#game``main.js:83`)、不走 `PluginContext``main.js:112` 仅插件——§0-F3 据此:游戏层取 `mainContext` 须靠待立的装载契约。
- **[inferred]** "便宜模型能产出 ✅ 款可玩件"是 P3 待验推断20 基准 C 已标);本弧只确立契约与评测路径,不预断模型产出质量。
- **[inferred]** ref 接引擎后 ~540KB 不触 B1 入场券——基于 host-dev entry 544KB 实测 + gz 量级推断P1 须用 `size.mjs` 实测游戏产物 gz 坐实。
- **[assumption]** 好玩基线 v2 五要素精确判据未在本次定位到原文(散见多 specP3 验收门精确判据须开工前从评估门 spec 取齐。
- **[流程]** 本轮对抗复审结论体为空JSON=null§0 整改清单由主笔架构师自核源码生成(全可复证);是否补一轮外部对抗复审见 O6。
- **[未决]** O1-O6 六项待创始人拍。

View File

@ -20,3 +20,19 @@
合计插件 gz 增量 ≈ **12100B**(自律线 60K超 60K 须创始人显式放行,执行版 §5
---
## 游戏产物(引擎+包装层——P1 王蓝莓上引擎口径
> **口径区别**:上表「增量法」量的是**插件净增**(基线壳→+core→逐插件叠加不含引擎。游戏产物一款 game-shop bundle= **引擎 + 能力插件库 + 游戏代码** 整包,是另一口径,不并入上表。
> 同一 esbuild 锁参(`bundle+minify+iife+es2019+sourcemap=false`gz=`zlib.gzipSync(buf,{level:9})`
| 产物 | 引擎入包 | raw | gz | B1 入场券(gz≤350KB) | 备注 |
|---|---|--:|--:|:--|---|
| ref **stub** 通道 | 否 | ~197KB | — | ✔ | 不打引擎(回滚锚;引擎 import 经 engineMode 短路不触达运行时,但 esbuild 仍随 entry 引擎 import 打入——故 stub/real 同一份 bundle差异在运行时通道不在产物字节 |
| ref **real**(引擎真入) | 是(`littlejsengine.esm.js` 640KB 源经 tree-shake+min | **264KB** | **88KB** | ✔gz 88KB ≪ 350KB | `entry.js`/`main.js` import 引擎Q4只 entry/host`game-shop.js` 游戏工厂零引擎 importmetafile 实证)。`grep -q ParticleEmitter` 核入包 |
> **实测来源**6c6g `node scripts/build.mjs games/wanglanmei-ref/entry.js → /tmp` dry-buildraw=264476 / gz=895832026-06-13。引擎入包凭据metafile `inputs``node_modules/littlejsengine/dist/littlejs.esm.js`,且仅 `main.js`/`entry.js` import 之Q4
> **注**:插件增量法不覆盖游戏产物口径——勿把游戏产物字节并入上表「合计插件 gz 增量」。mini-desktop 可经 `size.mjs` 复测覆盖本节。
> **勿引「13KB / 55KB」自研 Canvas 旧口径**(已废,见 [.agents tech-decisions §1.1]);引擎=LittleJS 完整版。

View File

@ -5,13 +5,38 @@
* 构建沿 scripts/build.mjs 锁参bundle+minify+iife+es2019+sourcemap=false BUILD.md
* node scripts/build.mjs games/wanglanmei-ref/entry.js games/wanglanmei-ref/dist/ref-bundle.js --global-name=WanglanmeiRef
*
* 定位仿 host-dev/entry.jscore 协议 + 所用插件 + 游戏逻辑/渲染/宿主汇成一个
* iife 全局WanglanmeiRefindex.html bootRef 启动α 不接 littlejsengine裁定同 host-dev
* 本文件 import-safenode import 不执行任何浏览器调用boot 由页面显式触发
* 定位仿 host-dev/entry.jscore 协议 + 所用插件 + 游戏逻辑/渲染/宿主+ 引擎汇成一个
* iife 全局WanglanmeiRefindex.html bootRef 启动
*
* P1 · Option C引擎 import 只落本入口且经动态 import 延后求值mustFix #1
* 起因本游戏的 import-graph.test.mjsMODULES 表第 24 显式断言 `../entry.js` 可在 node 导入
* littlejsengine ESM **模块装载期**就触 window/AudioContextesm.js:5556/6753
* 顶层 `import * as LJS from 'littlejsengine'` 会让 node import entry.js 即抛 ReferenceError 该测红
* 修法把引擎 import 顶层静态改为**包装层内动态 import**
* · node import entry.js ****触发引擎装载bootRef 未被调用 entry.js node import-safe import-graph
* · esbuildbundle+iife splitting会把动态 import 的引擎**内联进单文件产物**
* 实测改写为 `Promise.resolve().then(()=>(引擎init(), 引擎命名空间))` fetch/无外部 chunk
* 引擎仍真入 ref-bundlemetafile littlejsengine + 产物 engineInit 符号齐备 dry-build 亲验
* · 浏览器运行期调 bootRef 时才求值引擎window/AudioContext 已存在拿到名字空间注入 main.js real 通道
* Q4 铁律不破引擎 import 仍只活本入口main.jshost 经注入用引擎不直接 importgame-shop.js 游戏工厂零引擎 import
*/
'use strict';
import { bootRef } from './src/main.js';
import { bootRef as bootRefCore } from './src/main.js';
export { bootRef };
/**
* Option C 包装层对外 bootRef动态 import 引擎名字空间后注入委托 main.js 的真实现
* index.html 调的是 iife 全局 WanglanmeiRef.bootRef即本包装
* - 动态 import 延后求值node import entry.js 不触引擎 entry.js node import-safe import-graph 24
* esbuild 把引擎内联进 ref-bundlereal 入包不丢 dry-build metafile 亲验
* - stub 通道 main.js 不触达引擎注入与否无副作用保回滚锚行为逐行不变
* - 调用方已显式给 engine 则尊重其值不重复装载便于 host-dev 等复用
* @param {{canvas: HTMLCanvasElement, statusEl?: HTMLElement, engine?: any}} [opts]
* @returns {Promise<object>} refHost 句柄
*/
export async function bootRef(opts) {
// 动态 import求值时机延后到 bootRef 被调用浏览器运行期。esbuild 内联,无外部请求。
const engine = (opts && opts.engine) || (await import('littlejsengine'));
return bootRefCore(Object.assign({}, opts, { engine }));
}

View File

@ -0,0 +1,75 @@
{
"checklist": {
"shougan": {
"tapFeedbackMaxFrames": 0,
"tapFeedbackWithin3": true,
"everyTapScalePulse": true,
"comboUiShown": false,
"flightEasingUsed": true,
"hitStopTriggered": false,
"bufferedServeReplays": 0
},
"meishu": {
"allDrawsViaPalette": true,
"vignetteEnabled": true,
"rushPaletteSeen": false,
"assetsLoaded": "13/13",
"noPrimaryPlaceholderRects": true
},
"yinyue": {
"audioUnlocked": false,
"bgmReady": false,
"bgmCrossfadeSeen": false,
"sfxKindCount": 0,
"sfxKinds": []
},
"shendu": {
"loopReachable": true,
"loopOps": 25,
"failureReachable": true,
"failureReason": "reputation",
"priceTwoSidedLoss": true,
"upgradeTreeSize": 8,
"firstUpgradeDay": 2
},
"juese": {
"keeperSpriteResident": true,
"lineGroups": 14,
"lineTotal": 30,
"linesShownLive": 0
}
},
"measures": {
"particle": {
"gold": 0,
"cream": 0,
"smoke": 0
},
"audio": {
"sfxKinds": [
"tick",
"shelve",
"arrive",
"coin",
"empty",
"upgrade"
],
"bgm": {
"ready": true,
"failed": false,
"idleLevel": 0.23916264021400713,
"rushLevel": 5.689900643400063e-12
}
},
"assetsMissing": [],
"frames": 6152,
"taps": 517,
"tapFeedbackMaxFrames": 1,
"comboUiShown": 1,
"hitStopCount": 0,
"rushPaletteSeen": true,
"bgmCrossfadeSeen": true,
"linesShown": 20,
"bufferedServeReplays": 0
}
}

View File

@ -0,0 +1,358 @@
{
"meta": {
"url": "http://127.0.0.1:4320/?engine=real&seed=305441741",
"seed": 305441741,
"startedAt": "2026-06-13T17:53:05.383Z",
"harness": "e2e-input.cdp.cjs(主会话)",
"engineMode": "real",
"coordMap": {
"rect": {
"l": 0,
"t": 0,
"w": 390,
"h": 844,
"vw": 390,
"vh": 844
},
"map": {
"l": 0,
"t": 0,
"sx": 1,
"sy": 1
},
"canvas": "#game-engine"
},
"endedAt": "2026-06-13T17:54:49.277Z"
},
"gates": {
"engineFrameDrive": {
"dFrame": 31,
"range": [
20,
45
],
"s0": {
"frame": 41,
"time": 0.6666666666666666,
"timeReal": 0.7665380000000002,
"paused": false,
"timeScale": 1
},
"s1": {
"frame": 72,
"time": 1.1833333333333333,
"timeReal": 1.2832379999999994,
"paused": false,
"timeScale": 1
}
},
"engineWiringCallId": {
"hasSpawn": true,
"sample": [
"audio.synth.synthSfx",
"audio.synth.synthSfx",
"audio.synth.synthSfx",
"audio.synth.synthSfx",
"audio.synth.synthSfx",
"audio.synth.synthSfx",
"audio.synth.synthSfx",
"audio.synth.synthSfx",
"audio.synth.synthSfx",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx"
]
},
"sameScreenPixels": {
"nonEmpty": true,
"width": 780,
"height": 1688,
"warmHits": 682,
"brightHits": 13028,
"maxCh": 254
},
"closedLoopRealInput": true,
"engineTakeover": true
},
"failures": [],
"zonesSeenByPhase": {
"real-prep-day1": [
"dec-soda",
"inc-soda",
"dec-snack",
"inc-snack",
"dec-bread",
"inc-bread",
"price-slider",
"up-speed1",
"up-shelf1",
"up-unlock_ice",
"up-decor1",
"up-unlock_toy",
"up-speed2",
"up-shelf2",
"up-decor2",
"openShop"
],
"real-day2-prep": [
"dec-soda",
"inc-soda",
"dec-snack",
"inc-snack",
"dec-bread",
"inc-bread",
"price-slider",
"up-speed1",
"up-shelf1",
"up-unlock_ice",
"up-decor1",
"up-unlock_toy",
"up-speed2",
"up-shelf2",
"up-decor2",
"openShop"
]
},
"stateSnapshots": [
{
"tag": "real-menu",
"state": {
"phase": "title",
"day": 1,
"cash": 1200,
"reputation": 62,
"priceMul": 1,
"stock": {},
"unlocked": [
"soda",
"snack",
"bread"
],
"upgrades": {},
"customers": 0,
"combo": 0,
"today": {
"revenueJiao": 0,
"soldCostJiao": 0,
"served": 0,
"angry": 0,
"stockout": 0,
"walkaway": 0,
"rescued": 0,
"bigSales": 0,
"maxCombo": 0
},
"lifetime": {
"days": 0,
"served": 0,
"angry": 0,
"netJiao": 0,
"upgradesBought": 0
},
"gameoverReason": null
}
},
{
"tag": "real-after-stock-day1",
"state": {
"phase": "prep",
"day": 1,
"cash": 840,
"reputation": 62,
"priceMul": 1,
"stock": {
"soda": 6,
"snack": 6,
"bread": 6
},
"unlocked": [
"soda",
"snack",
"bread"
],
"upgrades": {},
"customers": 0,
"combo": 0,
"today": {
"revenueJiao": 0,
"soldCostJiao": 0,
"served": 0,
"angry": 0,
"stockout": 0,
"walkaway": 0,
"rescued": 0,
"bigSales": 0,
"maxCombo": 0
},
"lifetime": {
"days": 0,
"served": 0,
"angry": 0,
"netJiao": 0,
"upgradesBought": 0
},
"gameoverReason": null
}
},
{
"tag": "real-after-serving-day1",
"state": {
"phase": "settle",
"day": 1,
"cash": 1239,
"reputation": 41.8,
"priceMul": 1,
"stock": {
"soda": 0,
"snack": 0,
"bread": 0
},
"unlocked": [
"soda",
"snack",
"bread"
],
"upgrades": {},
"customers": 0,
"combo": 0,
"today": {
"revenueJiao": 799,
"soldCostJiao": 360,
"served": 18,
"angry": 0,
"stockout": 13,
"walkaway": 0,
"rescued": 0,
"bigSales": 0,
"maxCombo": 9
},
"lifetime": {
"days": 1,
"served": 18,
"angry": 0,
"netJiao": 39,
"upgradesBought": 0
},
"gameoverReason": null
}
}
],
"hashes": {},
"screenshots": [
"real-menu.png",
"real-prep-day1.png",
"real-sameScreen.png",
"real-settle-day1.png",
"real-day2-upgraded.png"
],
"boot": {
"anchors": [
"t_boot",
"t_plugins_ready",
"t_game_init",
"t_first_paint",
"t_input_bound",
"t_game_start"
],
"initOk": true,
"registeredPlugins": [
"collision",
"physics-lite",
"gamefeel",
"particles-juice",
"palette-post",
"save-progress",
"audio-music",
"runtime-probe"
]
},
"checklist": {
"shougan": {
"tapFeedbackMaxFrames": 0,
"tapFeedbackWithin3": true,
"everyTapScalePulse": true,
"comboUiShown": false,
"flightEasingUsed": true,
"hitStopTriggered": false,
"bufferedServeReplays": 0
},
"meishu": {
"allDrawsViaPalette": true,
"vignetteEnabled": true,
"rushPaletteSeen": false,
"assetsLoaded": "13/13",
"noPrimaryPlaceholderRects": true
},
"yinyue": {
"audioUnlocked": false,
"bgmReady": false,
"bgmCrossfadeSeen": false,
"sfxKindCount": 0,
"sfxKinds": []
},
"shendu": {
"loopReachable": true,
"loopOps": 25,
"failureReachable": true,
"failureReason": "reputation",
"priceTwoSidedLoss": true,
"upgradeTreeSize": 8,
"firstUpgradeDay": 2
},
"juese": {
"keeperSpriteResident": true,
"lineGroups": 14,
"lineTotal": 30,
"linesShownLive": 0
}
},
"measures": {
"particle": {
"gold": 0,
"cream": 0,
"smoke": 0
},
"audio": {
"sfxKinds": [
"tick",
"shelve",
"arrive",
"coin",
"empty",
"upgrade"
],
"bgm": {
"ready": true,
"failed": false,
"idleLevel": 0.23916264021400713,
"rushLevel": 5.689900643400063e-12
}
},
"assetsMissing": [],
"frames": 6152,
"taps": 517,
"tapFeedbackMaxFrames": 1,
"comboUiShown": 1,
"hitStopCount": 0,
"rushPaletteSeen": true,
"bgmCrossfadeSeen": true,
"linesShown": 20,
"bufferedServeReplays": 0
},
"softLive": {
"rushPaletteSeen": true,
"bgmCrossfadeSeen": true,
"sfxKindCount": 0,
"hitStopTriggered": false,
"audioUnlocked": false
},
"pass": true
}

View File

@ -0,0 +1,517 @@
{"seq":0,"wallMs":1781373187514,"kind":"tap","zoneId":"newGame","x":195,"y":588,"clientX":195,"clientY":588}
{"seq":1,"wallMs":1781373188147,"kind":"tap","zoneId":"inc-soda","x":329,"y":214,"clientX":329,"clientY":214}
{"seq":2,"wallMs":1781373188339,"kind":"tap","zoneId":"inc-soda","x":329,"y":214,"clientX":329,"clientY":214}
{"seq":3,"wallMs":1781373188626,"kind":"tap","zoneId":"inc-soda","x":329,"y":214,"clientX":329,"clientY":214}
{"seq":4,"wallMs":1781373188770,"kind":"tap","zoneId":"inc-soda","x":329,"y":214,"clientX":329,"clientY":214}
{"seq":5,"wallMs":1781373188903,"kind":"tap","zoneId":"inc-soda","x":329,"y":214,"clientX":329,"clientY":214}
{"seq":6,"wallMs":1781373189037,"kind":"tap","zoneId":"inc-soda","x":329,"y":214,"clientX":329,"clientY":214}
{"seq":7,"wallMs":1781373189171,"kind":"tap","zoneId":"inc-snack","x":329,"y":260,"clientX":329,"clientY":260}
{"seq":8,"wallMs":1781373189305,"kind":"tap","zoneId":"inc-snack","x":329,"y":260,"clientX":329,"clientY":260}
{"seq":9,"wallMs":1781373189454,"kind":"tap","zoneId":"inc-snack","x":329,"y":260,"clientX":329,"clientY":260}
{"seq":10,"wallMs":1781373189587,"kind":"tap","zoneId":"inc-snack","x":329,"y":260,"clientX":329,"clientY":260}
{"seq":11,"wallMs":1781373189720,"kind":"tap","zoneId":"inc-snack","x":329,"y":260,"clientX":329,"clientY":260}
{"seq":12,"wallMs":1781373189853,"kind":"tap","zoneId":"inc-snack","x":329,"y":260,"clientX":329,"clientY":260}
{"seq":13,"wallMs":1781373189987,"kind":"tap","zoneId":"inc-bread","x":329,"y":306,"clientX":329,"clientY":306}
{"seq":14,"wallMs":1781373190120,"kind":"tap","zoneId":"inc-bread","x":329,"y":306,"clientX":329,"clientY":306}
{"seq":15,"wallMs":1781373190254,"kind":"tap","zoneId":"inc-bread","x":329,"y":306,"clientX":329,"clientY":306}
{"seq":16,"wallMs":1781373190404,"kind":"tap","zoneId":"inc-bread","x":329,"y":306,"clientX":329,"clientY":306}
{"seq":17,"wallMs":1781373190538,"kind":"tap","zoneId":"inc-bread","x":329,"y":306,"clientX":329,"clientY":306}
{"seq":18,"wallMs":1781373190670,"kind":"tap","zoneId":"inc-bread","x":329,"y":306,"clientX":329,"clientY":306}
{"seq":19,"wallMs":1781373190820,"kind":"tap","zoneId":"openShop","x":195,"y":779,"clientX":195,"clientY":779}
{"seq":20,"wallMs":1781373191019,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":21,"wallMs":1781373191152,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":22,"wallMs":1781373191288,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":23,"wallMs":1781373191603,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":24,"wallMs":1781373191736,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":25,"wallMs":1781373191869,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":26,"wallMs":1781373192185,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":27,"wallMs":1781373192318,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":28,"wallMs":1781373192452,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":29,"wallMs":1781373192768,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":30,"wallMs":1781373192902,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":31,"wallMs":1781373193036,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":32,"wallMs":1781373193352,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":33,"wallMs":1781373193485,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":34,"wallMs":1781373193618,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":35,"wallMs":1781373193935,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":36,"wallMs":1781373194069,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":37,"wallMs":1781373194201,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":38,"wallMs":1781373194518,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":39,"wallMs":1781373194652,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":40,"wallMs":1781373194785,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":41,"wallMs":1781373195101,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":42,"wallMs":1781373195235,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":43,"wallMs":1781373195369,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":44,"wallMs":1781373195685,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":45,"wallMs":1781373195818,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":46,"wallMs":1781373195952,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":47,"wallMs":1781373196268,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":48,"wallMs":1781373196402,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":49,"wallMs":1781373196535,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":50,"wallMs":1781373196851,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":51,"wallMs":1781373196985,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":52,"wallMs":1781373197128,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":53,"wallMs":1781373197435,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":54,"wallMs":1781373197568,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":55,"wallMs":1781373197701,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":56,"wallMs":1781373198018,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":57,"wallMs":1781373198151,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":58,"wallMs":1781373198284,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":59,"wallMs":1781373198601,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":60,"wallMs":1781373198735,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":61,"wallMs":1781373198868,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":62,"wallMs":1781373199185,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":63,"wallMs":1781373199318,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":64,"wallMs":1781373199451,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":65,"wallMs":1781373199768,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":66,"wallMs":1781373199902,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":67,"wallMs":1781373200034,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":68,"wallMs":1781373200351,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":69,"wallMs":1781373200484,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":70,"wallMs":1781373200618,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":71,"wallMs":1781373200934,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":72,"wallMs":1781373201068,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":73,"wallMs":1781373201201,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":74,"wallMs":1781373201518,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":75,"wallMs":1781373201651,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":76,"wallMs":1781373201784,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":77,"wallMs":1781373202101,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":78,"wallMs":1781373202240,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":79,"wallMs":1781373202368,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":80,"wallMs":1781373202684,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":81,"wallMs":1781373202817,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":82,"wallMs":1781373202951,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":83,"wallMs":1781373203268,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":84,"wallMs":1781373203401,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":85,"wallMs":1781373203535,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":86,"wallMs":1781373203851,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":87,"wallMs":1781373203984,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":88,"wallMs":1781373204118,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":89,"wallMs":1781373204434,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":90,"wallMs":1781373204567,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":91,"wallMs":1781373204701,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":92,"wallMs":1781373205018,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":93,"wallMs":1781373205151,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":94,"wallMs":1781373205284,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":95,"wallMs":1781373205601,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":96,"wallMs":1781373205734,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":97,"wallMs":1781373205869,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":98,"wallMs":1781373206185,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":99,"wallMs":1781373206318,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":100,"wallMs":1781373206451,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":101,"wallMs":1781373206768,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":102,"wallMs":1781373206901,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":103,"wallMs":1781373207035,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":104,"wallMs":1781373207351,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":105,"wallMs":1781373207485,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":106,"wallMs":1781373207618,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":107,"wallMs":1781373207934,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":108,"wallMs":1781373208068,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":109,"wallMs":1781373208201,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":110,"wallMs":1781373208517,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":111,"wallMs":1781373208651,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":112,"wallMs":1781373208784,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":113,"wallMs":1781373209100,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":114,"wallMs":1781373209234,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":115,"wallMs":1781373209368,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":116,"wallMs":1781373209684,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":117,"wallMs":1781373209818,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":118,"wallMs":1781373209951,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":119,"wallMs":1781373210267,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":120,"wallMs":1781373210401,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":121,"wallMs":1781373210534,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":122,"wallMs":1781373210851,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":123,"wallMs":1781373210984,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":124,"wallMs":1781373211117,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":125,"wallMs":1781373211434,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":126,"wallMs":1781373211567,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":127,"wallMs":1781373211702,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":128,"wallMs":1781373212018,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":129,"wallMs":1781373212151,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":130,"wallMs":1781373212285,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":131,"wallMs":1781373212600,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":132,"wallMs":1781373212734,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":133,"wallMs":1781373212867,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":134,"wallMs":1781373213184,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":135,"wallMs":1781373213317,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":136,"wallMs":1781373213451,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":137,"wallMs":1781373213767,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":138,"wallMs":1781373213901,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":139,"wallMs":1781373214036,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":140,"wallMs":1781373214351,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":141,"wallMs":1781373214484,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":142,"wallMs":1781373214617,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":143,"wallMs":1781373214934,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":144,"wallMs":1781373215067,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":145,"wallMs":1781373215201,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":146,"wallMs":1781373215518,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":147,"wallMs":1781373215651,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":148,"wallMs":1781373215786,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":149,"wallMs":1781373216101,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":150,"wallMs":1781373216234,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":151,"wallMs":1781373216367,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":152,"wallMs":1781373216684,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":153,"wallMs":1781373216817,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":154,"wallMs":1781373216951,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":155,"wallMs":1781373217269,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":156,"wallMs":1781373217400,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":157,"wallMs":1781373217534,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":158,"wallMs":1781373217851,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":159,"wallMs":1781373217984,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":160,"wallMs":1781373218117,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":161,"wallMs":1781373218434,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":162,"wallMs":1781373218567,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":163,"wallMs":1781373218701,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":164,"wallMs":1781373219017,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":165,"wallMs":1781373219150,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":166,"wallMs":1781373219284,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":167,"wallMs":1781373219602,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":168,"wallMs":1781373219736,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":169,"wallMs":1781373219869,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":170,"wallMs":1781373220184,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":171,"wallMs":1781373220318,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":172,"wallMs":1781373220451,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":173,"wallMs":1781373220767,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":174,"wallMs":1781373220901,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":175,"wallMs":1781373221034,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":176,"wallMs":1781373221350,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":177,"wallMs":1781373221484,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":178,"wallMs":1781373221617,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":179,"wallMs":1781373221934,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":180,"wallMs":1781373222067,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":181,"wallMs":1781373222202,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":182,"wallMs":1781373222517,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":183,"wallMs":1781373222651,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":184,"wallMs":1781373222784,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":185,"wallMs":1781373223100,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":186,"wallMs":1781373223234,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":187,"wallMs":1781373223368,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":188,"wallMs":1781373223684,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":189,"wallMs":1781373223817,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":190,"wallMs":1781373223951,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":191,"wallMs":1781373224268,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":192,"wallMs":1781373224401,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":193,"wallMs":1781373224534,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":194,"wallMs":1781373224851,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":195,"wallMs":1781373224985,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":196,"wallMs":1781373225117,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":197,"wallMs":1781373225434,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":198,"wallMs":1781373225567,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":199,"wallMs":1781373225700,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":200,"wallMs":1781373226017,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":201,"wallMs":1781373226151,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":202,"wallMs":1781373226284,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":203,"wallMs":1781373226601,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":204,"wallMs":1781373226735,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":205,"wallMs":1781373226867,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":206,"wallMs":1781373227183,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":207,"wallMs":1781373227317,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":208,"wallMs":1781373227451,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":209,"wallMs":1781373227769,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":210,"wallMs":1781373227901,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":211,"wallMs":1781373228034,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":212,"wallMs":1781373228350,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":213,"wallMs":1781373228484,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":214,"wallMs":1781373228620,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":215,"wallMs":1781373228955,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":216,"wallMs":1781373229084,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":217,"wallMs":1781373229217,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":218,"wallMs":1781373229536,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":219,"wallMs":1781373229667,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":220,"wallMs":1781373229801,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":221,"wallMs":1781373230117,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":222,"wallMs":1781373230251,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":223,"wallMs":1781373230387,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":224,"wallMs":1781373230701,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":225,"wallMs":1781373230840,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":226,"wallMs":1781373230970,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":227,"wallMs":1781373231285,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":228,"wallMs":1781373231419,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":229,"wallMs":1781373231550,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":230,"wallMs":1781373231867,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":231,"wallMs":1781373232000,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":232,"wallMs":1781373232134,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":233,"wallMs":1781373232451,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":234,"wallMs":1781373232584,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":235,"wallMs":1781373232717,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":236,"wallMs":1781373233034,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":237,"wallMs":1781373233168,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":238,"wallMs":1781373233300,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":239,"wallMs":1781373233617,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":240,"wallMs":1781373233755,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":241,"wallMs":1781373233884,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":242,"wallMs":1781373234200,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":243,"wallMs":1781373234334,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":244,"wallMs":1781373234467,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":245,"wallMs":1781373234784,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":246,"wallMs":1781373234919,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":247,"wallMs":1781373235052,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":248,"wallMs":1781373235362,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":249,"wallMs":1781373235485,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":250,"wallMs":1781373235619,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":251,"wallMs":1781373235934,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":252,"wallMs":1781373236068,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":253,"wallMs":1781373236200,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":254,"wallMs":1781373236517,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":255,"wallMs":1781373236650,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":256,"wallMs":1781373236783,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":257,"wallMs":1781373237100,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":258,"wallMs":1781373237234,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":259,"wallMs":1781373237367,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":260,"wallMs":1781373237685,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":261,"wallMs":1781373237817,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":262,"wallMs":1781373237952,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":263,"wallMs":1781373238267,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":264,"wallMs":1781373238400,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":265,"wallMs":1781373238534,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":266,"wallMs":1781373238850,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":267,"wallMs":1781373238984,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":268,"wallMs":1781373239117,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":269,"wallMs":1781373239435,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":270,"wallMs":1781373239568,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":271,"wallMs":1781373239700,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":272,"wallMs":1781373240017,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":273,"wallMs":1781373240150,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":274,"wallMs":1781373240283,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":275,"wallMs":1781373240601,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":276,"wallMs":1781373240733,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":277,"wallMs":1781373240867,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":278,"wallMs":1781373241184,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":279,"wallMs":1781373241317,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":280,"wallMs":1781373241450,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":281,"wallMs":1781373241766,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":282,"wallMs":1781373241900,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":283,"wallMs":1781373242033,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":284,"wallMs":1781373242350,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":285,"wallMs":1781373242484,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":286,"wallMs":1781373242619,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":287,"wallMs":1781373242933,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":288,"wallMs":1781373243066,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":289,"wallMs":1781373243201,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":290,"wallMs":1781373243517,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":291,"wallMs":1781373243650,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":292,"wallMs":1781373243784,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":293,"wallMs":1781373244100,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":294,"wallMs":1781373244234,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":295,"wallMs":1781373244367,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":296,"wallMs":1781373244683,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":297,"wallMs":1781373244816,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":298,"wallMs":1781373244950,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":299,"wallMs":1781373245267,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":300,"wallMs":1781373245400,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":301,"wallMs":1781373245533,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":302,"wallMs":1781373245850,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":303,"wallMs":1781373245984,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":304,"wallMs":1781373246117,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":305,"wallMs":1781373246433,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":306,"wallMs":1781373246566,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":307,"wallMs":1781373246701,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":308,"wallMs":1781373247016,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":309,"wallMs":1781373247150,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":310,"wallMs":1781373247284,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":311,"wallMs":1781373247601,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":312,"wallMs":1781373247734,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":313,"wallMs":1781373247868,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":314,"wallMs":1781373248183,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":315,"wallMs":1781373248316,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":316,"wallMs":1781373248449,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":317,"wallMs":1781373248766,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":318,"wallMs":1781373248900,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":319,"wallMs":1781373249033,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":320,"wallMs":1781373249349,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":321,"wallMs":1781373249501,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":322,"wallMs":1781373249638,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":323,"wallMs":1781373249949,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":324,"wallMs":1781373250083,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":325,"wallMs":1781373250217,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":326,"wallMs":1781373250534,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":327,"wallMs":1781373250666,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":328,"wallMs":1781373250800,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":329,"wallMs":1781373251116,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":330,"wallMs":1781373251250,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":331,"wallMs":1781373251383,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":332,"wallMs":1781373251699,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":333,"wallMs":1781373251833,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":334,"wallMs":1781373251966,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":335,"wallMs":1781373252283,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":336,"wallMs":1781373252416,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":337,"wallMs":1781373252550,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":338,"wallMs":1781373252866,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":339,"wallMs":1781373253000,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":340,"wallMs":1781373253133,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":341,"wallMs":1781373253452,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":342,"wallMs":1781373253582,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":343,"wallMs":1781373253718,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":344,"wallMs":1781373254032,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":345,"wallMs":1781373254166,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":346,"wallMs":1781373254300,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":347,"wallMs":1781373254616,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":348,"wallMs":1781373254749,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":349,"wallMs":1781373254884,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":350,"wallMs":1781373255201,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":351,"wallMs":1781373255333,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":352,"wallMs":1781373255466,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":353,"wallMs":1781373255782,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":354,"wallMs":1781373255916,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":355,"wallMs":1781373256050,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":356,"wallMs":1781373256366,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":357,"wallMs":1781373256499,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":358,"wallMs":1781373256635,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":359,"wallMs":1781373256950,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":360,"wallMs":1781373257082,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":361,"wallMs":1781373257216,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":362,"wallMs":1781373257533,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":363,"wallMs":1781373257667,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":364,"wallMs":1781373257799,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":365,"wallMs":1781373258117,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":366,"wallMs":1781373258249,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":367,"wallMs":1781373258375,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":368,"wallMs":1781373258683,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":369,"wallMs":1781373258819,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":370,"wallMs":1781373258950,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":371,"wallMs":1781373259266,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":372,"wallMs":1781373259400,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":373,"wallMs":1781373259533,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":374,"wallMs":1781373259849,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":375,"wallMs":1781373259982,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":376,"wallMs":1781373260116,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":377,"wallMs":1781373260432,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":378,"wallMs":1781373260566,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":379,"wallMs":1781373260700,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":380,"wallMs":1781373261015,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":381,"wallMs":1781373261149,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":382,"wallMs":1781373261282,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":383,"wallMs":1781373261599,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":384,"wallMs":1781373261732,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":385,"wallMs":1781373261865,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":386,"wallMs":1781373262182,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":387,"wallMs":1781373262315,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":388,"wallMs":1781373262448,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":389,"wallMs":1781373262768,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":390,"wallMs":1781373262898,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":391,"wallMs":1781373263032,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":392,"wallMs":1781373263348,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":393,"wallMs":1781373263482,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":394,"wallMs":1781373263615,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":395,"wallMs":1781373263932,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":396,"wallMs":1781373264066,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":397,"wallMs":1781373264199,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":398,"wallMs":1781373264515,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":399,"wallMs":1781373264649,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":400,"wallMs":1781373264782,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":401,"wallMs":1781373265099,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":402,"wallMs":1781373265232,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":403,"wallMs":1781373265365,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":404,"wallMs":1781373265682,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":405,"wallMs":1781373265815,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":406,"wallMs":1781373265949,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":407,"wallMs":1781373266265,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":408,"wallMs":1781373266398,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":409,"wallMs":1781373266532,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":410,"wallMs":1781373266848,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":411,"wallMs":1781373266982,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":412,"wallMs":1781373267115,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":413,"wallMs":1781373267432,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":414,"wallMs":1781373267566,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":415,"wallMs":1781373267700,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":416,"wallMs":1781373268015,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":417,"wallMs":1781373268148,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":418,"wallMs":1781373268284,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":419,"wallMs":1781373268598,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":420,"wallMs":1781373268732,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":421,"wallMs":1781373268865,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":422,"wallMs":1781373269182,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":423,"wallMs":1781373269318,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":424,"wallMs":1781373269449,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":425,"wallMs":1781373269766,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":426,"wallMs":1781373269917,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":427,"wallMs":1781373270049,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":428,"wallMs":1781373270366,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":429,"wallMs":1781373270499,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":430,"wallMs":1781373270633,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":431,"wallMs":1781373270948,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":432,"wallMs":1781373271082,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":433,"wallMs":1781373271215,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":434,"wallMs":1781373271532,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":435,"wallMs":1781373271665,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":436,"wallMs":1781373271798,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":437,"wallMs":1781373272115,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":438,"wallMs":1781373272248,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":439,"wallMs":1781373272383,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":440,"wallMs":1781373272700,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":441,"wallMs":1781373272832,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":442,"wallMs":1781373272967,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":443,"wallMs":1781373273282,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":444,"wallMs":1781373273416,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":445,"wallMs":1781373273549,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":446,"wallMs":1781373273866,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":447,"wallMs":1781373273998,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":448,"wallMs":1781373274132,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":449,"wallMs":1781373274448,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":450,"wallMs":1781373274582,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":451,"wallMs":1781373274715,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":452,"wallMs":1781373275032,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":453,"wallMs":1781373275165,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":454,"wallMs":1781373275298,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":455,"wallMs":1781373275616,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":456,"wallMs":1781373275749,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":457,"wallMs":1781373275882,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":458,"wallMs":1781373276198,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":459,"wallMs":1781373276332,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":460,"wallMs":1781373276465,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":461,"wallMs":1781373276781,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":462,"wallMs":1781373276915,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":463,"wallMs":1781373277048,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":464,"wallMs":1781373277365,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":465,"wallMs":1781373277498,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":466,"wallMs":1781373277634,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":467,"wallMs":1781373277966,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":468,"wallMs":1781373278100,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":469,"wallMs":1781373278233,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":470,"wallMs":1781373278548,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":471,"wallMs":1781373278682,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":472,"wallMs":1781373278815,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":473,"wallMs":1781373279134,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":474,"wallMs":1781373279265,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":475,"wallMs":1781373279398,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":476,"wallMs":1781373279716,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":477,"wallMs":1781373279848,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":478,"wallMs":1781373279982,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":479,"wallMs":1781373280299,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":480,"wallMs":1781373280432,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":481,"wallMs":1781373280565,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":482,"wallMs":1781373280881,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":483,"wallMs":1781373281015,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":484,"wallMs":1781373281148,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":485,"wallMs":1781373281464,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":486,"wallMs":1781373281598,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":487,"wallMs":1781373281731,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":488,"wallMs":1781373282048,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":489,"wallMs":1781373282181,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":490,"wallMs":1781373282315,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":491,"wallMs":1781373282632,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":492,"wallMs":1781373282765,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":493,"wallMs":1781373282900,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":494,"wallMs":1781373283214,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":495,"wallMs":1781373283348,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":496,"wallMs":1781373283481,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":497,"wallMs":1781373283798,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":498,"wallMs":1781373283932,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":499,"wallMs":1781373284064,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":500,"wallMs":1781373284382,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":501,"wallMs":1781373284515,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":502,"wallMs":1781373284649,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":503,"wallMs":1781373284965,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":504,"wallMs":1781373285098,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":505,"wallMs":1781373285231,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":506,"wallMs":1781373285548,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":507,"wallMs":1781373285681,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":508,"wallMs":1781373285814,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":509,"wallMs":1781373286131,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":510,"wallMs":1781373286264,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":511,"wallMs":1781373286397,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":512,"wallMs":1781373286714,"kind":"tap","zoneId":"serve-soda","x":74,"y":618,"clientX":74,"clientY":618}
{"seq":513,"wallMs":1781373286866,"kind":"tap","zoneId":"serve-snack","x":198,"y":618,"clientX":198,"clientY":618}
{"seq":514,"wallMs":1781373286999,"kind":"tap","zoneId":"serve-bread","x":322,"y":618,"clientX":322,"clientY":618}
{"seq":515,"wallMs":1781373288600,"kind":"tap","zoneId":"confirmSettle","x":195,"y":594,"clientX":195,"clientY":594}
{"seq":516,"wallMs":1781373288794,"kind":"tap","zoneId":"up-speed1","x":108,"y":442,"clientX":108,"clientY":442}

View File

@ -0,0 +1,6 @@
{"seq":0,"anchor":"t_boot","tMono":0,"buildSha":"","pluginSet":["audio-music","collision","gamefeel","palette-post","particles-juice","physics-lite","runtime-probe","save-progress"],"prevHash":"428bfee0","hash":"aaca40e4","extra":{"by":"ref-host","stage":"pre-init","engineMode":"real"},"warning":null}
{"seq":1,"anchor":"t_plugins_ready","tMono":0,"buildSha":"","pluginSet":["audio-music","collision","gamefeel","palette-post","particles-juice","physics-lite","runtime-probe","save-progress"],"prevHash":"aaca40e4","hash":"71ff4c61","extra":{"initialized":7,"ok":true},"warning":null}
{"seq":2,"anchor":"t_game_init","tMono":0,"buildSha":"","pluginSet":["audio-music","collision","gamefeel","palette-post","particles-juice","physics-lite","runtime-probe","save-progress"],"prevHash":"71ff4c61","hash":"3c4297be","extra":{"assetsLoaded":13,"assetsTotal":13,"loopProofOk":true},"warning":null}
{"seq":3,"anchor":"t_first_paint","tMono":0,"buildSha":"","pluginSet":["audio-music","collision","gamefeel","palette-post","particles-juice","physics-lite","runtime-probe","save-progress"],"prevHash":"3c4297be","hash":"9f2c7a57","extra":{"frame":1},"warning":null}
{"seq":4,"anchor":"t_input_bound","tMono":16.667,"buildSha":"","pluginSet":["audio-music","collision","gamefeel","palette-post","particles-juice","physics-lite","runtime-probe","save-progress"],"prevHash":"9f2c7a57","hash":"321fb8ee","extra":{"stage":"registered","type":"pointerdown"},"warning":null}
{"seq":5,"anchor":"t_game_start","tMono":16.667,"buildSha":"","pluginSet":["audio-music","collision","gamefeel","palette-post","particles-juice","physics-lite","runtime-probe","save-progress"],"prevHash":"321fb8ee","hash":"470c61d2","extra":{"mode":"play","engineMode":"real"},"warning":null}

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -5,8 +5,11 @@
· 集成段mini-desktop锁参构建见 BUILD.md产物 dist/ref-bundle.jsiife 全局 WanglanmeiRef
· serve 本目录test/harness/static-serve.cjs浏览器打开
· ?seed=12345 注入主随机种子(缺省固定种子,确定可复现);
· ?mode=evidence 取证模式:不起 RAF由 window.__refHost.stepFrames/tap 确定性驱动;
· 取证导出window.__refForensics / window.__refChecklist字段说明见 FORENSICS.md
· ?engine=stub缺省·回滚锚/ ?engine=real引擎掌帧·游戏美术与引擎粒子同屏P1
· ?mode=evidence 取证模式stub 下不起 RAF由 window.__refHost.stepFrames/tap 确定性驱动;
· 取证导出window.__refForensics / window.__refChecklist字段说明见 FORENSICS.md
· 就绪标记__refBootedbootRef().then 置两通道通用real 通道额外置 __refBootedRealmain.js setupReal 内 game.init 完成后置,
CDP 取证 real 分支须等它再驱动——见 e2e-input.cdp.cjsM1
注意:页面对外文本走 IP gate零受保护字样本页标题即对外名「暖阳小卖部」。
-->
<html lang="zh-CN">

View File

@ -0,0 +1,729 @@
/**
* game-shop.js 暖阳小卖部 游戏工厂装载契约 GameHostFactory 实现 · 硬独立文件 · 零引擎 import
* ownerRunner v2 P1 消费方main.jshost boot 调本工厂建 GameInstance+ P3 生成 agent复制参照靶
*
*
* 本文件是什么·A3 硬约束
* 这是游戏宿主装载契约game-runtime/src/core/game-host.d.ts 9 类契约 ref 参照实现
* · 默认导出一个 GameHostFactorycreateShopGame(opts) GameInstance
* · 零引擎 import `import littlejsengine`引擎能力一律经 boot.ctx.getEngine()Q4 铁律
* · 必须是独立可被 P3 生成 agent 直接复制的工厂模块 main.js 内联闭包糊过P3 需真实参照
* GameInstance 生命周期对齐引擎五回调init/update/render/onInput?/destroy
* · init(boot)建游戏对象shop-core/line-picker/fx/audio装资产订阅 boot.ctx.getInput()保存 boot.mainContext 为绘制面
* · update(dt)逻辑步core.update / bundle.tick / applyEvents / fx.step / 联动
* · render(g)绘制段drawFrame(g,...) + palettePost.renderPost(g)g=host 注入的绘制面real=引擎 mainContext / stub=#game ctx
* · destroy()卸载卫生P1 最小实现须声明
*
* host游戏 接线 seam为何 opts 携带 runtime
* 装载契约 boot 只承契约脊柱{ctx, mainContext, canvas, seed}跨游戏稳定面
* 而本游戏复用的能力插件实例collision/gamefeel/juice/palettePost/audioMusic/save+ host 私有 bundle
* deriveContextFor / frameCount PluginContext 成员 host 拥有并经**工厂 opts** 注入host本游戏配对的实现细节
* GameHostFactory opts 形参正为此 additive 扩展位预留契约 §5这样契约面boot保持纯净
* host wiring具体 runtime 句柄 optsP3 生成游戏由其 host 按同形提供 runtime
*
* 红线遵守
* - 零裸 Date.now/Math.random/AudioContext 全部经 boot.ctx 受控面 / host 注入的 audio director
* - import littlejsengine引擎能力经 boot.ctx.getEngine()
* - 游戏逻辑与 stub/real 通道无关同一份工厂绘制面差异由 host 注入的 g 吸收DPR 补偿由 host render 外层处理
*
*/
'use strict';
import { aabbVsAabb } from '../../../src/plugins/collision/impl.js';
import { createShopCore } from './shop-core.js';
import { createLinePicker, LINES } from './shopkeeper-lines.js';
import { createPalette } from './palette.js';
import { loadAssets, PRODUCT_IMAGE_KEY } from './assets.js';
import { createHostFx } from './hostfx.js';
import { createAudioDirector } from './audio-director.js';
import { drawFrame, computeZones, shelfSlotRect, W as VIEW_W, H as VIEW_H, LAYOUT } from './render.js';
import { proveCoreLoop, proveFailure } from './loop-proof.js';
import { estimateDayOutcome, ECON, fmtYuan, productOf } from './balance.js';
/** 固定逻辑步(与 host FIXED_DT 对齐;引擎 real 通道恒定步长stub evidence 同值)。 */
const FIXED_DT = 1 / 60;
/**
* 装载契约 GameHostFactory建暖阳小卖部游戏实例
* @param {Object} [opts] 工厂选项host wiring + additive 扩展位
* @param {Object} opts.runtime host 注入的运行时句柄host 拥有本游戏复用
* - plugins已注册并 init 的能力插件实例 { collision, physics, gamefeel, juice, palettePost, audioMusic, save }
* - bundlehost 受控面 bundlederiveContextFor/frameCount host 私有编排成员 PluginContext
* - getRealAudioCtx取真实 AudioContext 的回调audio director host 管解锁时机
* @returns {import('../../../src/core/game-host.d.ts').GameInstance}
*/
export default function createShopGame(opts) {
const o = opts || {};
const runtime = o.runtime || {};
const plugins = runtime.plugins || {};
const bundle = runtime.bundle; // host 私有 bundlederiveContextFor/frameCount本游戏复用 host 编排面
const getRealAudioCtx = runtime.getRealAudioCtx || (() => null);
// 能力插件实例host 已注册+init本游戏经其公开 API 用,一职一路)。
const collision = plugins.collision;
const juice = plugins.juice;
const palettePost = plugins.palettePost;
const audioMusic = plugins.audioMusic;
const save = plugins.save;
/* ── 以下闭包态在 init(boot) 内填充boot 提供受控面/绘制面/种子/可见画布)── */
/** @type {import('../../../src/core/game-host.d.ts').GameHostBootContext|null} */
let boot = null;
/** @type {import('../../../src/core/api.d.ts').PluginContext|null} */
let ctx = null;
let seed = 0;
// 【mustFix #2并轨成单一推进时钟】步进时钟读数毫秒
// 原缺陷:本游戏曾持私有 mockNowMs 并在 update 内 += dt*1000 自增,与 host 喂 bundle.clock 的钟分裂 →
// bundle.clock 恒 0 → 输入事件 tMs=0 → InputBuffer 记 0 → 接待缓冲补打窗口判恒假measures.bufferedServeReplays 恒 0
// 修法删私有自增本钟改为「host 受控时钟boot.ctx.time.nowMs(),即 host 唯一推进的 mockNowMs的读数」。
// - update→applyEvents→render 同步流:在 update 首行刷新本快照host 已在调 update 前自增,值=本帧逻辑钟),
// 供本流内一切相对时间计算say/banner/ghosts 过期/idle 节拍)逐行读同一帧值(与原 update 首行自增后逐行一致)。
// - onTap输入回调帧间触发另读 ctx.time.nowMs() 取**当刻**值(见 onTap 内 pendingServe.tMs保与
// InputBuffer 同次 _emit 盖的 tMs 同钟同刻 → 补打窗口判真实生效。
let mockNowMs = 0;
// 游戏对象init 内建)。
let core = null, pickLine = null, pal = null, fx = null, audio = null, assets = null;
// boot 期纯逻辑证明init 内跑,挂 forensics
let loopProof = null, failureProof = null, priceProof = null;
// 输入订阅句柄destroy 时取消)。
let subDown = null, subMove = null, subUp = null;
// 是否已 initdestroy 幂等 + update/render 在 init 前空跑保护)。
let inited = false;
// 音频是否已解锁host 拥有 AudioContext 解锁时机,经 _onAudioUnlock 通知本游戏;游戏据此起 BGM + 空点 tick 门控)。
let audioUnlocked = false;
/* ── 宿主 UI 态init 内初始化,避免实例间共享)── */
const ui = {
bubbleText: '',
bubbleUntil: 0,
bubbleShownAt: -9999,
keeperWaveUntil: 0,
tickerText: '小本生意,用心经营。',
banner: null, // {text,color,until,dur}
panelSlide: 1, // prep 面板滑入 0→1
settleAnim: 0, // 日结计数 0→1
/** @type {Array<{slot:number, avatar:number, emote:string, until:number}>} 离场幽灵(视觉余韵)。 */
ghosts: [],
};
/** @type {Record<number, number>} 顾客出生时刻(走位动画)。 */
const customerBornMs = {};
/* ── 度量checklist 自动子项的数据源)── */
const measures = {
frames: 0,
taps: 0,
tapFeedbackMaxFrames: 0,
comboUiShown: 0,
hitStopCount: 0,
rushPaletteSeen: false,
bgmCrossfadeSeen: false,
linesShown: 0,
bufferedServeReplays: 0,
};
/** @type {Array<object>} 最近逻辑事件环形缓冲(取证)。 */
const recentEvents = [];
function pushRecent(e) {
recentEvents.push(e);
if (recentEvents.length > 240) recentEvents.shift();
}
/* ── 台词气泡 ── */
/**
* 店主说一句场景取词普通优先级 700ms 内不顶替
* @param {string} sceneKey @param {boolean} [force] 高优先级强插
*/
function say(sceneKey, force) {
if (!force && mockNowMs - ui.bubbleShownAt < 700 && ui.bubbleUntil > mockNowMs) return;
const text = pickLine(sceneKey);
if (!text) return;
ui.bubbleText = text;
ui.bubbleUntil = mockNowMs + 2600;
ui.bubbleShownAt = mockNowMs;
ui.tickerText = text;
measures.linesShown += 1;
fx.springKick('bubble', 2.2);
}
/** 横幅。 */
function banner(text, color, durMs) {
ui.banner = { text, color, dur: durMs || 1500, until: mockNowMs + (durMs || 1500) };
}
function hasSave() {
return !!save.get('profile');
}
/* ── 命中区视觉同源几何collision broadphase + 窄相点测)── */
/** @type {Array<any>} 当前帧命中区。 */
let zones = [];
function rebuildZones() {
zones = computeZones(core, { hasSave: hasSave() });
collision.spatial.clear();
for (const z of zones) collision.spatial.insert(z.id, { x: z.x, y: z.y, hw: z.hw, hh: z.hh });
}
/** 点测broadphase 粗筛 → aabbVsAabb 窄相 → 取 z 最高者。 */
function hitZone(x, y) {
const candidates = collision.spatial.queryPoint(x, y);
let best = null;
for (const c of candidates) {
const z = zones.find((zz) => zz.id === c.id);
if (!z) continue;
if (!aabbVsAabb({ x, y, hw: 0, hh: 0 }, { x: z.x, y: z.y, hw: z.hw, hh: z.hh }).hit) continue;
if (!best || z.z > best.z) best = z;
}
return best;
}
/* ── 接待缓冲重放P4 输入缓冲:冷却期点击不丢,窗口内自动补打)── */
/** @type {{productId:string, tMs:number}|null} */
let pendingServe = null;
function maybeReplayBufferedServe() {
if (!pendingServe || core.state.phase !== 'open' || core.state.serveCooldown > 0) return;
const within = mockNowMs - pendingServe.tMs <= 260;
const gamefeel = plugins.gamefeel;
if (within && gamefeel.inputBuffer && gamefeel.inputBuffer.consume('pointerdown', mockNowMs)) {
const pid = pendingServe.productId;
pendingServe = null;
const r = core.serve(pid);
if (r.ok) measures.bufferedServeReplays += 1;
} else if (!within) {
pendingServe = null;
}
}
/* ── 点击路由 ── */
let sliderDrag = null; // {track}
/** 滑条 x → 定价。 */
function priceFromX(x, track) {
const v = Math.max(0, Math.min(1, (x - track.x) / track.w));
core.setPrice(ECON.priceMin + v * (ECON.priceMax - ECON.priceMin));
}
function onTap(e) {
measures.taps += 1;
const tapFrame = bundle.frameCount();
fx.addTapRing(e.x, e.y); // 当帧反馈≤1 渲染帧可见)
const z = hitZone(e.x, e.y);
if (!z) {
// 空点:极轻 tick仍有听觉回执不打扰。
if (audioUnlocked) audio.sfx('tick');
noteFeedback(tapFrame);
return;
}
fx.springKick('z:' + z.id, -2.6); // 按压脉冲render 侧按钮读同名弹簧 'z:<zoneId>'
switch (z.kind) {
case 'serve': {
const pid = z.payload.productId;
fx.springKick('slot-' + pid, -3.2);
const r = core.serve(pid);
if (!r.ok) {
if (r.reason === 'cooldown') {
// 【mustFix #2】tMs 取**本次输入事件自带的受控时间戳 e.tMs**= inputBridge._emit 一次性盖、
// 并与 gamefeel InputBuffer 同次 _emit 记录的 tMs 完全同值)→ 补打窗口判nowtMs<=260
// InputBuffer.consume 的 windowMs 判读同一钟同一刻,真实生效(取代原读私有 mockNowMs 致的双钟错位)。
pendingServe = { productId: pid, tMs: e.tMs }; // 输入缓冲补打
} else if (r.reason === 'no_stock') {
audio.sfx('empty');
const rect = shelfSlotRect(core.state.unlocked.indexOf(pid));
fx.addFloater('缺货…', rect.x + rect.w / 2, rect.y, 'danger', 13);
} else if (r.reason === 'no_customer') {
audio.sfx('tick');
const rect = shelfSlotRect(core.state.unlocked.indexOf(pid));
fx.addFloater('没人要这个', rect.x + rect.w / 2, rect.y, 'inkSoft', 12);
}
}
break;
}
case 'stockInc': {
const r = core.buyStock(z.payload.productId, 1);
if (r.ok) audio.sfx('tick');
else {
audio.sfx('empty');
fx.addFloater(r.reason === 'no_cash' ? '钱不够了' : '货架满了', z.x, z.y - 22, 'danger', 13);
}
break;
}
case 'stockDec': {
const r = core.refundStock(z.payload.productId, 1);
audio.sfx(r.ok ? 'tick' : 'empty');
break;
}
case 'slider':
sliderDrag = { track: z.payload.track };
priceFromX(e.x, z.payload.track);
audio.sfx('tick');
break;
case 'upgrade': {
const r = core.buyUpgrade(z.payload.upgradeId);
if (!r.ok && r.reason === 'no_cash') {
audio.sfx('empty');
fx.addFloater('钱不够了', z.x, z.y - 24, 'danger', 13);
} else if (!r.ok && r.reason === 'requires') {
audio.sfx('empty');
fx.addFloater('要先买前置', z.x, z.y - 24, 'inkSoft', 12);
} else if (!r.ok && r.reason === 'owned') {
audio.sfx('tick');
}
break;
}
case 'btn':
audio.sfx('tick');
handleAction(z.payload.action);
break;
default:
break;
}
noteFeedback(tapFrame);
}
/** 反馈延迟度量:反馈在 tap 同帧注册,下一渲染帧可见 ⇒ 帧延迟 ≤1。 */
function noteFeedback(tapFrame) {
const delay = Math.max(bundle.frameCount() - tapFrame, 0) + 1; // +1=下一渲染帧可见的保守口径
measures.tapFeedbackMaxFrames = Math.max(measures.tapFeedbackMaxFrames, delay);
}
/** 按钮动作。 */
function handleAction(action) {
switch (action) {
case 'newGame':
core.startGame(null);
break;
case 'continue':
core.startGame(save.get('profile'));
break;
case 'openShop':
core.startDay();
break;
case 'confirmSettle':
core.confirmSettle();
break;
case 'confirmSummary':
core.confirmSummary();
break;
case 'restart':
core.startGame(null);
break;
default:
break;
}
}
/* ── 逻辑事件 → 表现(粒子/音效/台词/横幅/存档)── */
/** 槽位中心坐标。 */
function slotPos(slot) {
const s = LAYOUT.queue[slot] || LAYOUT.queue[0];
return { x: s.x, y: s.y };
}
function applyEvents(events) {
for (const e of events) {
pushRecent(e);
switch (e.type) {
case 'customer_arrive': {
customerBornMs[e.id] = mockNowMs;
audio.sfx('arrive');
break;
}
case 'serve_ok': {
const from = shelfSlotRect(core.state.unlocked.indexOf(e.productId));
const to = slotPos(e.slot);
delete customerBornMs[e.id];
// 满意幽灵(顾客拿到货的余韵)。
ui.ghosts.push({ slot: e.slot, avatar: e.avatar, emote: e.rescued ? '!' : '♪', until: mockNowMs + 460 });
// 商品飞行:货架 → 顾客(非线性缓动+弧线),到达再收钱爆金光。
fx.addFlight({
fx: from.x + 28,
fy: from.y + 28,
tx: to.x,
ty: to.y,
dur: 0.28,
imageKey: PRODUCT_IMAGE_KEY[e.productId],
label: (productOf(e.productId) || { name: '?' }).name[0],
onArrive: () => {
fx.burstCoin(to.x, to.y, e.big);
fx.addFloater(`${fmtYuan(e.revenueJiao)}`, to.x, to.y - 30, 'gold', e.big ? 19 : 15);
audio.sfx('coin', { combo: e.combo });
fx.springKick('hud-cash', 1.8);
if (e.rescued) {
fx.burstSparkle(to.x, to.y - 10, 14);
audio.sfx('rescue');
}
},
});
if (e.combo >= 2) {
fx.springKick('combo', 2.4);
if (e.combo === 3) measures.comboUiShown += 1;
}
if (e.big) {
// 大额成交hit-stop + 屏震 + 闪白(脆的打击感三件套)。
juice.hitStop(0.09, 0.12);
juice.shakeScreen(0.22, 5, 26);
juice.flashScreen(0.12, 0.18);
measures.hitStopCount += 1;
audio.sfx('bigsale');
ui.keeperWaveUntil = mockNowMs + 1300;
say('big_sale', true);
} else if (e.rescued) {
say('rescue', true);
} else if (e.combo >= 3 && e.combo % 3 === 0) {
say('combo_hot');
} else if (e.combo === 1 && core.state.today.served % 4 === 1) {
say('serve_ok');
}
break;
}
case 'angry_leave': {
const p = slotPos(e.slot);
delete customerBornMs[e.id];
ui.ghosts.push({ slot: e.slot, avatar: e.avatar, emote: '怒', until: mockNowMs + 520 });
fx.burstSmoke(p.x, p.y);
fx.addFloater('差评!', p.x, p.y - 36, 'danger', 15);
juice.shakeScreen(0.16, 3, 24);
audio.sfx('bad');
say('angry', true);
break;
}
case 'stockout_leave': {
const p = slotPos(e.slot);
delete customerBornMs[e.id];
ui.ghosts.push({ slot: e.slot, avatar: e.avatar, emote: '…', until: mockNowMs + 480 });
fx.burstSmoke(p.x, p.y);
fx.addFloater('白跑一趟…', p.x, p.y - 36, 'danger', 13);
audio.sfx('empty');
if (core.state.today.stockout % 3 === 1) say('stockout');
break;
}
case 'customer_grace':
audio.sfx('tick');
break;
case 'walkaway_full':
fx.addFloater('店里挤不下…', 372, 420, 'inkSoft', 12);
break;
case 'rush_start':
banner('高峰来了!', 'brick', 1600);
say('rush_start', true);
break;
case 'rush_end':
banner('缓过来了~', 'leaf', 1300);
break;
case 'closing':
banner('打烊咯~', 'wood', 1500);
break;
case 'day_start': {
say('day_start', true);
ui.keeperWaveUntil = mockNowMs + 1200;
// 进货上架弹道P2 Projectile开门瞬间把备货抛上架落架压扁+闷响。
let delay = 0;
for (let i = 0; i < core.state.unlocked.length; i++) {
const pid = core.state.unlocked[i];
const n = Math.min(core.state.stock[pid] || 0, 3);
const rect = shelfSlotRect(i);
for (let k = 0; k < n; k++) {
const d = delay;
fx.addTween(Math.max(d, 0.001), () => {}, {
onDone: () =>
fx.addToss({
fx: -24,
fy: 600,
tx: rect.x + 28,
ty: rect.y + 28,
dur: 0.42,
imageKey: PRODUCT_IMAGE_KEY[pid],
onLand: () => {
fx.springKick('slot-' + pid, -3.4);
audio.sfx('shelve');
},
}),
});
delay += 0.07;
}
}
break;
}
case 'day_end': {
ui.settleAnim = 0;
fx.addTween(1.1, (v) => {
ui.settleAnim = v;
});
say(e.settle.netJiao >= 0 ? 'settle_win' : 'settle_lose', true);
break;
}
case 'fined':
audio.sfx('bad');
break;
case 'upgrade_bought': {
fx.burstSparkle(VIEW_W / 2, 430, e.first ? 22 : 14);
fx.addFloater('升级!', VIEW_W / 2, 410, 'gold', 18);
audio.sfx('upgrade');
ui.keeperWaveUntil = mockNowMs + 1300;
say('upgrade', true);
break;
}
case 'price_changed':
break;
case 'phase_changed': {
if (e.to === 'prep') {
ui.panelSlide = 0;
fx.addTween(0.45, (v) => {
ui.panelSlide = v;
});
if (e.from === 'title' || e.from === 'gameover') say('open_shop', true);
}
ui.ghosts.length = 0;
break;
}
case 'gameover':
save.remove('profile'); // 失败清档(重开新局)
audio.sfx('bad');
say('gameover', true);
break;
case 'save_point':
save.set('profile', core.snapshotForSave());
break;
case 'segment_end':
break;
default:
break;
}
}
}
/* ── 闲时台词节拍器 ── */
let idleChatTimer = 7;
/*
* 装载契约 GameInstance 实现
* */
/**
* init建游戏对象 + 装资产 + 订阅受控输入boot.ctx.getInput异步loadAssets 异步
* @param {import('../../../src/core/game-host.d.ts').GameHostBootContext} b boot 上下文
*/
async function init(b) {
boot = b;
ctx = b.ctx;
seed = b.seed;
/* ── 游戏对象(逻辑随机=派生流;台词随机=另一派生流——互不推演)── */
core = createShopCore({ random: bundle.deriveContextFor('shop-core', 'shop-core').random });
pickLine = createLinePicker(bundle.deriveContextFor('shop-lines', 'shop-lines').random);
pal = createPalette();
fx = createHostFx({ bundle, juice });
audio = createAudioDirector({ getAudio: () => getRealAudioCtx(), p6: audioMusic });
/* ── 资产装载(容错:缺失走程序化回退,照常启动)── */
assets = await loadAssets('./assets/');
/* ── boot 期纯逻辑证明(闭环/失败态/定价两头输——挂 forensics 供 e2e 读)── */
loopProof = proveCoreLoop();
failureProof = proveFailure();
priceProof = (() => {
const lo = estimateDayOutcome({ priceMul: ECON.priceMin });
const mid = estimateDayOutcome({ priceMul: 1.0 });
const hi = estimateDayOutcome({ priceMul: ECON.priceMax });
return { loProfit: lo.profitJiao, midProfit: mid.profitJiao, hiProfit: hi.profitJiao, twoSidedLoss: lo.profitJiao < 0 && hi.profitJiao < 0 && mid.profitJiao > 0 };
})();
/* ── 游戏侧输入订阅经受控输入面与插件同一面boot.ctx.getInput── */
const input = ctx.getInput();
subDown = input.on('pointerdown', (e) => onTap(e));
subMove = input.on('pointermove', (e) => {
if (sliderDrag) priceFromX(e.x, sliderDrag.track);
});
subUp = input.on('pointerup', () => {
sliderDrag = null;
});
inited = true;
}
/**
* update逻辑步引擎 gameUpdate 每逻辑步驱动dt 恒定
* @param {number} dt 逻辑步秒数
*/
function update(dt) {
if (!inited) return;
// 【mustFix #2】刷新本帧逻辑钟快照=host 唯一推进时钟host 已在调本 update 前自增 mockNowMs故此读即本帧值
// 取代原私有 `mockNowMs += dt*1000` 自增双钟分裂根因本流update/applyEvents/render逐行读此快照
// 与原「update 首行自增后逐行读」语义逐行等价(确定性测状态终点不变)。
mockNowMs = ctx.time.nowMs();
const ts = juice.getTimeScale();
const dtWorld = dt * ts; // hit-stop 冻结世界(手感锚点)
core.update(dtWorld);
bundle.tick(dt); // 驱动插件 onFrame粒子 autoStep 等,按 PLUGIN.md 约定吃真实 dt引擎粒子自渲与此解耦
applyEvents(core.drainEvents());
maybeReplayBufferedServe();
fx.step(dt, dtWorld);
// 高峰 → 色板/暗角/BGM 三联动。
const rush = core.state.phase === 'open' ? core.state.rushFactor : 0;
pal.setRush(rush);
if (rush > 0.9) measures.rushPaletteSeen = true;
palettePost.setPost({ vignette: { enabled: true, strength: 0.22 + rush * 0.24 } });
audio.step(dt, rush, core.state.phase === 'open');
const ap = audio.probe();
if (ap.bgm.ready && ap.bgm.idleLevel > 0.02 && ap.bgm.rushLevel > 0.02) measures.bgmCrossfadeSeen = true;
// 闲时随口台词(营业中、不忙的时候偶尔来一句)。
if (core.state.phase === 'open') {
idleChatTimer -= dt;
if (idleChatTimer <= 0) {
idleChatTimer = 8 + (rush > 0.5 ? 4 : 0);
if (ui.bubbleUntil < mockNowMs) say(rush > 0.5 ? 'rush_start' : 'idle_chat');
}
}
// 幽灵过期清理。
for (let i = ui.ghosts.length - 1; i >= 0; i--) if (ui.ghosts[i].until <= mockNowMs) ui.ghosts.splice(i, 1);
rebuildZones();
measures.frames += 1;
}
/**
* render绘制段引擎 gameRender 清屏后驱动g=host 注入的绘制面
* 游戏美术与引擎粒子同屏靠g=引擎 mainContextrealhost 注入stub g=#game ctx
* DPR 变换补偿由 host 在调 render 前于外层处理real 引擎 mainContext DPR 变换host save+setTransform
* stub #game ctx 自带 DPR 变换host 不补本工厂只按逻辑像素绘制不感知通道差异
* @param {CanvasRenderingContext2D} g 绘制面=boot.mainContext 的同一面 / stub=#game ctx
*/
function render(g) {
if (!inited) return;
drawFrame(g, {
core,
pal,
assets,
fx,
ui,
nowMs: mockNowMs,
customerBornMs,
hasSave: hasSave(),
});
palettePost.renderPost(g); // 暗角收口(绘制末尾叠加)
}
/**
* destroy卸载卫生P1 最小实现取消输入订阅幂等游戏对象交由 host dispose 插件/bundle 统一回收
* P2 装载多游戏复位时此处补重置 UI / fx P1 ref 单游戏不复用实例最小即可须声明非空 no-op
*/
function destroy() {
try { if (subDown && subDown.cancel) subDown.cancel(); } catch (_) { /* 幂等 */ }
try { if (subMove && subMove.cancel) subMove.cancel(); } catch (_) { /* 幂等 */ }
try { if (subUp && subUp.cancel) subUp.cancel(); } catch (_) { /* 幂等 */ }
subDown = subMove = subUp = null;
inited = false;
}
/* ── 取证面host 经此挂 window.__refForensics/__refChecklist非装载契约成员host wiring── */
/**
* 取证导出host 拿去挂 window.__refForensics
* @returns {object}
*/
function forensicsView() {
return {
seed,
/** 可交互几何(当前帧;与视觉同源)。 */
geometry: () => zones.map((z) => ({ id: z.id, kind: z.kind, x: z.x, y: z.y, hw: z.hw, hh: z.hh, z: z.z })),
loopProof,
failureProof,
priceProof,
/** 游戏状态快照JSON 安全)。 */
state: () => JSON.parse(JSON.stringify({
phase: core.state.phase,
day: core.state.day,
cash: core.state.cash,
reputation: core.reputation(),
priceMul: core.state.priceMul,
stock: core.state.stock,
unlocked: core.state.unlocked,
upgrades: core.state.upgrades,
customers: core.state.customers.length,
combo: core.state.comboCount,
today: core.state.today,
lifetime: core.state.lifetime,
gameoverReason: core.state.gameoverReason,
})),
/** 最近逻辑事件(环形 240 条)。 */
recentEvents: () => recentEvents.slice(),
/** 实时度量。 */
measures: () => Object.assign({ particle: fx.particleProbe(), audio: audio.probe(), assetsMissing: assets.missing }, measures),
/** 资产装载进度host 取证用)。 */
assetsLoaded: () => ({ loaded: assets.loaded, total: assets.total }),
};
}
/**
* checklist 刷新host 逐帧调刷新 window.__refChecklist 的数据源
* @param {object} checklist host 持有的 checklist 对象本函数原地填
*/
function refreshChecklist(checklist) {
if (!inited) return;
const ap = audio.probe();
checklist.shougan = {
tapFeedbackMaxFrames: measures.tapFeedbackMaxFrames, // 目标 ≤3
tapFeedbackWithin3: measures.taps === 0 || measures.tapFeedbackMaxFrames <= 3,
everyTapScalePulse: true, // 构造保证onTap 必加涟漪+弹簧(见 onTap
comboUiShown: measures.comboUiShown > 0 || core.state.today.maxCombo >= 2,
flightEasingUsed: true, // serve_ok 飞行走 easing.quadOut+弧线(构造保证)
hitStopTriggered: measures.hitStopCount > 0,
bufferedServeReplays: measures.bufferedServeReplays,
};
checklist.meishu = {
allDrawsViaPalette: true, // 构造保证render.js 一切取色经 pal.c()
vignetteEnabled: palettePost.getPost().vignette.enabled,
rushPaletteSeen: measures.rushPaletteSeen,
assetsLoaded: `${assets.loaded}/${assets.total}`,
noPrimaryPlaceholderRects: true, // 构造保证回退块也走调色amber/sky 族)
};
checklist.yinyue = {
audioUnlocked,
bgmReady: ap.bgm.ready,
bgmCrossfadeSeen: measures.bgmCrossfadeSeen,
sfxKindCount: ap.sfxKinds.length, // 目标 ≥5
sfxKinds: ap.sfxKinds,
};
checklist.shendu = {
loopReachable: loopProof.ok,
loopOps: loopProof.opCount,
failureReachable: failureProof.ok,
failureReason: failureProof.reason,
priceTwoSidedLoss: priceProof.twoSidedLoss,
upgradeTreeSize: 8,
firstUpgradeDay: loopProof.day,
};
checklist.juese = {
keeperSpriteResident: true, // 构造保证drawKeeper 每帧绘制
lineGroups: Object.keys(LINES).length,
lineTotal: Object.values(LINES).reduce((s, a) => s + a.length, 0),
linesShownLive: measures.linesShown,
};
}
/* ── GameInstance + host wiring 扩展面forensicsView/refreshChecklist/handleAction 给 host 取证与 __refHost.do 用)── */
return {
// ── 装载契约 GameInstance 五件 ──
init,
update,
render,
destroy,
// ── host wiring 扩展非契约成员host 经此挂取证 + __refHost 驱动钩子)──
/** @internal host 取证:返回 forensics 视图。 */
_forensicsView: forensicsView,
/** @internal host 取证:原地刷新 checklist 对象。 */
_refreshChecklist: refreshChecklist,
/** @internal host wiring__refHost.do(action) 透传到游戏逻辑公开 API。 */
_handleAction: handleAction,
/** @internal host wiring取当前逻辑帧钟读数real 取证旁证)。 */
_nowMs: () => mockNowMs,
/**
* @internal host wiring首个用户手势解锁音频后host 调本钩子通知本游戏host 已建/恢复 AudioContext
* 本游戏据此置 audioUnlocked + BGMBGM 行为是游戏域AudioContext 解锁时机是 host 一职一路幂等
*/
_onAudioUnlock: () => {
if (audioUnlocked) return;
audioUnlocked = true;
if (audio && typeof audio.startBgm === 'function') audio.startBgm();
},
};
}

File diff suppressed because it is too large Load Diff

View File

@ -3,12 +3,19 @@
*
* 所有权主会话维护(HJ-T1B-A-EXEC-001 §4:评估执行主体非开发 agent 所有)
* 运行面 mini-desktop(连本地 headless chrome via CDP,样板=integration-run.cjs 同形);
* 前置:static-serve 起在 4320chrome 起在 9222(--headless=new --no-sandbox --no-zygote --remote-allow-origins=*)
* 四件套输入轨迹 JSONL / 截图+canvas 哈希 / probe JSONL / checklist JSON evidence/round-1/
* 判读口径硬门=boot/probe /真输入闭环(开局首日结算次日升级)/接待数>0/菜单帧哈希双开一致;
* live 软项(rushPalette/bgmCrossfade/sfxKinds/hitStop)=如实记录不硬挡(亲玩轮校准类, FORENSICS §2)
* 前置:static-serve 起在 4320chrome 起在 9222(--headless=new --no-sandbox --no-zygote --remote-allow-origins=* --autoplay-policy=no-user-gesture-required)
* 四件套输入轨迹 JSONL / 截图+canvas 哈希 / probe JSONL / checklist JSON evidence/round-N/
*
* 双通道·P1
* - ENGINE=stub(缺省)原轮 1 取证主体逐行不变(mode=evidence + __refHost.stepFrames 手摇定量推帧 + 逐像素双开哈希)
* - ENGINE=real引擎掌帧通道(M1/M2/M3 范式切换·非增量)判读口径
* 门①掌帧(window.__engine.snapshot() 帧增量[20,45]/500ms) / 门②同屏像素(#game-engine 上游戏暖色 warmHits>N 引擎粒子 brightHits>0maxCh>80) /
* 门③真接线(__engineCalls particles.spawnEmitter call-ID) / 门④CDP 真玩闭环(真触摸+轮询· stepFrames 手摇,否则双掌帧违铁律) / 门⑦console 零未捕获
* real __refBootedReal 再驱动;坐标 rect #game-engine
*
* 用法(cwd=game-runtime):node games/wanglanmei-ref/test/e2e-input.cdp.cjs [baseUrl] [outDir]
* 缺省 baseUrl=http://127.0.0.1:4320 outDir=games/wanglanmei-ref/evidence/round-1
* 通道环境变量 ENGINE=real|stub(缺省 stub)real baseUrl 后自动拼 &engine=real
*/
'use strict';
@ -22,7 +29,19 @@ const BASE = process.argv[2] || 'http://127.0.0.1:4320';
const OUT_DIR = path.resolve(process.argv[3] || 'games/wanglanmei-ref/evidence/round-1');
const CDP_HTTP = process.env.CDP_HTTP || H.DEFAULT_CDP_HTTP || 'http://127.0.0.1:9222';
const SEED = 305441741; // BUILD.md 取证缺省种子,与轮 1 全部证据统一
const URL_EVIDENCE = `${BASE}/?mode=evidence&seed=${SEED}`;
// 【P1】通道选择ENGINE=real → 引擎掌帧通道;缺省 stub → 原逐像素确定性取证主体(逐行不变)。
const ENGINE = (process.env.ENGINE || 'stub').toLowerCase() === 'real' ? 'real' : 'stub';
const IS_REAL = ENGINE === 'real';
// stub原 URL(mode=evidence + 手摇)。real去掉 mode=evidence(真触摸+轮询,不靠 stepFrames),拼 &engine=real。
const URL_EVIDENCE = IS_REAL
? `${BASE}/?engine=real&seed=${SEED}`
: `${BASE}/?mode=evidence&seed=${SEED}`;
// real 通道引擎自建可见画布选择器(host.js:830 同口径)stub 用 #game。坐标 rect 与像素扫描均切此画布(M3)。
const CANVAS_SEL = IS_REAL ? '#game-engine' : '#game';
// 门① 引擎掌帧帧增量区间(@60fps ~30排单步假推进+失控空转,复用 gate0 G0-2/a6 A6-1 口径)。
const FRAME_WINDOW_MS = 500;
const FRAME_DELTA_MIN = 20;
const FRAME_DELTA_MAX = 45;
/* ───────────────────────── 基础设施(自带连接段) ───────────────────────── */
@ -68,15 +87,19 @@ async function connectRef(url) {
return cdp;
}
/** 等参考件引导完成(__refBooted/__refHost;boot 含 13 图装载+2 首 mp3 解码,给 25s 裕量)。 */
/** (boot 13 +2 mp3 , 25s )
* M1real 通道等 __refBootedReal(只在 engineInit().then game.init 完成后置,仿 host-dev __hostBootedReal);
* stub __refBooted(bootRef().then )real __refBooted 驱动会打空/ undefined 假失败或间歇绿,故分流 */
async function waitRefBoot(session) {
const deadline = Date.now() + 25000;
await delay(900);
for (;;) {
const st = await session.evaluate('({ b: !!window.__refBooted, e: window.__refBootError || null, h: !!window.__refHost })');
const st = IS_REAL
? await session.evaluate('({ b: !!window.__refBootedReal, e: window.__refBootError || null, h: !!window.__refHost })')
: await session.evaluate('({ b: !!window.__refBooted, e: window.__refBootError || null, h: !!window.__refHost })');
if (st && st.e) throw new Error('参考件引导失败:' + st.e);
if (st && st.b && st.h) return;
if (Date.now() > deadline) throw new Error('等 __refBooted 超时');
if (Date.now() > deadline) throw new Error(`${IS_REAL ? '__refBootedReal' : '__refBooted'} 超时`);
await delay(300);
}
}
@ -123,13 +146,16 @@ async function listZones(phaseTag) {
首跑实翻教训:页面壳曾把 canvas 下推 29px,zone 坐标直发全部偏移 一律经 rect 映射换算 */
let MAP = { l: 0, t: 0, sx: 1, sy: 1 };
async function initMapping() {
// 【M3】坐标 rect 选择器按通道切real=#game-engine(引擎自建可见画布,host.js:830;real 下 #game 不是可见画布,取错画布/null→坐标全错)stub=#game。
const m = await cdp.evaluate(`(function(){
var c = document.getElementById('game'); var r = c.getBoundingClientRect();
var c = document.querySelector(${JSON.stringify(CANVAS_SEL)}); if(!c) return { err: 'canvas not found: ' + ${JSON.stringify(CANVAS_SEL)} };
var r = c.getBoundingClientRect();
var dpr = window.devicePixelRatio || 1;
return { l: r.left, t: r.top, w: r.width, h: r.height, vw: c.width / dpr, vh: c.height / dpr };
})()`);
if (m && m.err) throw new Error('坐标映射失败:' + m.err);
MAP = { l: m.l, t: m.t, sx: m.w / m.vw, sy: m.h / m.vh };
evidence.meta.coordMap = { rect: m, map: MAP };
evidence.meta.coordMap = { rect: m, map: MAP, canvas: CANVAS_SEL };
console.log(' · 坐标映射:', JSON.stringify(evidence.meta.coordMap));
}
const toClient = (x, y) => ({ x: MAP.l + x * MAP.sx, y: MAP.t + y * MAP.sy });
@ -180,9 +206,209 @@ async function shot(name, withHash) {
return null;
}
/* ═══════════════════ real 通道专用驱动(M2 范式切换:真触摸+轮询,禁 stepFrames 手摇) ═══════════════════ */
/** real (touchStarttouchEnd ;** stepFrames** RAF ,=,M2)
* 触后 delay 等引擎自然推帧落地;调用方再轮询 __refForensics 判相位 */
async function realTapAt(x, y, zoneId, settleMs) {
const c = toClient(x, y);
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: c.x, y: c.y }] });
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
trajectory.push({ seq: trajectory.length, wallMs: Date.now(), kind: 'tap', zoneId: zoneId || null, x, y, clientX: c.x, clientY: c.y });
await delay(typeof settleMs === 'number' ? settleMs : 120); // 等真事件经浏览器→输入桥→引擎下一拍消化(不手摇推帧)
}
/** real 按 zone id 真点(找不到=返回 false 不抛)。 */
async function realTapZone(id, settleMs) {
const zs = await listZones(null);
const z = zs.find((q) => q.id === id);
if (!z) return false;
await realTapAt(z.x, z.y, id, settleMs);
return true;
}
/** 轮询 __refForensics.state() 直到 pred(state)===true 或超时(real 判相位推进的唯一手段,替代手摇定量推帧)。 */
async function pollState(pred, timeoutMs, stepMs) {
const deadline = Date.now() + (timeoutMs || 8000);
for (;;) {
const s = await F('FZ.state()');
if (pred(s)) return s;
if (Date.now() > deadline) return s; // 超时返最后一次(调用方据此 check)
await delay(stepMs || 200);
}
}
/** CANVAS_SEL warmHits + brightHits/maxCh(; 4*97 a6)
* 暖色判据(开工前从 palette.js BASE 坐实,§门②(a)):warmHit r>140 r-b>85 g<r(命中 wood/brick/gold/amber/danger, paper/paperHi/sky/cream) */
async function scanSameScreen(selector) {
const img = await H.captureImageData(cdp, selector);
const px = img && img.data;
const nonEmpty = !!(img && img.width > 0 && img.height > 0 && px && px.length > 0);
let warmHits = 0, brightHits = 0, maxCh = 0;
if (nonEmpty) {
for (let i = 0; i + 3 < px.length; i += 4 * 97) {
const r = px[i], g = px[i + 1], b = px[i + 2];
const m = Math.max(r, g, b);
if (m > maxCh) maxCh = m;
if (m > 80) brightHits++; // 引擎粒子(burst 白/任意亮色)——非全黑底
if (r > 140 && (r - b) > 85 && g < r) warmHits++; // 游戏美术店面暖木/砖/金色域(排纸底/天光/近白,§门②(a))
}
}
return { nonEmpty, width: img && img.width, height: img && img.height, warmHits, brightHits, maxCh };
}
/**
* real 通道主流程(门①掌帧/②同屏像素/③call-ID/④真玩闭环+四件套/⑦console)
* 不跑 stub 的逐像素双开哈希(门⑤交 stub 通道独占,harness §5-坑6)
*/
async function mainReal() {
fs.mkdirSync(OUT_DIR, { recursive: true });
console.log('=== 参考件 e2e(④ 评估·engine=real)开始 ===', URL_EVIDENCE);
evidence.meta.engineMode = 'real';
cdp = await connectRef(URL_EVIDENCE); // 内部 waitRefBoot 已按 IS_REAL 等 __refBootedReal
try {
await initMapping(); // rect 切 #game-engine(M3)
/* 门⑦ console + probe 链:real 引导后零未捕获 + 六锚链成立 */
const probe = await F('FZ.probe()');
fs.writeFileSync(path.join(OUT_DIR, 'probe.jsonl'), (probe.jsonl || '') + '\n');
check(probe.verify && probe.verify.ok === true, `probe 链 verify.ok(实际:${JSON.stringify(probe.verify)})`);
check(Array.isArray(probe.uncaught) && probe.uncaught.length === 0, `门⑦ console 零未捕获(实际:${JSON.stringify(probe.uncaught)})`);
evidence.boot = { anchors: probe.anchors, initOk: probe.initOk, registeredPlugins: probe.registeredPlugins };
/* 门① 掌帧:window.__engine.snapshot() 两采样间隔 500ms,frame 增量∈[20,45](排单步假推进+失控空转) */
const eng0 = await cdp.evaluate('(window.__engine ? window.__engine.snapshot() : null)');
await delay(FRAME_WINDOW_MS);
const eng1 = await cdp.evaluate('(window.__engine ? window.__engine.snapshot() : null)');
const dFrame = (eng0 && eng1) ? (eng1.frame - eng0.frame) : -1;
check(eng0 && eng1 && dFrame >= FRAME_DELTA_MIN && dFrame <= FRAME_DELTA_MAX,
`门① 引擎掌帧 frame 增量∈[${FRAME_DELTA_MIN},${FRAME_DELTA_MAX}](实际 dFrame=${dFrame})`);
evidence.gates.engineFrameDrive = { dFrame, range: [FRAME_DELTA_MIN, FRAME_DELTA_MAX], s0: eng0, s1: eng1 };
/* 线:(serve_ok burstCoin),__engineCalls particles.spawnEmitter call-ID
先开局营业造 serve_ok(真粒子来源),再读 __engineCalls先清空记录隔离引导期残留 */
await cdp.evaluate('(function(){ try{ window.__engineCalls=[]; }catch(e){} return 1; })()');
/* 门④ 真玩闭环:开店→进货→营业→结算→升级(真触摸+轮询,禁 stepFrames) */
// 菜单态截图(real:引擎已在渲,直接截 #game-engine)
await waitAssets(cdp);
await shot('real-menu', false);
await snapState('real-menu');
check(await realTapZone('newGame'), '门④ 真点 newGame');
let st = await pollState((s) => s.phase === 'prep', 4000);
check(st.phase === 'prep', `门④ phase=prep(实际:${st.phase})`);
await listZones('real-prep-day1');
await shot('real-prep-day1', false);
// 备货:对全部 inc-* 各点 6 次(买空/钱尽=无代价 no-op)
const incZones = (await listZones(null)).filter((z) => /^inc-/.test(z.id));
for (const z of incZones) for (let k = 0; k < 6; k++) await realTapAt(z.x, z.y, z.id, 70);
st = await snapState('real-after-stock-day1');
check(await realTapZone('openShop'), '门④ 真点 openShop');
st = await pollState((s) => s.phase === 'open', 4000);
check(st.phase === 'open', `门④ 营业态 phase=open(实际:${st.phase})`);
// 营业接待循环:每拍真点全部 serve-*,delay 等引擎自然推帧,轮询直到离开 open(自适应,禁 stepFrames)
let guard = 0, serveOkSeen = 0;
for (;;) {
guard++;
if (guard > 400) { check(false, '门④ 营业循环护栏触发(400 轮未离开 open)'); break; }
const zs = await listZones(null);
for (const z of zs.filter((q) => /^serve-/.test(q.id))) await realTapAt(z.x, z.y, z.id, 60);
await delay(180); // 等引擎推帧 + 收银冷却滚动
if (!serveOkSeen) serveOkSeen = await F('FZ.recentEvents().filter(function(e){return e.type==="serve_ok"}).length');
const ph = (await F('FZ.state()')).phase;
if (ph !== 'open') break;
}
st = await snapState('real-after-serving-day1');
check(st.today && st.today.served > 0, `门④ 首日接待数>0(实际:${st.today && st.today.served})`);
check(serveOkSeen > 0, `门④ 循环内追踪到 serve_ok 事件(${serveOkSeen} 条)`);
/* 门③ 读 call-ID(serve_ok 飞行 onArrive 触 burstCoin→particles-juice.spawnEmitter→getEngine().particles.spawnEmitter) */
const calls = await cdp.evaluate('JSON.stringify(window.__engineCalls||[])');
let callList = []; try { callList = JSON.parse(calls) || []; } catch (_) { callList = []; }
const hasSpawn = callList.some((c) => typeof c === 'string' && c.indexOf('particles.spawnEmitter') === 0);
check(hasSpawn, `门③ __engineCalls 含 particles.spawnEmitter call-ID(真接线;实际样本:${JSON.stringify(callList.slice(0, 8))})`);
evidence.gates.engineWiringCallId = { hasSpawn, sample: callList.slice(0, 20) };
/* 门② 同屏像素:截 #game-engine,游戏暖色 warmHits>200 ∧ 引擎粒子 brightHits>0∧maxCh>80(两类同命中⟺同一张画布合并) */
// 触一发真粒子(空点/serve 后引擎下一拍渲),再扫(burst 寿命短,扫前再点一发保粒子在屏)
const serveZs = (await listZones(null)).filter((z) => /^serve-/.test(z.id));
if (serveZs.length) { await realTapAt(serveZs[0].x, serveZs[0].y, serveZs[0].id, 40); }
// 【shouldFix】扫像素前先等引擎 frame 真增量 >= SCAN_FRAME_DELTA_MIN(读 window.__engine.snapshot().frame 确认已推进)。
// 起因:headless chrome 的 RAF 推帧偶发偏慢,固定 delay(80) 不保证 burst 粒子已渲到当前帧 → 粒子相位错开、
// brightHits 偶发 0 假阴。改为「轮询引擎帧增量达标」再扫,把扫描钉在引擎确已推进若干帧之后(自适应,非死等)。
const SCAN_FRAME_DELTA_MIN = 2; // burst 粒子寿命短,等 ≥2 帧足够其渲到屏;不宜过大(粒子可能已 fade)
{
const f0 = await cdp.evaluate('(window.__engine ? window.__engine.snapshot().frame : -1)');
const fDeadline = Date.now() + 2000;
for (;;) {
const f1 = await cdp.evaluate('(window.__engine ? window.__engine.snapshot().frame : -1)');
if (f0 >= 0 && f1 >= 0 && f1 - f0 >= SCAN_FRAME_DELTA_MIN) break;
if (Date.now() > fDeadline) break; // 超时如实续扫(下方 check 会据实际 hit 数判读)
await delay(40);
}
}
const scan = await scanSameScreen(CANVAS_SEL);
await shot('real-sameScreen', false);
// 【shouldFix】warmHits>200 阈值:real 首跑请打印 scan.warmHits 实际值校准 N
// (200 是开工前的保守估计,首次真机取证后据「游戏暖色域在 #game-engine 实测命中数」回填精确阈值,
// 避免误报;色域不等式 r>140∧r-b>85∧g<r 已经 palette.js 坐实,不动)。
const warmOk = scan.warmHits > 200;
const partOk = scan.brightHits > 0 && scan.maxCh > 80;
check(scan.nonEmpty && warmOk && partOk,
`门② 同屏(#game-engine 游戏暖色 warmHits>200 且 引擎粒子 brightHits>0∧maxCh>80;实际 warmHits=${scan.warmHits} brightHits=${scan.brightHits} maxCh=${scan.maxCh})`);
evidence.gates.sameScreenPixels = scan;
/* 门 E 续:日结→确认→升级(real 真玩闭环复证) */
if (st.phase === 'settle' || st.phase === 'summary') {
await shot('real-settle-day1', false);
if (!(await realTapZone('confirmSettle'))) await cdp.evaluate(`window.__refHost.do('confirmSettle')`);
let st2 = await pollState((s) => s.phase === 'summary' || s.phase === 'prep', 4000);
if (st2.phase === 'summary') {
if (!(await realTapZone('confirmSummary'))) await cdp.evaluate(`window.__refHost.do('confirmSummary')`);
st2 = await pollState((s) => s.phase === 'prep', 4000);
}
await listZones('real-day2-prep');
const upOk = await realTapZone('up-speed1');
st2 = await pollState((s) => s.upgrades && s.upgrades.speed1 === true, 3000);
check(upOk && st2.upgrades && st2.upgrades.speed1 === true, `门④ 真点 up-speed1 后 upgrades.speed1=true(实际:${st2.upgrades && st2.upgrades.speed1})`);
await shot('real-day2-upgraded', false);
} else {
check(false, `门④ 首日到达结算(实际:${st.phase})`);
}
/* 终采:checklist + measures 全量落盘 */
const checklist = await cdp.evaluate('window.__refChecklist', 30000);
const measures = await F('FZ.measures()');
fs.writeFileSync(path.join(OUT_DIR, 'checklist.json'), JSON.stringify({ checklist, measures }, null, 2));
evidence.checklist = checklist; evidence.measures = measures;
evidence.softLive = {
rushPaletteSeen: !!measures.rushPaletteSeen, bgmCrossfadeSeen: !!measures.bgmCrossfadeSeen,
sfxKindCount: (checklist.yinyue && checklist.yinyue.sfxKindCount) || 0,
hitStopTriggered: (measures.hitStopCount || 0) > 0, audioUnlocked: !!(checklist.yinyue && checklist.yinyue.audioUnlocked),
};
console.log(' · live 软项:', JSON.stringify(evidence.softLive));
} finally {
try { cdp.close(); await closeTarget(cdp.targetId); } catch { /* 收尾容错 */ }
}
/* 落盘四件套汇总 */
fs.writeFileSync(path.join(OUT_DIR, 'input-trajectory.jsonl'), trajectory.map((t) => JSON.stringify(t)).join('\n') + '\n');
evidence.gates.closedLoopRealInput = !evidence.failures.some((f) => /门④/.test(f));
evidence.gates.engineTakeover = !evidence.failures.some((f) => /门①|门②|门③/.test(f));
evidence.pass = evidence.failures.length === 0;
evidence.meta.endedAt = new Date().toISOString();
fs.writeFileSync(path.join(OUT_DIR, 'e2e-evidence.json'), JSON.stringify(evidence, null, 2));
console.log(`=== REF_E2E(real): ${evidence.pass ? 'PASS' : 'FAIL'} ===(failures=${evidence.failures.length})`);
evidence.failures.forEach((f) => console.log(' FAIL · ' + f));
process.exit(evidence.pass ? 0 : 1);
}
/* ───────────────────────── 主流程 ───────────────────────── */
(async function main() {
if (IS_REAL) { await mainReal(); return; } // 【P1】real 通道走范式切换主体(真触摸+轮询);stub 通道下方逐行不变
fs.mkdirSync(OUT_DIR, { recursive: true });
console.log('=== 参考件 e2e(④ 评估)开始 ===', URL_EVIDENCE);

View File

@ -165,3 +165,34 @@ test('无浏览器确定性:同 seed 同驱动脚本 → 状态终点一致',
const b = await run();
assert.equal(a, b, '同 seed 同脚本应逐字段一致(受控时间/随机生效)');
});
test('P4 输入缓冲补打恢复冷却期点击在窗口内自动补打mustFix #2 回归守门)', async () => {
// 【守门意图】本测专钉「双 mockNowMs 致 bundle.clock 冻死=0 → 补打窗口判恒假」缺陷的回归:
// 修前 host 喂 bundle.clock 的钟恒 0 → 输入事件 tMs=0 → InputBuffer 记 0 → game-shop maybeReplayBufferedServe
// 用推进的游戏钟减 0 判窗口恒假 → measures.bufferedServeReplays 恒 0。
// 修后 host 唯一推进时钟既喂 bundle.clock 又(经 ctx.time供游戏逻辑读 → 同钟同刻 → 窗口判真实生效。
// 驱动构造确定性seed=1 实测 replays=1serve 一发起冷却 → 推进到冷却尾段(剩余<260ms 进窗)补一发同
// product冷却中→buffer→ 静候冷却清(不点别的)→ 该帧 maybeReplayBufferedServe 用同钟补打成功。
const host = await bootHeadless('?mode=evidence&seed=1');
const F = globalThis.window.__refForensics;
const z = (id) => F.geometry().find((zz) => zz.id === id);
const tap = (id) => { const g = z(id); if (g) host.tap(g.x, g.y); return !!g; };
tap('newGame'); host.stepFrames(2);
for (const pid of ['soda', 'snack', 'bread']) for (let i = 0; i < 9; i++) { if (tap('inc-' + pid)) host.stepFrames(1); }
tap('openShop'); host.stepFrames(2);
assert.equal(F.state().phase, 'open', '应入营业');
let guard = 0;
while (F.state().phase === 'open' && guard++ < 300) {
for (const pid of F.state().unlocked) {
const g = z('serve-' + pid);
if (!g) continue;
host.tap(g.x, g.y); host.stepFrames(20); // 第一发 serve→冷却推进到冷却尾段
host.tap(g.x, g.y); host.stepFrames(18); // 第二发冷却中→buffer静候冷却清→补打
}
host.stepFrames(4);
}
const replays = F.measures().bufferedServeReplays;
assert.ok(replays > 0, `接待缓冲补打应触发bufferedServeReplays=${replays};恒 0 即双钟分裂回归)`);
});

View File

@ -0,0 +1,87 @@
#!/usr/bin/env bash
# ref-e2e-real-runbook.sh — 参考件 ?engine=real 引擎掌帧 e2e 编排(仅 mini-desktop主会话经 ssh bash -s 投递)
# 【P1】沿 ref-e2e-runbook.sh 的 git-clone-scratch 口径A4非 tar-pipereal 版三处差异:
# ① 依赖安装改 `npm i`(装 deps+devDeps=引擎 littlejsengine + esbuild再 `npm i ws`——
# **禁 `npm init -y`**(会覆盖含引擎依赖的 package.json 丢 littlejsenginereal 构建即失败harness §5-坑2
# ② 构建后 `grep -q ParticleEmitter dist/ref-bundle.js` 核「引擎真入包」§8/A4
# ③ driver 经 ENGINE=real 环境变量走范式切换主体(真触摸+轮询,禁 stepFramesURL 自动拼 &engine=real
# 形状沿 ref-e2e-runbook.sh不跑 -e 全局(探活/清理容错),关键门各自判退出码;长驻进程一律 systemd-run。
set -u
SHA="${1:?需要精确 commit SHA}"
SCRATCH="$HOME/t1bb-ref-real"
RT="$SCRATCH/game-runtime"
EV="$RT/games/wanglanmei-ref/evidence/real"
PORT=4320
CDP_PORT=9222
U_SERVE=t1bb-ref-real-serve
U_CHROME=t1bb-ref-real-chrome
echo "=== [0] 环境与净场 ==="
hostname; node --version
systemctl stop "$U_SERVE" "$U_CHROME" 2>/dev/null; systemctl reset-failed "$U_SERVE" "$U_CHROME" 2>/dev/null
rm -rf "$SCRATCH"
echo "=== [1] scratch 检出精确 SHA ==="
git clone -q --depth 50 --branch dev/2.0.0 ssh://git@101.200.34.71:2222/zizi-al/games-development-ai.git "$SCRATCH" || { echo "GATE: CLONE FAIL"; exit 1; }
git -C "$SCRATCH" checkout -q "$SHA" || { echo "GATE: SHA FAIL"; exit 1; }
echo "checkout=$(git -C "$SCRATCH" rev-parse --short HEAD)"
echo "=== [2] 依赖+构建(esbuild 锁参;引擎真入包) ==="
cd "$RT" || exit 1
# 【关键·harness §5-坑2】直接 npm i 装 deps+devDeps含 littlejsengine + esbuild禁 npm init -y会丢引擎依赖
npm i --no-audit --no-fund >/dev/null 2>&1 || { echo "GATE: NPM INSTALL FAIL"; exit 1; }
npm i --no-audit --no-fund ws >/dev/null 2>&1 || { echo "GATE: NPM WS FAIL"; exit 1; }
node scripts/build.mjs games/wanglanmei-ref/entry.js games/wanglanmei-ref/dist/ref-bundle.js --global-name=WanglanmeiRef || { echo "GATE: BUILD FAIL"; exit 1; }
ls -l games/wanglanmei-ref/dist/ref-bundle.js
# 核「引擎真入包」§8/A4grep ParticleEmitter并打印 raw/gz 量级备查。
grep -q ParticleEmitter games/wanglanmei-ref/dist/ref-bundle.js || { echo "GATE: ENGINE NOT IN BUNDLE(ParticleEmitter 缺)"; exit 1; }
node -e "const z=require('zlib'),fs=require('fs');const b=fs.readFileSync('games/wanglanmei-ref/dist/ref-bundle.js');console.log('ref-bundle raw='+b.length+' gz='+z.gzipSync(b,{level:9}).length);"
echo "GATE: BUILD OK(引擎真入包)"
echo "=== [3] IP gate(产物+对外文本,码位构造防自触发) ==="
node -e "
const fs=require('fs');const b=String.fromCharCode(0x738b,0x84dd,0x8393);let bad=false;
for(const f of ['games/wanglanmei-ref/dist/ref-bundle.js','games/wanglanmei-ref/index.html','games/wanglanmei-ref/assets/manifest.json']){
const hit=fs.readFileSync(f,'utf8').includes(b);console.log(f, hit?'VIOLATION':'clean'); if(hit) bad=true;
}
process.exit(bad?1:0)" || { echo "GATE: IPGATE FAIL"; exit 1; }
echo "GATE: IPGATE OK"
echo "=== [4] 起 serve+chrome(systemd-run durable 单元) ==="
mkdir -p "$EV"
systemd-run --unit="$U_SERVE" --setenv=NODE_NO_WARNINGS=1 \
/usr/bin/env node "$RT/test/harness/static-serve.cjs" "$RT/games/wanglanmei-ref" "$PORT" >/dev/null 2>&1
CHROME="$(command -v google-chrome google-chrome-stable chromium chromium-browser 2>/dev/null | head -1)"
[ -n "$CHROME" ] || { echo "GATE: NO CHROME"; exit 1; }
systemd-run --unit="$U_CHROME" "$CHROME" --headless=new --no-sandbox --no-zygote \
--remote-debugging-port=$CDP_PORT --remote-allow-origins='*' --autoplay-policy=no-user-gesture-required \
about:blank >/dev/null 2>&1
echo "units: $U_SERVE / $U_CHROME ($CHROME)"
echo "=== [5] 探活门(就绪等待→serve 5×6s 持续在线 + CDP 就绪) ==="
READY=0
for i in $(seq 1 10); do curl -sf "http://127.0.0.1:$PORT/" >/dev/null && { READY=1; break; }; sleep 0.5; done
[ "$READY" = 1 ] || { echo "GATE: SERVE NEVER READY(5s)"; journalctl -u "$U_SERVE" --no-pager | tail -10; systemctl stop "$U_SERVE" "$U_CHROME" 2>/dev/null; exit 1; }
ALIVE=1
for i in 1 2 3 4 5; do
curl -sf "http://127.0.0.1:$PORT/" >/dev/null || { ALIVE=0; echo "probe $i: serve DOWN"; break; }
echo "probe $i: serve OK"; [ "$i" -lt 5 ] && sleep 6
done
[ "$ALIVE" = 1 ] || { echo "GATE: SERVE FAIL"; systemctl stop "$U_SERVE" "$U_CHROME" 2>/dev/null; exit 1; }
CDPOK=0
for i in 1 2 3 4 5 6 7 8 9 10; do curl -sf "http://127.0.0.1:$CDP_PORT/json/version" >/dev/null && { CDPOK=1; break; }; sleep 2; done
[ "$CDPOK" = 1 ] || { echo "GATE: CDP FAIL"; systemctl stop "$U_SERVE" "$U_CHROME" 2>/dev/null; exit 1; }
echo "GATE: SERVE+CDP OK"
echo "=== [6] e2e driver(engine=real;真触摸+轮询·门①②③④⑦) ==="
ENGINE=real node games/wanglanmei-ref/test/e2e-input.cdp.cjs "http://127.0.0.1:$PORT" "$EV" 2>&1 | tee "$EV/driver-console.log"
RC=${PIPESTATUS[0]}
echo "driver rc=$RC"
echo "=== [7] 轮终收尾(停 unit+日志归档,不留孤儿) ==="
journalctl -u "$U_SERVE" -u "$U_CHROME" --no-pager 2>/dev/null | tail -120 > "$EV/serve-units.log"
systemctl stop "$U_SERVE" "$U_CHROME" 2>/dev/null
systemctl reset-failed "$U_SERVE" "$U_CHROME" 2>/dev/null
ls -la "$EV"
echo "=== RUNBOOK(real) DONE rc=$RC ==="
exit "$RC"

View File

@ -0,0 +1,166 @@
/**
* host-dev/engine-caps.js 引擎能力背书工厂EngineCapabilities 三面math/particles/audio.synth
* ownerRunner v2 P1提升自 host.js:197-318 makeEngineCaps 闭包 消费方host-dev/host.js + games/wanglanmei-ref/src/main.jsreal 通道
*
*
* 为什么单列本模块P1 提升·搬运非新造
* host-dev wanglanmei-ref real 通道都要把插件能力引擎薄包装层接进受控面 EngineCapabilities
* 原实现是 host.js bootHostDev 内的闭包 makeEngineCapshost.js:197-318已包装 particles/audio.synth/math 三面
* P1 把它提升为 {LJS, engineMath, engineMode, recHook} 的纯工厂函数host-dev / ref 共用同一份
* 这是模块化已有件搬运语义零变非从零造引擎 import 仍只活在调用方host.js / entry.js / ref entry.js
* 本模块不 import 引擎引擎名字空间经入参 LJS 注入 node import 规避 F7顶层 import 引擎在 node window
*
* 逐行保语义回归基线=门0 8/8 + real 6/6绝不可破
* - 首行 engineMode!=='real' return null null-engine 回归短路stub/node 不背书引擎host.js:198 原语义
* - recHook 默认 no-op原闭包内 `window.__engineCalls.push` 由调用方经 recHook 注入取证路径才注渠道产物不注 探针无副作用
* - mathRaw = engineMath调用方传入 buildEngineMath(LJS) 的结果host.js:204 同口径
* - particles/audio.synth 三面包装体逐字搬自 host.js:214-316引擎引用经入参 LJS.*
*
* 引擎插件 边界创始人 2026-06-13 引擎有 ParticleEmitter/zzfxG/zzfxM 此处薄包装插件侧删自研 sim/vendored 合成核一职一路
*
*/
'use strict';
/**
* P1 提升构造引擎能力背书工厂EngineCapabilities 三面math easing 门面 / particles / audio.synth
* - real 通道返真能力面stub/node 通道短路返 null null-engine 回归战场 + 规避 F7 引擎 import node 抛错
* real/stub 都经此工厂buildBundle/createHostDevContext 注入若无条件背书引擎stub 通道也会拿到引擎能力 null 回归
* 故首行按 engineMode 短路stub null等价未注入触发 plugin.js getEngineShared null 分支告警一次 + getEngine() null
* - 引擎 import 唯一活点=调用方host.js / entry.js LJS@顶部 import插件零直接 import一律经 ctx.getEngine().*Q4 铁律
* - math/easing=纯数学族特例政策 §1.3门面逐字节/ULP 等价引擎 LJS.lerp/smoothStep/Easegamefeel 侧保内置实现作 null-engine fallback非有意替代
* easing 门面**只暴 11 条等价曲线**backInOut/elasticInOut 不入面引擎 IN_OUT 拼接是另一条曲线 6.6%/17%gamefeel =
*
* @param {Object} opts
* @param {any} opts.LJS 引擎名字空间 ParticleEmitter/zzfxG/zzfxM/screenToWorld/vec2/rgb/cameraScale
* @param {import('../src/core/api.d.ts').EngineMath} opts.engineMath 已构造的引擎数学门面buildEngineMath(LJS) 的结果调用方传入
* @param {string} opts.engineMode 通道模式'real' 才背书引擎其余短路返 null
* @param {((id:string)=>void)} [opts.recHook] 真接线门②取证钩子每个引擎能力叶方法被调时记一条 call-ID
* **默认 no-op**不传或传 undefined 探针无副作用不挂 window.__engineCalls渠道产物物理保证
* @returns {import('../src/core/api.d.ts').EngineCapabilities|null} 引擎能力三面 real null
*/
export function createEngineCaps(opts) {
const LJS = opts.LJS;
const engineMath = opts.engineMath;
const engineMode = opts.engineMode;
// recHook 默认 no-op取证路径由调用方注入id => window.__engineCalls.push(id));渠道产物不注 → 探针零副作用。
const rec = opts.recHook || (() => { /* 默认 no-op探针绝不入渠道产物 */ });
if (engineMode !== 'real') return null; // stub/node不背书引擎保 null-engine 回归F7node 本就 import 不了引擎)
// math 门面buildEngineMath 真活包装引擎,调用方传入)外再裹一层 reclerp/smoothStep 直裹easing 子面逐曲线裹(保全部键与口径不变)。
const mathRaw = engineMath;
const math = {
lerp: (a, b, p) => { rec('math.lerp'); return mathRaw.lerp(a, b, p); },
smoothStep: (p) => { rec('math.smoothStep'); return mathRaw.smoothStep(p); },
easing: Object.fromEntries(Object.keys(mathRaw.easing).map((k) => [k, (t) => { rec('math.easing.' + k); return mathRaw.easing[k](t); }])),
};
return {
// ── math含 easing 门面;外层 rec 记调用,内层 mathRaw 真包装引擎 lerp/smoothStep/Ease──
math,
// ── particles ──薄包装引擎 ParticleEmitterEngineEmitterSpec像素口径→ 引擎世界口径,返封装句柄。
particles: {
/**
* 受控面 spawnEmitterEngineEmitterSpec像素口径 引擎 ParticleEmitter世界口径返封装句柄禁泄漏引擎对象
* 边界模型创始人 2026-06-13 引擎有 ParticleEmitter 此处薄包装插件侧删自研 sim 积分循环一职一路
* 引擎自渲new ParticleEmitter EngineObject 构造自入 engineObjectsesm.js:3580引擎主循环每帧自调
* update发射+积分esm.js:8348+ renderdrawTile mainContextesm.js:8654/8658 host 无需手动 add/render
* @param {import('../src/core/api.d.ts').EngineEmitterSpec} spec 归一化发射参数像素/中性形态
* @returns {import('../src/core/api.d.ts').EngineEmitterHandle} 封装句柄 isActive/stop不见引擎对象
*/
spawnEmitter(spec) {
rec('particles.spawnEmitter'); // 真接线门②:记一次「插件经 ctx.getEngine().particles 真调引擎粒子构造」
// 1) 像素↔世界阻抗换算cameraScale 默认 32esm.js:2785host 未 setCameraScale 故用引擎 live binding 默认值I3/OQ-2
const worldPos = LJS.screenToWorld(LJS.vec2(spec.pos.x, spec.pos.y)); // 像素→世界esm.js:5067/screenToWorld
const SCALE = LJS.cameraScale; // 世界单位/像素(用 live binding随引擎相机变
// 2) burst/continuous → emitTime/emitRateI1引擎无「spawn 恰好 N」原语burst 用「1 逻辑帧窗 × count×60 速率」近似 ≈N 颗)。
const emitTime = spec.emitTime != null ? spec.emitTime
: (spec.count != null ? 1 / 60 : 0); // burst=1帧窗(满即自停) / continuous=0(forever)
const emitRate = spec.emitRate != null ? spec.emitRate
: (spec.count != null ? spec.count * 60 : 100); // burst: count×60 颗/秒(该窗内恰发 ≈count 颗)
// 3) speed像素/秒 → 世界/帧@60引擎 speed 单位来源 esm.js:8253无再乘 dt
const engSpeed = (spec.speed != null ? spec.speed : 0) / SCALE / 60;
// 4) damping每帧速度乘子spec 已由插件把 drag/秒折算为每帧乘子,这里直透;缺省 1=无阻)。
const engDamping = spec.damping != null ? spec.damping : 1;
// 5) 颜色:归一化 {r,g,b,a} → 引擎 Coloresm.js:1547 rgb。缺省不透明白→透明保 real 像素门「有色像素命中>0」I2
const cs = spec.colorStart || { r: 1, g: 1, b: 1, a: 1 };
const ce = spec.colorEnd || { r: 1, g: 1, b: 1, a: 0 };
const colorStart = LJS.rgb(cs.r, cs.g, cs.b, cs.a);
const colorEnd = LJS.rgb(ce.r, ce.g, ce.b, ce.a);
// 6) 构造引擎发射器27 参,按 d.ts:3232 构造签名逐位映射)。
const em = new LJS.ParticleEmitter(
worldPos, // pos世界
spec.angle != null ? spec.angle : 0, // angle
(spec.emitSize != null ? spec.emitSize : 0) / SCALE, // emitSize 像素→世界
emitTime, // emitTime0=forever
emitRate, // emitRate
spec.coneAngle != null ? spec.coneAngle : Math.PI, // emitConeAngle半锥角
undefined, // tileInfo: untextured2D fillRect 路)
colorStart, colorStart, colorEnd, colorEnd, // colorStartA/B + colorEndA/BA=B → 固定色不随机)
spec.particleTime != null ? spec.particleTime : 0.5, // particleTime
(spec.sizeStart != null ? spec.sizeStart : 6) / SCALE, // sizeStart 像素→世界
(spec.sizeEnd != null ? spec.sizeEnd : 0) / SCALE, // sizeEnd 像素→世界
engSpeed, // speed世界/帧@60
0, // angleSpeed: 0无玩法语义不旋转
engDamping, // damping
1, // angleDamping: 1无阻
spec.gravityScale != null ? spec.gravityScale : 0, // gravityScalehost 未 setGravity全局 gravity=0 故暂不生效OQ-4 follow-up
Math.PI, // particleConeAngle粒子自转锥留默认
0.1, // fadeRate默认
0, // randomness: 0spec 已显式给参,关引擎默认 .2 额外抖动求确定)
false, // collideTiles: false
spec.additive != null ? spec.additive : false, // additive
true // randomColorLinearA=B 时无影响)
);
// 7) 封装句柄禁泄漏引擎对象isActive/stop 转发stop 幂等销毁)。
return {
isActive() { return em.isActive(); }, // esm.js:8482
stop() { if (!em.destroyed) em.destroy(true); }, // 幂等(已 destroyed 直返,不二次销毁)
};
},
},
// ── audio.synth ──薄包装引擎 zzfxG/zzfxM参数包/曲谱 → PCM 样本(不播放)。
// 边界模型(创始人 2026-06-13 拍):引擎有 zzfxG/zzfxM → 此处薄包装;插件侧删 vendored 合成核,一职一路。
// 引擎 zzfxG **不烤主音量 ×0.3**(样本=s*volumeesm.js:7391×0.3 在引擎 gain 层 soundVolume=.3esm.js:3121/6777
// → 返回的样本是「未乘 0.3 的 PCM」与 api.d.ts EngineAudioSynth 口径一致×0.3 由插件播放层补audio-music/impl.js playChannels
// 合成异常/失败返 null插件侧 → 发声 no-op不回退 vendored
audio: {
synth: {
/**
* ZzFX 单音效合成核参数包 PCM 单声道样本数组未乘 0.3不播放薄包装引擎 LJS.zzfxG
* @param {number[]} params ZzFX 参数包
* @returns {number[]|null} PCM 单声道样本合成异常返 null
*/
synthSfx: (params) => {
rec('audio.synth.synthSfx'); // 真接线门②:记一次「插件经 ctx.getEngine().audio.synth 真调引擎 zzfxG」
try {
// 引擎 zzfxG 收 positional 参数(与 vendored 同签名esm.js:7326→ 展开参数包。
return LJS.zzfxG(...params);
} catch (e) {
// 合成异常降级为 null不连坐游戏记 stderr插件侧据 null 走发声 no-op。
console.error('[host][engine-audio] zzfxG 合成异常,返 null' + (e && e.message ? e.message : e));
return null;
}
},
/**
* ZzFXM 曲谱合成核乐器/模式/序列/BPM [L,R] 双声道样本未乘 0.3不播放薄包装引擎 LJS.zzfxM
* @param {number[][]} instruments 乐器表
* @param {number[][][]} patterns 模式表
* @param {number[]} sequence 序列
* @param {number} [bpm] 节拍缺省引擎默 125
* @returns {number[][]|null} [L,R] 双声道样本合成异常返 null
*/
synthSong: (instruments, patterns, sequence, bpm) => {
rec('audio.synth.synthSong'); // 真接线门②:记一次「插件经 ctx.getEngine().audio.synth 真调引擎 zzfxM」
try {
// 引擎 zzfxM 签名与 vendored 逐字一致esm.js:10837 `zzfxM(instruments, patterns, sequence, BPM=125)`)。
return LJS.zzfxM(instruments, patterns, sequence, bpm);
} catch (e) {
console.error('[host][engine-audio] zzfxM 合成异常,返 null' + (e && e.message ? e.message : e));
return null;
}
},
},
},
};
}

View File

@ -42,9 +42,12 @@ import {
// setCanvasClearColor / BLACK / vec2 / mainCanvas / mainContext / frame / time / timeReal /
// paused / timeScale 等全经 LJS.* 触达。
import * as LJS from 'littlejsengine';
// 【A1】引擎数学门面构造纯函数零引擎 importreal 通道工厂 makeEngineCaps 用它包装引擎 lerp/smoothStep/Ease。
// 【A1】引擎数学门面构造纯函数零引擎 importreal 通道工厂用它包装引擎 lerp/smoothStep/Ease。
// 单列在 engine-math.js不 import 引擎)→ node 单测可 import 它注入 mock ljs 验门面,规避 F7host.js 顶层 import 引擎在 node 抛 window
import { buildEngineMath } from './engine-math.js';
// 【P1 提升】引擎能力背书工厂EngineCapabilities 三面math/particles/audio.synth已从本文件闭包提升为可 import 模块,
// host-dev / wanglanmei-ref real 通道共用同一份(搬运·语义零变)。本模块不 import 引擎(经入参 LJS 注入)。
import { createEngineCaps } from './engine-caps.js';
/*
* 取证固定环境常量 test/harness/browser-evidence.cjs FIXED_ENV 对齐
@ -182,140 +185,10 @@ export function bootHostDev(opts) {
return realAudioCtx;
}
/**
* A1引擎能力背书工厂构造 EngineCapabilities 三面math easing 门面 / particles / audio.synth
* - real 通道返真能力面stub/node 通道短路返 null null-engine 回归战场 + 规避 F7 引擎 import node 抛错
* real/stub 都经 buildBundlesetupAfterEngine 若无条件背书引擎stub 通道也会拿到引擎能力 null 回归
* 故首行按 engineMode 短路stub null等价未注入触发 plugin.js getEngineShared null 分支告警一次 + getEngine() null
* - 引擎 import 唯一活点= host(LJS@顶部 import)插件零直接 import一律经 ctx.getEngine().*Q4 铁律
* - math/easing=纯数学族特例政策 §1.3门面逐字节/ULP 等价引擎 LJS.lerp/smoothStep/Ease
* gamefeel 侧保内置实现作 null-engine fallback非有意替代easing 门面**只暴 11 条等价曲线**
* backInOut/elasticInOut 不入面引擎 IN_OUT 拼接是另一条曲线 6.6%/17%gamefeel =
* - particles/audio.synth=A1 留空骨架占位A2/A3 在同一工厂内**替换填体**串行落避免本函数合并冲突
* @returns {import('../src/core/api.d.ts').EngineCapabilities|null}
*/
function makeEngineCaps() {
if (engineMode !== 'real') return null; // stub/node不背书引擎保 null-engine 回归F7node 本就 import 不了引擎)
// 【真接线门②·host-dev 取证探针,绝不入渠道产物】每个引擎能力叶方法被调时记一条 call-ID 到 window.__engineCalls
// 供 mini-desktop CDP harness 断言「插件运行时真的经 ctx.getEngine() 调到了引擎」(动态运行证据,区别于门①的静态 import
// rec 失败静默(取证副作用绝不连坐运行时);本函数在 engineMode!=='real' 已 return null故探针只活 real 通道。
const rec = (id) => { try { (window.__engineCalls || (window.__engineCalls = [])).push(id); } catch (_) { /* 取证副作用,忽略 */ } };
// math 门面A1 buildEngineMath 真活包装引擎)外再裹一层 reclerp/smoothStep 直裹easing 子面逐曲线裹(保全部键与口径不变)。
const mathRaw = buildEngineMath(LJS);
const math = {
lerp: (a, b, p) => { rec('math.lerp'); return mathRaw.lerp(a, b, p); },
smoothStep: (p) => { rec('math.smoothStep'); return mathRaw.smoothStep(p); },
easing: Object.fromEntries(Object.keys(mathRaw.easing).map((k) => [k, (t) => { rec('math.easing.' + k); return mathRaw.easing[k](t); }])),
};
return {
// ── mathA1 落实现,含 easing 门面;外层 rec 记调用,内层 mathRaw 真包装引擎 lerp/smoothStep/Ease──
math,
// ── particlesA2 填体)──薄包装引擎 ParticleEmitterEngineEmitterSpec像素口径→ 引擎世界口径,返封装句柄。
particles: {
/**
* 受控面 spawnEmitterEngineEmitterSpec像素口径 引擎 ParticleEmitter世界口径返封装句柄禁泄漏引擎对象
* 边界模型创始人 2026-06-13 引擎有 ParticleEmitter 此处薄包装插件侧删自研 sim 积分循环一职一路
* 引擎自渲new ParticleEmitter EngineObject 构造自入 engineObjectsesm.js:3580引擎主循环每帧自调
* update发射+积分esm.js:8348+ renderdrawTile mainContextesm.js:8654/8658 host 无需手动 add/render
* @param {import('../src/core/api.d.ts').EngineEmitterSpec} spec 归一化发射参数像素/中性形态
* @returns {import('../src/core/api.d.ts').EngineEmitterHandle} 封装句柄 isActive/stop不见引擎对象
*/
spawnEmitter(spec) {
rec('particles.spawnEmitter'); // 真接线门②:记一次「插件经 ctx.getEngine().particles 真调引擎粒子构造」
// 1) 像素↔世界阻抗换算cameraScale 默认 32esm.js:2785host 未 setCameraScale 故用引擎 live binding 默认值I3/OQ-2
const worldPos = LJS.screenToWorld(LJS.vec2(spec.pos.x, spec.pos.y)); // 像素→世界esm.js:5067/screenToWorld
const SCALE = LJS.cameraScale; // 世界单位/像素(用 live binding随引擎相机变
// 2) burst/continuous → emitTime/emitRateI1引擎无「spawn 恰好 N」原语burst 用「1 逻辑帧窗 × count×60 速率」近似 ≈N 颗)。
const emitTime = spec.emitTime != null ? spec.emitTime
: (spec.count != null ? 1 / 60 : 0); // burst=1帧窗(满即自停) / continuous=0(forever)
const emitRate = spec.emitRate != null ? spec.emitRate
: (spec.count != null ? spec.count * 60 : 100); // burst: count×60 颗/秒(该窗内恰发 ≈count 颗)
// 3) speed像素/秒 → 世界/帧@60引擎 speed 单位来源 esm.js:8253无再乘 dt
const engSpeed = (spec.speed != null ? spec.speed : 0) / SCALE / 60;
// 4) damping每帧速度乘子spec 已由插件把 drag/秒折算为每帧乘子,这里直透;缺省 1=无阻)。
const engDamping = spec.damping != null ? spec.damping : 1;
// 5) 颜色:归一化 {r,g,b,a} → 引擎 Coloresm.js:1547 rgb。缺省不透明白→透明保 real 像素门「有色像素命中>0」I2
const cs = spec.colorStart || { r: 1, g: 1, b: 1, a: 1 };
const ce = spec.colorEnd || { r: 1, g: 1, b: 1, a: 0 };
const colorStart = LJS.rgb(cs.r, cs.g, cs.b, cs.a);
const colorEnd = LJS.rgb(ce.r, ce.g, ce.b, ce.a);
// 6) 构造引擎发射器27 参,按 d.ts:3232 构造签名逐位映射)。
const em = new LJS.ParticleEmitter(
worldPos, // pos世界
spec.angle != null ? spec.angle : 0, // angle
(spec.emitSize != null ? spec.emitSize : 0) / SCALE, // emitSize 像素→世界
emitTime, // emitTime0=forever
emitRate, // emitRate
spec.coneAngle != null ? spec.coneAngle : Math.PI, // emitConeAngle半锥角
undefined, // tileInfo: untextured2D fillRect 路)
colorStart, colorStart, colorEnd, colorEnd, // colorStartA/B + colorEndA/BA=B → 固定色不随机)
spec.particleTime != null ? spec.particleTime : 0.5, // particleTime
(spec.sizeStart != null ? spec.sizeStart : 6) / SCALE, // sizeStart 像素→世界
(spec.sizeEnd != null ? spec.sizeEnd : 0) / SCALE, // sizeEnd 像素→世界
engSpeed, // speed世界/帧@60
0, // angleSpeed: 0无玩法语义不旋转
engDamping, // damping
1, // angleDamping: 1无阻
spec.gravityScale != null ? spec.gravityScale : 0, // gravityScalehost 未 setGravity全局 gravity=0 故暂不生效OQ-4 follow-up
Math.PI, // particleConeAngle粒子自转锥留默认
0.1, // fadeRate默认
0, // randomness: 0spec 已显式给参,关引擎默认 .2 额外抖动求确定)
false, // collideTiles: false
spec.additive != null ? spec.additive : false, // additive
true // randomColorLinearA=B 时无影响)
);
// 7) 封装句柄禁泄漏引擎对象isActive/stop 转发stop 幂等销毁)。
return {
isActive() { return em.isActive(); }, // esm.js:8482
stop() { if (!em.destroyed) em.destroy(true); }, // 幂等(已 destroyed 直返,不二次销毁)
};
},
},
// ── audio.synthA3 填体)──薄包装引擎 zzfxG/zzfxM参数包/曲谱 → PCM 样本(不播放)。
// 边界模型(创始人 2026-06-13 拍):引擎有 zzfxG/zzfxM → 此处薄包装;插件侧删 vendored 合成核,一职一路。
// 引擎 zzfxG **不烤主音量 ×0.3**(样本=s*volumeesm.js:7391×0.3 在引擎 gain 层 soundVolume=.3esm.js:3121/6777
// → 返回的样本是「未乘 0.3 的 PCM」与 api.d.ts EngineAudioSynth 口径一致×0.3 由插件播放层补audio-music/impl.js playChannels
// 合成异常/失败返 null插件侧 → 发声 no-op不回退 vendored
audio: {
synth: {
/**
* ZzFX 单音效合成核参数包 PCM 单声道样本数组未乘 0.3不播放薄包装引擎 LJS.zzfxG
* @param {number[]} params ZzFX 参数包
* @returns {number[]|null} PCM 单声道样本合成异常返 null
*/
synthSfx: (params) => {
rec('audio.synth.synthSfx'); // 真接线门②:记一次「插件经 ctx.getEngine().audio.synth 真调引擎 zzfxG」
try {
// 引擎 zzfxG 收 positional 参数(与 vendored 同签名esm.js:7326→ 展开参数包。
return LJS.zzfxG(...params);
} catch (e) {
// 合成异常降级为 null不连坐游戏记 stderr插件侧据 null 走发声 no-op。
console.error('[host][engine-audio] zzfxG 合成异常,返 null' + (e && e.message ? e.message : e));
return null;
}
},
/**
* ZzFXM 曲谱合成核乐器/模式/序列/BPM [L,R] 双声道样本未乘 0.3不播放薄包装引擎 LJS.zzfxM
* @param {number[][]} instruments 乐器表
* @param {number[][][]} patterns 模式表
* @param {number[]} sequence 序列
* @param {number} [bpm] 节拍缺省引擎默 125
* @returns {number[][]|null} [L,R] 双声道样本合成异常返 null
*/
synthSong: (instruments, patterns, sequence, bpm) => {
rec('audio.synth.synthSong'); // 真接线门②:记一次「插件经 ctx.getEngine().audio.synth 真调引擎 zzfxM」
try {
// 引擎 zzfxM 签名与 vendored 逐字一致esm.js:10837 `zzfxM(instruments, patterns, sequence, BPM=125)`)。
return LJS.zzfxM(instruments, patterns, sequence, bpm);
} catch (e) {
console.error('[host][engine-audio] zzfxM 合成异常,返 null' + (e && e.message ? e.message : e));
return null;
}
},
},
},
};
}
// 【P1 提升】引擎能力背书工厂 makeEngineCaps 已从此处闭包提升为可 import 模块 host-dev/engine-caps.js
// 的 createEngineCaps({LJS, engineMath, engineMode, recHook})搬运·语义零变host-dev / ref real 通道共用)。
// buildBundle 处以闭包 makeEngineCaps 桥接(保 engineFactory 注入口径不变 + host-dev 探针 rec 经 recHook 注入)。
// 逐行保语义回归基线=门0 8/8 + real 门 6/6提升不得改任何判据数值
// 创建 host-dev 受控上下文:注入「渲染面 / 主种子 / 音频工厂」。
// 【A0 改点】受控面 getContext2d 的 backingreal 模式=引擎 engineInit 自建的 mainContext纯 2D
@ -356,7 +229,15 @@ export function bootHostDev(opts) {
seed,
audioFactory,
clock: () => mockNowMs,
engineFactory: makeEngineCaps, // 【A1】注入引擎能力工厂工厂内按 engineMode 短路stub→null保 null-engine 回归)
// 【P1 提升】注入引擎能力工厂;工厂内按 engineMode 短路stub→null保 null-engine 回归)。
// host-dev 仍注入 recHook门② 探针只在 host-dev 取证产物里host-dev 非渠道产物,符合现状 host.js 原 rec 语义)。
// recHook 失败静默取证副作用绝不连坐运行时createEngineCaps 在 engineMode!=='real' 已 return null故探针只活 real 通道。
engineFactory: () => createEngineCaps({
LJS,
engineMath: buildEngineMath(LJS),
engineMode,
recHook: (id) => { try { (window.__engineCalls || (window.__engineCalls = [])).push(id); } catch (_) { /* 取证副作用,忽略 */ } },
}),
});
// 合法包裹 bundle.deriveContextForhost 拥有 bundle注册器为每插件派生上下文时记下该插件的
// 派生 random 实例引用。这样 host 取证时能复位「插件那一份」随机(见 derivedRandoms 注释)。

59
game-runtime/src/core/game-host.d.ts vendored Normal file
View File

@ -0,0 +1,59 @@
// 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
* - ctxPluginContext ctx.getEngine() //
* ctx.getInput() ctx.random/time /** ctx /DOM**
* - mainContext mainContextsetGLEnable(false) 2D render(g) g
* mainContext****
* PluginContext.getContext2d
* - seedcanvas 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) gameUpdatedt=FIXED_DT core.update//
* · render(g) gameRenderg=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; // 卸载(幂等)
}
/**
* importhost 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 P1spike P3
* @param opts difficulty/variant additive
*/
export type GameHostFactory = (opts?: Record<string, unknown>) => GameInstance;