merge(wg1): 并入 L1 scale-20 放量 + 宿主键盘桥 fix(d754b71·关键盘输入缺口)

独立验证(不信自述)后并入 fix/host-keyboard-bridge(=wg1/scale-20 超集,merge-base d79f2c3):

 宿主键盘桥 fix(d754b71·5行纯增量):boot-game-host.js attachInput() 补 window keydown/keyup
  桥接(ctx.getInput 键盘通道原为空→方向键/空格游戏输入死),对齐 host-dev/host.js:427-428 既有范式,
  inputBridge _assertType 白名单本已含 keydown/keyup=零兼容风险。独立验:tetris verdict 修后
  pass=True/9门/0挂(翻绿铁证)、tictactoe pass=True。

L1 scale-20 14 款放量(briefs/results/calibration/m3_player_test.py + tictactoe/tetris 正样本):
  主力 7/8 真可玩;spec 预判门面缺口几乎全推翻(CCD/文本HUD/网格/match-finding 便宜模型自写对),
  真·L0 门面缺口收敛为唯一一处=宿主键盘桥(本 fix 已补)。trial merge 干净(仅 .agents skill
  auto-merge),我 3b-B 文件(service.py/CallbackSignatureVerifier)完好。

校正:我 §5『点击坑=moat 真壁垒』系单样本(3b-B)过度泛化,与 L1 §9 scale-20 实证矛盾→已改为
  承认便宜模型相当能干、真缺口=键盘桥(已修)+driver-coverage(harness),非模型能力。

重大上修:moat 生成质量远好于 3b-B 单样本所示;失败主因=宿主门面(已补)+harness driver 覆盖,非模型。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
zizi 2026-06-14 23:49:40 +00:00
commit 23e86934d2
161 changed files with 18609 additions and 12 deletions

View File

@ -37,8 +37,8 @@ brief 题面 → 便宜模型写 `generated-factory.js`(一个 `GameHostFactor
## 5. 边界与未做
- 只消费冻结契约(`game-host.d.ts` / 6 受控面 / `__GameBundle` / `bootGameHost`**零修改**;只在 lane 分支,不推 dev/2.0.0。
- **✅ 生产接线已成(3b-B 派发面,`c99014c`)**`AigcGenerateExecutor.dispatchGeneric`→真 worker `service.py`(包 `run_studio`)→HMAC 回调→落库入 feed 全链真证闭合(见 [`ai-generation-pipeline.md`](./ai-generation-pipeline.md) §⓪′)。**但 3b-B e2e 确认「点击坑」是 moat 真壁垒**:一句话「点击小怪物」三款真生成全 `nine_gate_failed`——截图证真出 on-theme 可渲可动游戏(Score/3s/monsters),仅点击→计分坏(Score 恒 0)→**九门正确拦截不可玩**(正是 §3 点击坑/harness §7 G-H 门逮的),`run_studio` 自修 `max_repairs` 未救回。**铁律重申**:plumbing 易、**便宜模型把「点击命中→计分」写对=真难点**;质量门拒发不可玩=系统正确履职(可玩性地板),非故障
- **未做**(创始人并行 / 后续波次):全 20 款 bake-off、四模型横比过门率/重试/¥每过门)、碰撞穿透门 J、**落一个过九门的正样本**v5 挡板太慢挂 H/I;3b-B「点击小怪物」挂点击命中——均需 prompt 给具体参数[挡板速度/命中半径] + driver 持续/精准出招 + 阈值校准)——规划见 [`2026-06-14-WG1-bakeoff天花板与Claude-free评估门-review.md`](../../docs/agent-specs/2026-06-14-WG1-bakeoff天花板与Claude-free评估门-review.md)。
- **✅ 生产接线已成(3b-B 派发面,`c99014c`)**`AigcGenerateExecutor.dispatchGeneric`→真 worker `service.py`(包 `run_studio`)→HMAC 回调→落库入 feed 全链真证闭合(见 [`ai-generation-pipeline.md`](./ai-generation-pipeline.md) §⓪′)。3b-B 单款一句话「点击小怪物」真生成卡九门(Score 恒 0→**质量门正确拒发不可玩=系统履职非故障**),`run_studio` 自修 `max_repairs` 未救回。**⚠️ 但单点失败≠模型壁垒**:L1 scale-20 14 款实证(§9)便宜模型实际相当能干(主力 7/8 可玩、自写 CCD/文本HUD/网格/match 全对),多数失败是**宿主键盘桥 host bug(已修 `d754b71`)+ driver-coverage(harness 太朴素),非模型**;3b-B 那款真因(driver-coverage 或该款 hit-detection)续查并入 §9 画像。**真难点不是模型能力,是 harness driver 覆盖 + 宿主门面完备**(§9)
- **未做/续(创始人并行)**20 款补全(scale-20 已 14 款,见 §9)、四模型横比(过门率/重试/¥每过门)、6 个 driver-coverage 失败款的 driver 增强(flappy/multiball/愤怒小鸟等)、键盘款修后批量重跑、calibration 人锚补全——规划见 [`2026-06-14-WG1-bakeoff天花板与Claude-free评估门-review.md`](../../docs/agent-specs/2026-06-14-WG1-bakeoff天花板与Claude-free评估门-review.md)。
## 6. 官方 latch 终态契约 + 套壳P3 对齐2026-06-14
@ -66,3 +66,13 @@ P3 主线把 L1 产出靶定死为**官方通用装载壳**`game-runtime/game
- **gate-H 盲打脚本会假阴性**:固定坐标 taps 打不动技巧游戏→"0 进展"≠空心(创始人把被判空心的 v2 排第 2。解法 = **适配性真玩 driver**(读 state 接球harness §7
- **founder/人锚不可替代**(至少现阶段):没有它会把碰撞有问题的 pro 当"最佳"发出去。**校准 harness** = `wg1/gen-worker/worker/agent_loop/calibrate.py`(吃 `calibration/labels.json` 人锚,算 player vs 人锚 MAE/吻合/反样本否决)。
- **路线**:确定性门做"地板"(能跑/真接线/有进展/latch/跟手),手感尽量拆成确定性检查(碰撞穿透门 J 规划中);拆不掉的主观手感留**创始人抽检**。
## 9. scale-20 放量实证:模型能力画像 + spec 预判推翻(W-G1 lane v3,2026-06-14)
flash 打底放量 14 款(主力 8 + 压测 6),Opus 对抗审计核实(2 个 ultracode workflow):
- **主力 7/8 真可玩**(0 假绿):井字棋/见缝插针/打砖块/跑酷/太空侵略者/Doodle/扫雷(扫雷=borderline——本体可玩,但门弱断言经"踩雷 lose"路径满足、证据薄)。flappy 唯一未过=**driver-coverage**(升 pro 仍挂,但源码物理合理、理想 AI 得分→是 flap-to-gap driver 太朴素,**非模型天花板**)。
- **压测**:match3 ✅;**2048/Tetris/Asteroids 三款键盘游戏全挂于宿主缺键盘桥**(非模型,见 [`game-e2e-cdp-harness.md`](./game-e2e-cdp-harness.md) §8);multiball/愤怒小鸟=driver-coverage。
- **★ 模型能力画像(flash)**:文本 HUD / 网格状态机(井字棋胜负+AI、扫雷洪泛)/ 子步 swept CCD(asteroids/multiball)**均自写正确**;计时/物理精度(Flappy)需**题面 pin 死数值 + 收紧观测契约**(Doodle 实证:收紧计分 + seek-x driver → regen 首发过)。栽点主要在**静态门可拦的纪律滑坡**(Date.now/Math.random/缺 render·destroy/没调 getEngine),零 CDP 成本拦下、回喂即修。
- **★ spec 预判的门面缺口几乎全推翻**:CCD / 文本HUD / 网格 / match-finding / 手势均**非缺口**(便宜模型自带或自写;故"碰撞穿透门 J"已非急需)。**真·L0 门面缺口收敛为唯一一处=宿主键盘桥**(已 spawn_task 交 L0)。完整数据见 [`2026-06-14-WG1-scale20-短板量化报告.md`](../../docs/agent-specs/2026-06-14-WG1-scale20-短板量化报告.md)。
- **M3 视觉 player 当生产过滤器**(`calibration/m3_test.json`):与门 8/9 一致、抓显性空心(flappy fun1)、漏截图不可见失败(键盘死/弱证据,恰被确定性门兜住)→ **三层互补**(门=地板 / M3=主观过滤 + 便宜 fun 信号 / 创始人=人锚)。MAE 压低待创始人对新款补锚(框架沿用 `calibrate.py`)。

View File

@ -73,3 +73,24 @@
**G 门 = 本 lane 关键发明,专逮"假绿"**:Pong 这类带**自走动画**的游戏,输入全失灵也"能动"、能骗过 C/D/E;G 门用同帧号 A/B 哈希对照隔离出"输入到底改没改变走向"。实证逮住一处**输入坐标契约写错**(生成 prompt 误称归一坐标、实为逻辑像素 `boot-game-host.js:295`)致"能编译能渲染能动但点击全失灵"的假绿。
**CDP 触摸下发坑**(承 §2 律3,生成游戏侧细化):tap 须**镜像 ref `realTapAt`**——裸 `{x,y}`(逻辑像素,经 §2 律2 rect 映射)、`touchStart`/`touchEnd` **背靠背**(无 radius/force/长按),否则触摸不达 `getInput()`。坐标契约 = **逻辑像素 [0..390]/[0..844],非归一**(便宜模型与人皆易错)。
## 8. scale-20 放量实证:driver 库泛化 + 门承重件 + 对抗审计硬化(W-G1 lane v3,2026-06-14)
**适配 driver 库 1→6 型 + drag 输入事件**(`play.cdp.cjs`,`spec.driver.type` 分发;盲打打不动技巧/实时游戏=H 假阴根因,故按机制族建 driver):
| driver | 机制族 | 读什么自动出招 |
|---|---|---|
| `paddle-intercept` | 挡板接球 | `ball.x` → 挡板对齐 |
| `tap-targets` | 点击离散目标 | `state.targets[{x,y,occupied}]` → 点未占用(井字棋/扫雷/打地鼠/Simon/invaders…) |
| `flap-to-gap` | 重力穿缝 | `bird.y` vs `nextGap.centerY` → 偏低拍翅 |
| `seek-x` | 横向趋近接台 | `bird.x` vs `nextPlatform.x` → 点左/右半 |
| `tap-pairs` | 相邻交换(三消) | `targets[k]`/`[k+1]` 成对点 |
| `key-cycle` | 纯按键 | 循环按 `keys[]` 到 gameover(**依赖宿主键盘桥,见下** ) |
固定输入新增 `{t:'drag',from,to,ms}`(touchStart→插值 touchMove→touchEnd,合成 swipe/拖拽)。**driver 覆盖度 = 门能否兜住的前提**:shooter/填充型 latch 需足够步数驱动到终局(invaders/tetris/multiball 的 latch 假阴皆此因,非游戏坏;invaders 步数 30→50 即翻绿,实证)。
**★ 地板真承重件 = `expectLatch` + `G_input`,不是 progress 计数**(对抗审计实证):`H_progress.pass = progressPass && latchPass`;单条 `assertAfterPlay`(score/revealed/moves +1)鉴别力弱——saolei 踩雷也 revealed++、runner score 计时自增,可被"漂亮空壳"蹭过;真隔离空壳的是 latch(真到 gameover+600ms 驻留)+ G_input(同帧 A/B 哈希证输入真生效)。**新游戏务必让 progress 断言带终局语义**(全清/三连/真死),否则靠 latch+G 兜。
**★ 宿主键盘桥缺失(L0 硬缺口,实证)**:`boot-game-host.js attachInput()` 只桥 pointer 三态、**不桥 keydown/keyup** → `ctx.getInput()` 键盘通道空,所有方向键游戏(2048/Tetris/Asteroids)输入死。**失败严格沿"键盘 vs 指针"分界=最强系统性证据**(指针款全过、键盘款全挂)。修法对齐 `host-dev/host.js:427-428`(已 spawn_task 交 L0)。→ **移动端轻游戏优先 pointer/tap/swipe,键盘款待宿主补桥**;配套 `play.cdp.cjs` key() 对 Arrow 须传 `code===key`(原 undefined 致 event.key 空)。
**★ 对抗审计硬化门(Opus build-then-exit,ultracode workflow)**:脚手架期用 Opus 多 agent 对抗审计九门判定(反驳每个 PASS 找假绿 + 归因每个 FAIL)——实证抓出 saolei 弱断言(门+M3 都没抓到)、把 flappy 错归"模型天花板"纠正为"driver 缺陷"、定位宿主键盘根因。**生产无 Claude:门(地板)+ M3 便宜 player(视觉过滤:抓显性空心、漏截图不可见失败)+ 创始人(人锚)三层互补**;Opus 审完去硬化门/driver 即退出。

View File

@ -0,0 +1,148 @@
# 《W-G1 · 20 经典轻游戏放量 — 短板量化报告》
> 状态:**进行中**(主力批 8 款已终;压测批 6 款 + flappy@pro 后台跑中;player 校准待做)· 2026-06-14
> lane:`wg1/scale-20`(基于 `dev/2.0.0 @d79f2c3`)· 模型:DeepSeek-V4-flash 打底、耗尽升 pro(经 new-api 网关 `100.64.0.8:3000`,无 Claude)
> 口径:每款过**九门**(A-G 假绿守卫 + H 机制/latch + I 控制手感),`results/<id>.json` 为原始证据。靶集见 [20 经典轻游戏基准 review](2026-06-13-WG1-20经典轻游戏基准-review.md)。
---
## 一、结论先行(给创始人/L0 的三句话)
1. **便宜模型(flash)+ 九门 harness 能产可玩轻游戏**:主力批 8 款 flash 实过 **7/8**(井字棋/见缝插针/打砖块/跑酷/太空侵略者/Doodle 经 Opus 对抗审计**确认真可玩、0 假绿**;扫雷=**borderline**,本体可玩但门证据过薄,见 §七)。多数款 1 次重试内过,**失败基本被静态门零 CDP 成本拦下、回喂即修**。**唯 Flappy 主力批未过**——经审计:**非模型天花板**(升 pro 仍挂、但理想瞄准 AI 可得分),根因是适配 driver 太朴素(driver-coverage,见 §七)。
2. **⚠️ 推翻 spec 预判**:基准 review 预测最痛缺口是「文本 HUD / 网格状态」。**实测 flash 这档能自写文本+网格**(井字棋、扫雷均过,grid 状态机/洪水填充/AI/计分自管正确)→ **这两类门面优先级应下调**
3. **真天花板 = 计时/物理精度类的生成质量 + harness 适配 driver 的覆盖**:Flappy(物理太硬秒死、缝隙观测漏出)flash 2×2 次全挂;Doodle 靠收紧计分语义 + 适配 driver 才救回;太空侵略者靠调 driver 步数才验到 latch。**门兜底有效的前提是 harness 有对的适配 driver**——本批把 driver 库从 1 种扩到 6 种即为此。
---
## 二、方法与口径
- **生成侧**:`wg1/gen-worker` worker(裸 openai client → new-api 网关),`briefs/<id>.json` = 题面 + play-spec(九门 driver 配置);闭环 generate→静态门→esbuild→九门真玩→失败回喂重试 ≤2。
- **真玩侧**:`game-runtime/games/_wg1-gen/_shared/play.cdp.cjs`(macOS 本机 Chrome+CDP)。**H/I 门靠"适配性真玩 driver"**:读 `_forensicsView().state()` 实体位/状态自动出招(盲打固定坐标对技巧游戏必假阴)。
- **成本**:每款 token 见下表;¥ 折算(cost.py / new-api quota)待最终补;闸 ¥0.15/款。
---
## 三、主力批 8 款结果(flash 打底)
| # | 款 | 结果 | 重试 | tokens(in/out) | driver | 首次失败 / 关键 |
|---|---|---|---|---|---|---|
| 1 | 井字棋 tictactoe | ✅ | 1 | 8354/10055 | tap-targets | 静态门:`Date.now` 违禁 → 回喂修 |
| 7 | 扫雷 saolei | ⚠️ ✅borderline | 1 | 8214/13932 | tap-targets | 本体源码可玩,但该 verdict 的 H 仅 `revealed 0→1` 且经【踩雷 lose 路径】满足(盲打首 tap 即踩雷、核心玩法零暴露)→ Opus 深审判 borderline,见 §七 |
| 18 | 见缝插针 jianfeng | ✅ | 0 | 3943/7005 | 固定输入 | **首发全过** |
| 5 | 打砖块 breakout | ✅ | 1 | 8115/9646 | paddle-intercept | 静态门:缺 render/destroy + 没调 getEngine + `Math.random` |
| 16 | 跑酷 runner | ✅ | 1 | 7908/16124 | 固定输入 | 静态门:`Date.now`;score 自增稳过,G 验跳跃有效、latch 验真撞死 |
| 10 | 太空侵略者 invaders | ✅ | 2 + 步数调 | 12211/25573 | tap-targets | **游戏本身好**(score 0→12 全清);仅 driver 步数 30→50 才驱动到 latch |
| 14 | 跳跃者 doodle | ✅ | 0(regen) | 4021/10197 | seek-x | 首版计分语义错(score 卡 118);收紧题面 + seek-x → regen 首发过(score 259→1003) |
| 4 | Flappy flappy | ❌(待 pro) | 2×2 | — | flap-to-gap | 生成物理太硬(`vy≈626` 秒坠死)+ `nextGap` 开局漏出;flash 天花板,升 pro 中 |
**主力批:flash 实过 7/8。** flappy@pro 结果待填。
---
## 四、短板发现(证据支撑)
1. **便宜模型主要栽点 = 静态门可拦的契约纪律滑坡**。高频违规:`Date.now`/`Math.random`(应走受控 `time`/`random`)、缺 `render`/`destroy` 方法、忘调 `getEngine()`(真接线门无信号)。**全部静态门零 CDP 成本拦下,回喂一次即修**——印证"门兜底"是便宜模型路线的命门,且大头开销不在 CDP 而在生成。
2. **文本 HUD / 网格状态:flash 能自写,非最痛缺口(推翻 spec 预判)**。井字棋(3×3 状态机+胜负+AI+文本)、扫雷(洪水填充+雷数文本)均过;仅扫雷的进展观测量 `revealed` 首发没接对、retry 修。→ **text/grid 门面优先级下调**;若补,价值在"省 token/一致性",非"造不出"。
3. **计时/物理精度类:生成质量 + driver 覆盖双重挑战(原"模型天花板"判断已被审计纠正)**。Doodle 首版计分语义错(score 卡 118)→ **收紧题面计分契约 + seek-x driver** regen 首发过(score 259→1003),证**生成质量可经题面收紧 + 适配 driver 救回**。Flappy 经 Opus 审计:**非模型天花板**——升 pro 三次仍挂、但源码物理参数合理(GRAVITY420/FLAP-260/GAP220),"理想瞄准 AI"多种子可得 5-15 分 → 根因是 **flap-to-gap driver 太朴素(无前瞻预测落点),对极端高位缝追不上**(driver-coverage)。→ 对策:**收紧物理/观测题面 + 给适配 driver 加前瞻**,而非简单升档。
4. **harness 适配 driver 的覆盖 = 门能否兜住的前提**。技巧/实时类盲打固定坐标必假阴(H 假阴);shooter 类 latch 需足够 driver 步数(invaders 实证:步数不够≠游戏坏)。本批据此扩 driver 库(见下)。
---
## 五、harness 产出:适配 driver 库(1 → 6 种 + drag 事件)
> gate-H 推广在实践中落地:把"读 state 自动真玩"覆盖到六类机制族。play.cdp.cjs。
| driver | 机制族 | 覆盖款 | 状态 |
|---|---|---|---|
| `paddle-intercept` | 挡板接球 | 打砖块 / 多球弹球 | 既有 |
| `tap-targets` | 点击离散目标 | 井字棋 / 扫雷 / 太空侵略者(+翻牌/Simon…) | 本批新建,3 款验证 |
| `flap-to-gap` | 重力穿缝 | Flappy | 本批新建,验证中 |
| `seek-x` | 横向趋近接台 | Doodle | 本批新建,✅ Doodle |
| `tap-pairs` | 相邻交换 | Match-3 | 本批新建,压测中 |
| `key-cycle` | 纯按键循环 | Tetris / 2048 / Asteroids | 本批新建,压测中 |
| `drag`(输入事件) | 拖拽手势 | 愤怒小鸟 | 本批新建,压测中 |
---
## 六、压测批 6 款结果(测上限/暴露缺口)— 已完成
> 经第二轮对抗诊断 workflow(7 agents)逐款归因。**重大发现:6 款失败严格沿「键盘 vs 指针」分界**——所有 key-cycle 键盘款(t2048/tetris/asteroids)集体挂、所有 pointer/tap 款全过,锁定根因 = **宿主键盘门面缺失**(见 §八),非模型逐个写坏。
| # | 款 | 结果 | driver | 根因 | 真·门面缺口? |
|---|---|---|---|---|---|
| 13 | Match-3 | ✅ r0 | tap-pairs | **真可玩**(score 0→40 真消除、latch 驻留;盲态交换凑出三连) | 无(swipe/match-finding 预判**未成立**) |
| 20 | 多球弹球 | ❌ latch | paddle-intercept | driver-coverage(游戏含**子步 swept CCD**、本体正确;9s 既清不完砖又护住末球→终态不可达) | 无;driver 加"建进展→弃守排空"相位即翻绿 |
| 11 | Tetris | ❌ latch | key-cycle | **宿主键盘门面缺失**(piecesLocked 0→1 实为重力自动锁定、非键盘;progress 假性过) | **是:键盘桥** |
| 2 | 2048 | ❌ score0 | key-cycle | **宿主键盘门面缺失**(50 次按键棋盘零变化;move/合并/终态本体正确) | **是:键盘桥** |
| 12 | Asteroids | ❌ score0 + F_wiring0 | key-cycle | **宿主键盘门面缺失**(键盘没到→不开火→不调引擎;子弹**已是 swept CCD**,CCD 预判未成立) | **是:键盘桥** |
| 15 | 愤怒小鸟 | ❌ score0(latch 过) | 固定 drag | drag-aiming(固定盲拖弹道最近距猪~218px;本体正确、F_wiring 过) | 无;driver 加第 7 型 drag-aiming(读目标反解拖拽矢量)+ 放宽关卡 tuning |
**压测批结论**:6 款里 **0 个是"便宜模型造不出"**——match3 真可玩;3 款键盘款挂在**同一处宿主缺键盘桥**;multiball/愤怒小鸟挂在 harness driver 覆盖。**spec 预判的 CCD/match-finding/手势/文本 HUD 缺口全部未成立**(详 §八)。
> **★ host 修复已验证(`fix/host-keyboard-bridge` 分支 · commit d754b71 · 2026-06-14,经创始人授权隔离执行)**:补宿主键盘桥 + 重打包 3 款重跑九门——**键盘输入全部接通**:**tetris 整盘翻绿**(piecesLocked 0→10 + latch✓,修前 0→1 是重力假性)、t2048 **score 0→4 真合并**(修前 0→0)、asteroids **F_wiring 0→27 + latch✓**(船真动真开火,修前全冻 0)。残留:t2048 latch(2048 盲态几乎不死锁)、asteroids score(盲射没瞄准)= driver-coverage 跟进,**非键盘非模型**。→ host 键盘桥根因**坐实**,供 L0 cherry-pick `d754b71` 到 dev/2.0.0。
---
## 七、三方对照:确定性门 vs M3 便宜 player vs Opus 教师审计(核心实证)
对主力批 9 款(7 pass + flappy/tetris fail)做三通道交叉验证。
### 7.1 M3 视觉 player(生产中间层,Claude-free)
MiniMax-M3 看首帧/玩后截图 + 九门数据判可玩/好玩(production-faithful,`roles.player_system` 人格)。结果 `wg1/gen-worker/calibration/m3_test.json`:
- **与门 8/9 一致**;独立判出 flappy「空心、开局即死、零得分」(fun=1, verdict=fix)——**便宜 player 能抓明显差的**。
- **fun 压缩**(可玩款都给 3、doodle 给 4):**细排不行(需创始人锚),但粗筛 hollow(1)↔可玩(3-4)能分开** = 过滤器能力成立。
- **盲点**:saolei 给 pass(fun 3)——M3 凭截图+常识判"扫雷骨架完整可玩",**没识破该次 run 只踩雷、核心玩法零暴露的弱证据**(见 7.3)。
**M3 作生产 player 过滤层:抓明显坏可信;审"门证据质量"不行**(那是教师层的事)。
### 7.2 Opus 教师审计(脚手架期,10 agents 对抗反驳)
每款派对抗审计员读源码+截图+state 尽力反驳"假绿"/归因失败:
- **0 假绿(危险侧无失守)**:7 个 claimed-pass 中 **6 真可玩**(tictactoe/jianfeng/breakout/runner/invaders/doodle,refutation 全失败,含独立 node 复算棋局、碰撞数学、分数防灌源码证伪)——**没有一个坏游戏被当好游戏放过**。
- **saolei = borderline(关键告警)**:游戏本体源码可玩(布雷/邻数/数字渲染/0格洪泛/win 判定全对),但该 verdict 的 H 仅 `revealed 0→1` 且经【踩雷 lose】满足 → 核心玩法在门证据里零暴露。**门没误判"游戏不存在",是 driver 盲打无避雷致证据过薄;门+M3 都没抓到,只有 Opus 深审抓到。**
- **2 fail 全是 driver-coverage,零 model-gap / 零 hard-ceiling**:
- **flappy**:8 门绿、仅 progress(score 0→0)挂。flap-to-gap driver 太朴素(无前瞻),对极端高位缝(centerY174/鸟559,单拍仅升 80px)追不上→撞管。**源码物理合理,理想瞄准 AI 多种子得 5-15 分→游戏可玩**。**升 pro 三次仍同门挂 → 证 driver 缺陷、非模型档位**。
- **tetris**:8 门绿、仅 latch 挂(piecesLocked 0→1 过但没堆到顶)。key-cycle 盲打 50 步 + clearLines 持续消行→永堆不到 spawn 行→gameover 不可达。**源码 latch 契约忠实、对人类可达**。换"读盘单列堆顶"driver 或大幅加步即翻绿。
### 7.3 地板的真实承重件 = latch + G_input,不是 progress 计数
审计核实 `play.cdp.cjs:516` `H_progress.pass = progressPass && latchPass`(硬 AND)。7 款 `assertAfterPlay` 全是单条弱计数(moves/revealed/inserted/score/remaining 动 1 步即过),其中 **saolei(踩雷也 ++)/ runner(score 计时自增、spec 自承"稳过")/ doodle(理论纯自弹也可能蹭高度)** 的 progress 子句鉴别力弱。**真正隔离"漂亮空壳"的是 expectLatch(真到 gameover + 600ms 驻留、堵自动重开)+ G_input(同帧 A/B 哈希证输入真生效)**;tictactoe/invaders/breakout 因终局语义(全清/三连/真掉球)progress 才较强。**门有鉴别力非橡皮图章**:flappy 卡 progress、tetris 卡 latch、t2048 连 E/G 都红——失败分布在不同门。
### 7.4 三方结论
- **确定性门 = 地板**:危险侧 **0 假绿**、有鉴别力;但**地板高度由 driver 覆盖度封顶**——driver 太弱则塌成 false-fail(flappy/tetris/saolei 同源),弱 progress 计数靠 latch+G_input 兜。
- **M3 便宜 player = 过滤器**:抓明显坏(flappy)、粗筛可玩,**担生产中间层可信**;审不出弱证据(saolei),细排靠创始人。**这正是三层 reframe 的中间层落地。**
- **Opus 教师 = build-then-exit 的硬化工具**:抓到 saolei 弱断言 + 把 flappy 错归因"模型天花板"纠正为"driver 缺陷"——**审完去硬化门/driver,生产跑 门+M3+创始人(无 Claude)**。
> 方法学诚实标注【inferred/静态】:Opus 审计系**源码 + 落盘证据静态交叉验证**(本机 serve/CDP 已停,saolei-borderline、flappy@pro-fail、invaders-replay 等部分款未现场重跑);结论可复核但非全量现场实测。
### 7.5 player-as-filter 量化(Task #6 自主部分)
以 Opus 审计的 playableTruth 为代理真值(创始人锚 MAE 需创始人补,框架沿用 `calibrate.py`),量化 M3 当过滤器(数据 `calibration/m3_test.json`):
- **M3 vs 确定性门:8/9 一致**(唯 tetris 分歧:门判 fail/latch、M3 判 pass——**门更严、抓到 M3 看不见的机制失败**)。
- **M3 vs 审计真值:7/9**。M3 正确:6 款 confirmed-playable 全 pass(**好游戏 0 误杀**)+ flappy 正确判 hollow(fun1)。漏判 2 款:**saolei(审计 borderline)、tetris(审计 fail/键盘死)——共性="截图里看着没问题"**(saolei 踩雷 lose 屏、tetris 有 1 块落定),M3 凭视觉+常识判可玩,看不出"该次 run 核心玩法没真跑"。
- **过滤器结论**:M3 **抓视觉显性空心**(flappy:鸟死/零分/空屏)有效;**漏截图不可见的失败**(键盘死、弱证据)——但这些**恰好确定性门已兜住**(tetris latch 挂、键盘款 score0 挂、saolei 弱断言已列整改)。→ **生产三层互补非冗余**:门抓机制失败(地板)、M3 抓视觉空心 + 给便宜 fun 信号(过滤 + 主观层)、创始人抓截图不可见的手感/弱证据(人锚)。**M3 边际价值不在"抓门漏的坏"(门更强),而在门通过后的便宜主观 fun 信号 + 减创始人对显性差件的抽检量。**
- **压 MAE / 查准下一步**:需创始人对这 9+ 新款亲玩给锚(我产不了)→ 入 `labels.json``calibrate.py` 出 MAE/查准查全数值;框架就绪、待人锚。
---
## 八、门面优先级建议(定稿,经两轮对抗诊断 workflow)
> 经压测批实测 + 源码核验,**spec 预判的功能门面缺口几乎全部被推翻**;真·L0 门面缺口高度收敛为**唯一一处**。
### P0 — 唯一真·L0 硬门面缺口(系统性命中 3 款,最高优先)
**宿主键盘事件门面缺失** —— `game-runtime/src/host/boot-game-host.js``attachInput()` 只桥 `pointerdown/move/up`,**完全不桥 `keydown/keyup`** → `ctx.getInput()` 键盘通道空。`getInput` 契约声明「pointer 三态 + key 两态」,实现漏了 key 两态。
- **证据**:失败严格沿「键盘 vs 指针」分界(3 键盘款全挂、所有指针款全过;同射击品类 invaders 走 pointer 即过);bundle 内 `_emit` 仅 pointer×5、`keydown` emit 0 命中;桥白名单已含 keydown/keyup 却无人 emit。
- **修法(L0,2 行,零兼容风险)**:`attachInput()``window.addEventListener('keydown', ev => { unlockAudioOnce(); inputBridge._emit('keydown',{key:ev.key}) })` + keyup,对齐同仓 `host-dev/host.js:427-428` 已有正确范式。一次解锁 t2048 / asteroids / tetris 三款。**已 spawn_task 交 L0**。
- **配套(我域,已修)**:`play.cdp.cjs:159` key() 对 Arrow 键发 `key=undefined` → 改为 `code===key` 直传(否则桥了 ev.key 游戏仍读空)。
### 实测未成立的预判(逐条澄清,勿再投入门面)
- **CCD swept**:asteroids(`lineCircleIntersect` 线段vs圆)、multiball(`SUB_STEP` 子步 swept)便宜模型**已自带连续碰撞**,无 tunneling。**不是缺口**。
- **文本 HUD**:6 款 `_forensicsView` + HUD 渲染齐全,D_render/E_live 全过。**不是缺口**。
- **网格状态**:井字棋/扫雷/2048/match3 网格逻辑 flash **自写正确**。**不是缺口**。
- **match-finding / swipe**:match3 盲态 tap-pairs 已凑出三连真过。**非 L0 门面**(要确定性可在 driver 侧加 grid-swap-finding)。
- **手势(drag-to-aim)**:愤怒小鸟缺的是 harness driver 第 7 型,**driver 域、非门面**。
### driver-coverage(harness 我域,非门面;是门面定优先级的前置净化步)
**"driver-coverage 可修翻绿"已双证**:invaders(步数 30→50 翻绿)、**tetris(host 键盘修复后整盘翻绿)**。其余残留经 driver 调参实测(flap 加速度前瞻 / paddle 弃守排空相位 / 2048 双向堆叠)**未轻易翻绿——因玩法特性、收益递减**,如实记档(均**非门面、非模型**,实验已 revert):
- **multiball**:终局可达,但盲态弃守排空时挡板被钳在屏内、角落仍接球,末球不漏 → latch 难达;需"反接球振荡排空"driver(未做)。
- **t2048**:2048 盲态**无强制终局**(随机走总有合法移动,实测双向也只死 1 次合并)→ latch 非盲态可达;需"填库死锁"driver 或 progress-only 门(玩法特性,非缺陷)。
- **flappy**:flap-to-gap 加速度前瞻仍救不回**该 pro 生成件**(物理太硬 + 缝隙观测早期 None)→ 需近理想控制 driver 或 pin-易物理重生成。
- **愤怒小鸟**:需 drag-aiming driver(读目标反解拖拽矢量)+ 游戏导出目标位;未做。
两轮诊断证实:把这些 false-fail 误记为门面缺口会污染优先级,故先净化 driver 覆盖再定门面。论题已立,残留属"调参/重生成"打磨项,非阻塞。

View File

@ -156,7 +156,7 @@ async function tap(cdp, xLogical, yLogical) {
async function key(cdp, code, downMs) {
const map = { ArrowLeft: 37, ArrowRight: 39, ArrowUp: 38, ArrowDown: 40, Space: 32, Enter: 13 };
const keyCode = map[code] || 0;
const k = code === 'Space' ? ' ' : (code && code.startsWith('Arrow')) ? undefined : code;
const k = code === 'Space' ? ' ' : code; // 方向键 DOM code===key,直接传;原对 Arrow 发 undefined 致 event.key 空、键盘游戏读 e.key 拿空被忽略(压测批诊断纠)
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', code, key: k, windowsVirtualKeyCode: keyCode, nativeVirtualKeyCode: keyCode });
await delay(downMs || 200);
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', code, key: k, windowsVirtualKeyCode: keyCode, nativeVirtualKeyCode: keyCode });
@ -200,9 +200,145 @@ function getPath(obj, p) {
return String(p).split('.').reduce((o, k) => (o == null ? undefined : o[k]), obj);
}
/** state
* type='paddle-intercept'每步读球 xtap 到球正下方把挡板移过去接真玩到清砖 gameover 即停 */
/** 适配性真玩驱动:读 state 自适应出招,解「盲打固定坐标打不动技巧游戏」的假阴性。按 driver.type 分发,缺省=paddle-intercept向后兼容。 */
async function runDriver(cdp, driver, hashes) {
if (driver.type === 'tap-targets') return runTapTargets(cdp, driver, hashes);
if (driver.type === 'flap-to-gap') return runFlapToGap(cdp, driver, hashes);
if (driver.type === 'seek-x') return runSeekX(cdp, driver, hashes);
if (driver.type === 'tap-pairs') return runTapPairs(cdp, driver, hashes);
if (driver.type === 'key-cycle') return runKeyCycle(cdp, driver, hashes);
return runPaddleIntercept(cdp, driver, hashes);
}
/** type='tap-targets' state().<targetsPath> {x,y,occupied} gameover latch
* 复用于井字棋/扫雷/翻牌/打地鼠/Simon/见缝插针/节奏等点击离散目标类游戏 */
async function runTapTargets(cdp, driver, hashes) {
const targetsPath = driver.targetsPath || 'targets';
const steps = driver.steps || 12;
const stepMs = driver.stepMs != null ? driver.stepMs : 320;
let drove = 0, cyc = 0;
for (let i = 0; i < steps; i++) {
const s = await readGameState(cdp);
if (s && s.phase === 'gameover') break; // 终局即停(交 latch 校验)
const targets = getPath(s, targetsPath);
if (Array.isArray(targets) && targets.length) {
let t = targets.find((g) => g && g.occupied !== true && typeof g.x === 'number');
if (!t) { t = targets[cyc % targets.length]; } // 全占用/未导出 occupied → 按序轮点
cyc++;
if (t && typeof t.x === 'number') { await tap(cdp, Math.max(4, Math.min(386, t.x)), Math.max(4, Math.min(840, t.y))); drove++; }
else { await delay(stepMs); }
} else { await delay(stepMs); } // 没导出 targets → 空转H/G 门会判红线)
try { hashes.push(await sampleHash(cdp, '#game-engine')); } catch (_) {}
await delay(stepMs);
}
return { drove, steps };
}
/** type='flap-to-gap':读控制体 y 与目标缝隙中心 y——体在缝下方y 更大=更低)就拍翅上升,否则任重力下落。驱动 Flappy 类穿缝。 */
async function runFlapToGap(cdp, driver, hashes) {
const yPath = driver.entityPath || 'bird.y';
const gapPath = driver.gapPath || 'nextGap.centerY';
const tapX = driver.tapX != null ? driver.tapX : 195;
const steps = driver.steps || 60;
const stepMs = driver.stepMs != null ? driver.stepMs : 110;
const margin = driver.margin != null ? driver.margin : 16;
let drove = 0;
for (let i = 0; i < steps; i++) {
const s = await readGameState(cdp);
if (s && s.phase === 'gameover') break; // 已结束(撞死/通关)→停,交 latch 校验
const by = getPath(s, yPath);
let gy = getPath(s, gapPath);
if (typeof gy !== 'number') gy = 422; // 无缝隙信息→维持中线高度
if (typeof by === 'number' && by > gy + margin) { await tap(cdp, tapX, 420); drove++; } // 偏低→拍翅上升
else { await delay(20); } // 偏高/已对齐→不拍,让重力下落
try { hashes.push(await sampleHash(cdp, '#game-engine')); } catch (_) {}
await delay(stepMs);
}
return { drove, steps };
}
/** type='seek-x':读控制体 x 与下一平台 x——点屏左/右半把体水平移向平台,驱动 Doodle 类踩台上升。 */
async function runSeekX(cdp, driver, hashes) {
const xPath = driver.entityPath || 'bird.x';
const targetPath = driver.targetPath || 'nextPlatform.x';
const leftX = driver.leftX != null ? driver.leftX : 70;
const rightX = driver.rightX != null ? driver.rightX : 320;
const steps = driver.steps || 60;
const stepMs = driver.stepMs != null ? driver.stepMs : 120;
const dead = driver.deadzone != null ? driver.deadzone : 18;
let drove = 0;
for (let i = 0; i < steps; i++) {
const s = await readGameState(cdp);
if (s && s.phase === 'gameover') break; // 已结束(掉落)→停,交 latch 校验
const bx = getPath(s, xPath);
const tx = getPath(s, targetPath);
if (typeof bx === 'number' && typeof tx === 'number') {
if (tx < bx - dead) { await tap(cdp, leftX, 420); drove++; } // 平台在左→点左半左移
else if (tx > bx + dead) { await tap(cdp, rightX, 420); drove++; } // 平台在右→点右半右移
else { await delay(20); } // 已对齐→不动
} else { await delay(stepMs); }
try { hashes.push(await sampleHash(cdp, '#game-engine')); } catch (_) {}
await delay(stepMs);
}
return { drove, steps };
}
/** 触屏拖拽touchStart(from) → 多帧 touchMove 插值到 to → touchEnd。受控面只给原始 pointerswipe/拖拽手势须自合成(愤怒小鸟蓄力等)。 */
async function drag(cdp, from, to, ms) {
const f = from || { x: 195, y: 600 }, t = to || { x: 195, y: 700 };
const n = 8, dur = ms || 400;
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: f.x, y: f.y }] });
for (let i = 1; i <= n; i++) {
const k = i / n, x = f.x + (t.x - f.x) * k, y = f.y + (t.y - f.y) * k;
await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x, y }] });
await delay(Math.max(8, Math.floor(dur / n)));
}
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
await delay(120);
}
/** type='tap-pairs' state().<targetsPath> (AB=) gameover
* 驱动 Match-3 交换相邻消除盲态轮换交换验便宜模型能否产可消三连(多半暴露需读盘找可消的更强 driver/swipe 门面) */
async function runTapPairs(cdp, driver, hashes) {
const targetsPath = driver.targetsPath || 'targets';
const steps = driver.steps || 30;
const stepMs = driver.stepMs != null ? driver.stepMs : 280;
let drove = 0, k = 0;
for (let i = 0; i < steps; i++) {
const s = await readGameState(cdp);
if (s && s.phase === 'gameover') break; // 终局即停(交 latch 校验)
const targets = getPath(s, targetsPath);
if (Array.isArray(targets) && targets.length >= 2) {
const a = targets[k % targets.length], b = targets[(k + 1) % targets.length];
k++;
if (a && b && typeof a.x === 'number' && typeof b.x === 'number') {
await tap(cdp, a.x, a.y); await delay(90); await tap(cdp, b.x, b.y); drove++;
} else { await delay(stepMs); }
} else { await delay(stepMs); }
try { hashes.push(await sampleHash(cdp, '#game-engine')); } catch (_) {}
await delay(stepMs);
}
return { drove, steps };
}
/** type='key-cycle':循环按下 driver.keys 列表里的键,到 gameover 即停。驱动 Tetris/2048/Asteroids 等纯按键游戏(盲态铺满/合并/射击→暴露上限)。 */
async function runKeyCycle(cdp, driver, hashes) {
const keys = (driver.keys && driver.keys.length) ? driver.keys : ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'Space'];
const steps = driver.steps || 40;
const stepMs = driver.stepMs != null ? driver.stepMs : 200;
let drove = 0;
for (let i = 0; i < steps; i++) {
const s = await readGameState(cdp);
if (s && s.phase === 'gameover') break; // 终局即停(交 latch 校验)
await key(cdp, keys[i % keys.length], driver.downMs || 60); drove++;
try { hashes.push(await sampleHash(cdp, '#game-engine')); } catch (_) {}
await delay(stepMs);
}
return { drove, steps };
}
/** type='paddle-intercept':每步读球 x、tap 到球正下方把挡板移过去接(真玩到清砖);到 gameover 即停。 */
async function runPaddleIntercept(cdp, driver, hashes) {
const ballPath = driver.ballPath || 'ball.x';
const paddleY = driver.paddleY != null ? driver.paddleY : 800;
const steps = driver.steps || 60;
@ -291,6 +427,7 @@ async function main() {
for (const ev of (spec.inputs || [])) {
if (ev.t === 'tap') await tap(cdp, ev.x, ev.y);
else if (ev.t === 'key') await key(cdp, ev.code, ev.downMs);
else if (ev.t === 'drag') await drag(cdp, ev.from, ev.to, ev.ms);
else if (ev.t === 'wait') await delay(ev.ms || 200);
try { hashes.push(await sampleHash(cdp, '#game-engine')); } catch (_) {}
await delay(50);

File diff suppressed because one or more lines are too long

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -0,0 +1,121 @@
{
"gameId": "angrybirds",
"url": "http://localhost:4320/games/_wg1-gen/angrybirds/index.html",
"ts": null,
"guards": {
"A_boot": {
"pass": true
},
"C_frame": {
"pass": true,
"f0": 48,
"f1": 78,
"delta": 30
},
"I_control": {
"pass": true,
"skipped": "未声明 controlCheck向后兼容"
},
"D_render": {
"pass": true,
"bright": 34719,
"maxCh": 255
},
"E_live": {
"pass": true,
"distinctStates": 10,
"samples": 15
},
"B_uncaught": {
"pass": true,
"uncaught": []
},
"F_wiring": {
"pass": true,
"callCount": 18,
"sample": [
"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",
"particles.spawnEmitter",
"audio.synth.synthSfx"
],
"expected": [
"particles.",
"audio.synth"
]
},
"G_input": {
"pass": true,
"skipped": "无 tap/key 输入"
},
"H_progress": {
"pass": false,
"checks": [
{
"path": "score",
"op": "increased",
"before": 0,
"after": 0,
"pass": false,
"why": "盲态固定拖拽发射→偶中目标得分;若固定角度打不中(需读目标算拖拽矢量)则不升=暴露拖拽瞄准缺口"
}
],
"latch": {
"pass": true,
"dwelledMs": 600,
"after": "gameover",
"reason": ""
},
"state0": {
"phase": "playing",
"result": null,
"score": 0,
"birdsLeft": 4,
"targets": [
{
"x": 290,
"y": 560
},
{
"x": 310,
"y": 520
},
{
"x": 280,
"y": 480
}
]
},
"state1": {
"phase": "gameover",
"result": "lose",
"score": 0,
"birdsLeft": 0,
"targets": [
{
"x": 290,
"y": 560
},
{
"x": 310,
"y": 520
},
{
"x": 280,
"y": 480
}
]
}
}
},
"pass": false,
"notes": []
}

View File

@ -0,0 +1,493 @@
'use strict';
/**
* 简版愤怒小鸟 GameHostFactory 模块
* 竖屏 390×844弹弓发射重力弹道砖块结构 + 5只鸟拖拽瞄准蓄力粒子+音效反馈
*/
export default function createGame(opts) {
const o = opts || {};
let ctx = null;
let seed = null;
let now = 0;
let t = 0;
let subPointers = null; // 订阅句柄
let subPointerup = null;
let subPointermove = null;
// 游戏状态
let phase = 'booting'; // 'playing' | 'gameover'
let result = '';
let score = 0;
let birdsLeft = 5;
let birdIndex = 0; // 当前正在用第几只 (0~4)
let bird = null; // 当前小鸟 { x, y, vx, vy, state: 'ready'|'flying'|'hit' }
let bricks = [];
let pigs = [];
// 拖拽状态
let dragging = false;
let dragStart = null; // { x, y } 弹弓口
let dragEnd = null; // 当前拖拽点
// 特效
let explosions = []; // { x, y, born, duration }
let texts = []; // 飘字 +score
const GRAVITY = 980; // px/s^2
const LAUNCH_FACTOR = 0.5; // 拖拽距离 -> 速度乘数
const BIRD_RADIUS = 8;
const PIG_RADIUS = 16;
const SLING_X = 70;
const SLING_Y = 580;
// ========== 内部辅助 ==========
function resetGameData() {
phase = 'playing';
result = '';
score = 0;
birdsLeft = 5;
birdIndex = 0;
bird = null;
bricks = [];
pigs = [];
dragging = false;
dragStart = null;
dragEnd = null;
explosions = [];
texts = [];
loadLevel();
spawnBird();
}
function loadLevel() {
// 固定布局:三个猪被砖块包围
// 猪
const pigPositions = [
{ x: 290, y: 560 },
{ x: 310, y: 520 },
{ x: 280, y: 480 },
];
for (const p of pigPositions) {
pigs.push({ x: p.x, y: p.y, r: PIG_RADIUS, active: true });
}
// 砖块 (矩形, 活跃)
const brickDefs = [
// 底部支撑
{ x: 270, y: 576, w: 24, h: 10 },
{ x: 306, y: 576, w: 24, h: 10 },
// 左竖墙
{ x: 262, y: 550, w: 10, h: 24 },
// 右竖墙
{ x: 322, y: 550, w: 10, h: 24 },
// 中层
{ x: 290, y: 546, w: 30, h: 10 },
{ x: 290, y: 534, w: 10, h: 12 }, // 垂直分隔
// 上层
{ x: 270, y: 506, w: 24, h: 10 },
{ x: 306, y: 506, w: 24, h: 10 },
{ x: 280, y: 490, w: 10, h: 16 },
{ x: 310, y: 490, w: 10, h: 16 },
// 顶保护
{ x: 280, y: 470, w: 30, h: 8 },
];
for (const b of brickDefs) {
bricks.push({ x: b.x, y: b.y, w: b.w, h: b.h, active: true });
}
}
function spawnBird() {
if (birdIndex >= 5) return; // 鸟用完了
bird = {
x: SLING_X,
y: SLING_Y - 10,
vx: 0,
vy: 0,
state: 'ready', // 'ready' | 'flying' | 'hit'
alive: true,
};
birdIndex++;
birdsLeft = 5 - birdIndex;
}
/** 发射小鸟 */
function launchBird(px, py) {
if (!bird || bird.state !== 'ready') return;
// 从弹弓口到拖拽点的反方向
const dx = SLING_X - px;
const dy = (SLING_Y - 10) - py;
const vx = dx * LAUNCH_FACTOR;
const vy = dy * LAUNCH_FACTOR;
bird.vx = vx;
bird.vy = vy;
bird.state = 'flying';
dragging = false;
dragStart = null;
dragEnd = null;
// 发射粒子 + 音效
spawnParticles(SLING_X, SLING_Y - 10, 30, { r: 1, g: 0.9, b: 0.3, a: 1 }, 0.6);
playSfx([1, 0.08, 600, , 0.1, 0.5, 0.5, 1.2]);
}
/** 碰撞检测与物理更新 */
function updatePhysics(dt) {
if (!bird || bird.state !== 'flying') return;
const b = bird;
// 重力
b.vy += GRAVITY * dt;
b.x += b.vx * dt;
b.y += b.vy * dt;
// 出界检测
if (b.y > 844 || b.x < -20 || b.x > 410 || b.y < -20) {
hitObstacle(b.x, b.y);
return;
}
// 碰撞检测: 猪
for (const pig of pigs) {
if (!pig.active) continue;
const dx = b.x - pig.x;
const dy = b.y - pig.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < BIRD_RADIUS + pig.r) {
pig.active = false;
score += 100;
// 爆炸粒子 + 音效
spawnParticles(pig.x, pig.y, 40, { r: 1, g: 0.4, b: 0.1, a: 1 }, 0.8);
playSfx([1, 0.9, 100, , 0.1, 0.4, 0.8, 1.5]);
addFloatingText(pig.x, pig.y, '+100');
hitObstacle(b.x, b.y);
return;
}
}
// 碰撞检测: 砖块
for (const brick of bricks) {
if (!brick.active) continue;
if (b.x > brick.x && b.x < brick.x + brick.w &&
b.y > brick.y && b.y < brick.y + brick.h) {
brick.active = false;
score += 10;
spawnParticles(brick.x + brick.w / 2, brick.y + brick.h / 2, 20, { r: 0.8, g: 0.7, b: 0.5, a: 1 }, 0.5);
playSfx([1, 0.2, 400, , 0.1, 0.3, 0.6, 1.3]);
addFloatingText(brick.x + brick.w / 2, brick.y, '+10');
hitObstacle(b.x, b.y);
return;
}
}
// 如果鸟落地上或太低,也视为落败
if (b.y > 800) {
hitObstacle(b.x, b.y);
}
}
/** 鸟撞击/消失处理 */
function hitObstacle(x, y) {
if (!bird || bird.state !== 'flying') return;
bird.state = 'hit';
bird.alive = false;
// 碰撞粒子
spawnParticles(x, y, 25, { r: 0.6, g: 0.8, b: 1, a: 1 }, 0.4);
playSfx([1, 0.05, 300, , 0.1, 0.2, 0.5, 1.0]);
// 延迟切换到下一只鸟
setTimeout(() => {
if (phase === 'gameover') return;
if (allPigsCleared()) {
phase = 'gameover';
result = 'win';
showResult();
return;
}
if (birdIndex >= 5) {
phase = 'gameover';
result = 'lose';
showResult();
return;
}
spawnBird();
}, 500);
}
function allPigsCleared() {
return pigs.every(p => !p.active);
}
function showResult() {
spawnParticles(195, 422, 50, { r: 1, g: 0.9, b: 0.2, a: 1 }, 1.5);
playSfx([1, 0.9, 800, , 0.1, 0.6, 0.9, 1.8]);
}
// ========== 输入处理 ==========
function onPointerDown(e) {
if (phase === 'gameover') {
// 检查是否点击重玩区域(固定: 中间偏下 "Tap to Restart"
if (e.x > 100 && e.x < 290 && e.y > 600 && e.y < 680) {
resetGameData();
phase = 'playing';
// 清除任何残留特效
explosions = [];
texts = [];
}
return;
}
if (!bird || bird.state !== 'ready') return;
// 检测点击是否在弹弓附近
const dx = e.x - SLING_X;
const dy = e.y - (SLING_Y - 10);
if (Math.sqrt(dx * dx + dy * dy) < 80) {
dragging = true;
dragStart = { x: SLING_X, y: SLING_Y - 10 };
dragEnd = { x: e.x, y: e.y };
}
}
function onPointerMove(e) {
if (!dragging) return;
dragEnd = { x: e.x, y: e.y };
}
function onPointerUp(e) {
if (!dragging) return;
dragging = false;
if (dragEnd) {
launchBird(dragEnd.x, dragEnd.y);
}
dragStart = null;
dragEnd = null;
}
// ========== 特效辅助 ==========
function spawnParticles(x, y, count, colorStart, life) {
const eng = ctx && ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x, y },
count: count,
speed: 100 + count * 2,
particleTime: life || 0.6,
colorStart: colorStart || { r: 1, g: 0.5, b: 0, a: 1 },
colorEnd: { r: 1, g: 0.2, b: 0, a: 0 },
sizeStart: 6,
sizeEnd: 2,
});
} catch (_) {}
}
}
function playSfx(params) {
const eng = ctx && ctx.getEngine();
if (eng && eng.audio && eng.audio.synth) {
try {
eng.audio.synth.synthSfx(params);
} catch (_) {}
}
}
function addFloatingText(x, y, text) {
texts.push({ x, y, text, born: now, duration: 800 });
}
// ========== 生命周期 ==========
function init(boot) {
ctx = boot.ctx;
seed = boot.seed || 0;
// 订阅输入
subPointers = ctx.getInput().on('pointerdown', onPointerDown);
subPointermove = ctx.getInput().on('pointermove', onPointerMove);
subPointerup = ctx.getInput().on('pointerup', onPointerUp);
// 先确保种子(可选)
if (ctx.random) {
ctx.random.reseed(seed);
}
// 初始化关卡
resetGameData();
phase = 'playing';
}
function update(dt) {
now = ctx.time.nowMs();
t = now / 1000;
if (phase !== 'playing') return;
// 更新物理
updatePhysics(dt);
// 检查是否所有猪消灭 -> win
if (allPigsCleared() && bird && bird.state !== 'flying') {
phase = 'gameover';
result = 'win';
showResult();
}
// 鸟用完且没有飞行中的鸟 => lose
if (birdIndex >= 5 && (!bird || bird.state === 'hit' || bird.state === 'ready')) {
phase = 'gameover';
result = 'lose';
showResult();
}
// 清理过期文字
texts = texts.filter(txt => now - txt.born < txt.duration);
// 清理过期爆炸效果(本示例仅依靠引擎粒子,此处保留一个简单列表用于自绘圈)
// 无额外自绘圈而是依赖引擎粒子和HUD
}
function render(g) {
// 背景
g.fillStyle = '#1a2b3c';
g.fillRect(0, 0, 390, 844);
// 绘制砖块
g.fillStyle = '#c8913e';
for (const brick of bricks) {
if (!brick.active) continue;
g.fillRect(brick.x, brick.y, brick.w, brick.h);
g.strokeStyle = '#5f3a1a';
g.lineWidth = 1;
g.strokeRect(brick.x, brick.y, brick.w, brick.h);
}
// 绘制猪
for (const pig of pigs) {
if (!pig.active) continue;
g.fillStyle = '#4CAF50';
g.beginPath();
g.arc(pig.x, pig.y, pig.r, 0, Math.PI * 2);
g.fill();
g.strokeStyle = '#2e7d32';
g.lineWidth = 2;
g.stroke();
// 鼻子
g.fillStyle = '#e2a0a0';
g.beginPath();
g.arc(pig.x - 4, pig.y - 2, 3, 0, Math.PI * 2);
g.fill();
}
// 绘制弹弓
g.strokeStyle = '#5c3a1e';
g.lineWidth = 6;
g.beginPath();
g.moveTo(60, 590);
g.lineTo(70, 540);
g.lineTo(80, 590);
g.stroke();
g.beginPath();
g.moveTo(70, 540);
g.lineTo(74, 600);
g.stroke();
// 绘制小鸟(如果 ready 或 flying
if (bird && (bird.state === 'ready' || bird.state === 'flying')) {
g.fillStyle = '#f44336';
g.beginPath();
g.arc(bird.x, bird.y, BIRD_RADIUS, 0, Math.PI * 2);
g.fill();
g.fillStyle = '#ffcdd2';
g.beginPath();
g.arc(bird.x - 3, bird.y - 3, 3, 0, Math.PI * 2);
g.fill();
// 嘴
g.fillStyle = '#ff9800';
g.beginPath();
g.moveTo(bird.x + 6, bird.y - 1);
g.lineTo(bird.x + 12, bird.y);
g.lineTo(bird.x + 6, bird.y + 2);
g.fill();
}
// 绘制拖拽线
if (dragging && dragStart && dragEnd) {
g.strokeStyle = 'rgba(255,255,255,0.5)';
g.lineWidth = 2;
g.setLineDash([6, 4]);
g.beginPath();
g.moveTo(dragStart.x, dragStart.y);
g.lineTo(dragEnd.x, dragEnd.y);
g.stroke();
g.setLineDash([]);
// 箭头指示方向反向
const dx = dragStart.x - dragEnd.x;
const dy = dragStart.y - dragEnd.y;
const len = Math.sqrt(dx*dx + dy*dy);
if (len > 5) {
const angle = Math.atan2(dy, dx);
const arrowLen = 12;
g.fillStyle = '#fff';
g.beginPath();
g.moveTo(dragStart.x + dx/4, dragStart.y + dy/4);
g.lineTo(dragStart.x + dx/4 - arrowLen * Math.cos(angle - 0.4), dragStart.y + dy/4 - arrowLen * Math.sin(angle - 0.4));
g.lineTo(dragStart.x + dx/4 - arrowLen * Math.cos(angle + 0.4), dragStart.y + dy/4 - arrowLen * Math.sin(angle + 0.4));
g.fill();
}
}
// HUD 文字 (顶部)
g.fillStyle = 'rgba(255,255,255,0.8)';
g.font = 'bold 20px monospace';
g.textAlign = 'left';
g.fillText('🐦 ×' + birdsLeft, 10, 30);
g.textAlign = 'right';
g.fillText('🏆 ' + score, 380, 30);
g.textAlign = 'center';
g.fillStyle = '#9f9';
g.font = '16px monospace';
const status = phase === 'gameover' ? (result === 'win' ? '✨ 全灭!' : '💀 失败') : (bird && bird.state === 'flying' ? '🎯 飞行中' : '📐 拖拽蓄力');
g.fillText(status, 195, 55);
// 飘字
for (const txt of texts) {
const age = (now - txt.born) / txt.duration;
const alpha = 1 - age;
g.globalAlpha = alpha;
g.fillStyle = '#ffdd44';
g.font = 'bold 18px monospace';
g.textAlign = 'center';
g.fillText(txt.text, txt.x, txt.y - 20 * age);
g.globalAlpha = 1;
}
// 游戏结束重玩提示
if (phase === 'gameover') {
g.fillStyle = 'rgba(0,0,0,0.4)';
g.fillRect(100, 600, 190, 60);
g.strokeStyle = '#fff';
g.lineWidth = 2;
g.strokeRect(100, 600, 190, 60);
g.fillStyle = '#fff';
g.font = 'bold 22px monospace';
g.textAlign = 'center';
g.fillText('点击重玩', 195, 638);
}
}
function destroy() {
try { subPointers && subPointers.cancel(); } catch(_) {}
try { subPointermove && subPointermove.cancel(); } catch(_) {}
try { subPointerup && subPointerup.cancel(); } catch(_) {}
subPointers = subPointermove = subPointerup = null;
explosions = [];
texts = [];
bricks = [];
pigs = [];
bird = null;
}
function _forensicsView() {
return {
seed: seed,
state: () => {
const remainingPigs = pigs.filter(p => p.active).map(p => ({ x: p.x, y: p.y }));
return {
phase: phase,
result: (phase === 'gameover' ? result : null),
score: score,
birdsLeft: birdsLeft,
targets: remainingPigs,
};
}
};
}
return {
init,
update,
render,
destroy,
_forensicsView
};
}

View File

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

View File

@ -0,0 +1,116 @@
{
"inputs": [
{
"t": "wait",
"ms": 700
},
{
"t": "drag",
"from": {
"x": 80,
"y": 600
},
"to": {
"x": 44,
"y": 668
},
"ms": 420
},
{
"t": "wait",
"ms": 1700
},
{
"t": "drag",
"from": {
"x": 80,
"y": 600
},
"to": {
"x": 40,
"y": 690
},
"ms": 420
},
{
"t": "wait",
"ms": 1700
},
{
"t": "drag",
"from": {
"x": 80,
"y": 600
},
"to": {
"x": 50,
"y": 655
},
"ms": 420
},
{
"t": "wait",
"ms": 1700
},
{
"t": "drag",
"from": {
"x": 80,
"y": 600
},
"to": {
"x": 38,
"y": 700
},
"ms": 420
},
{
"t": "wait",
"ms": 1700
},
{
"t": "drag",
"from": {
"x": 80,
"y": 600
},
"to": {
"x": 54,
"y": 648
},
"ms": 420
},
{
"t": "wait",
"ms": 1700
},
{
"t": "drag",
"from": {
"x": 80,
"y": 600
},
"to": {
"x": 42,
"y": 680
},
"ms": 420
},
{
"t": "wait",
"ms": 1700
}
],
"expectedEngineCallPrefixes": [
"particles.",
"audio.synth"
],
"assertAfterPlay": [
{
"path": "score",
"op": "increased",
"why": "盲态固定拖拽发射→偶中目标得分;若固定角度打不中(需读目标算拖拽矢量)则不升=暴露拖拽瞄准缺口"
}
],
"expectLatch": true
}

File diff suppressed because one or more lines are too long

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,106 @@
{
"gameId": "asteroids",
"url": "http://localhost:4320/games/_wg1-gen/asteroids/index.html",
"ts": null,
"guards": {
"A_boot": {
"pass": true
},
"C_frame": {
"pass": true,
"f0": 48,
"f1": 78,
"delta": 30
},
"I_control": {
"pass": true,
"skipped": "未声明 controlCheck向后兼容"
},
"D_render": {
"pass": true,
"bright": 66491,
"maxCh": 255
},
"E_live": {
"pass": true,
"distinctStates": 37,
"samples": 37
},
"B_uncaught": {
"pass": true,
"uncaught": []
},
"F_wiring": {
"pass": true,
"callCount": 27,
"sample": [
"particles.spawnEmitter",
"particles.spawnEmitter",
"particles.spawnEmitter",
"particles.spawnEmitter",
"particles.spawnEmitter",
"particles.spawnEmitter",
"particles.spawnEmitter",
"particles.spawnEmitter",
"particles.spawnEmitter",
"particles.spawnEmitter",
"particles.spawnEmitter",
"particles.spawnEmitter"
],
"expected": [
"particles.",
"audio.synth"
]
},
"G_input": {
"pass": true,
"inputHash": "d2d3585d",
"controlHash": "f13eb61f",
"atFrame": 901,
"ctrlFrame": 905
},
"H_progress": {
"pass": false,
"checks": [
{
"path": "score",
"op": "increased",
"before": 0,
"after": 0,
"pass": false,
"why": "循环旋转+推进+开火→击碎陨石得分上升;若高速子弹穿透(CCD缺)或打不中则不升=暴露 CCD 缺口"
}
],
"latch": {
"pass": true,
"dwelledMs": 600,
"after": "gameover",
"reason": ""
},
"state0": {
"phase": "playing",
"result": null,
"score": 0,
"asteroidsRemaining": 5,
"ship": {
"x": 195,
"y": 422,
"angle": -1.5707963267948966
}
},
"state1": {
"phase": "gameover",
"result": "lose",
"score": 0,
"asteroidsRemaining": 5,
"ship": {
"x": 152.8208837988882,
"y": 301.88159315392096,
"angle": -1.7707963267948967
}
}
}
},
"pass": false,
"notes": []
}

View File

@ -0,0 +1,354 @@
'use strict';
export default function createAsteroidsGame(opts) {
const o = opts || {};
let ctx = null;
let subHandlers = [];
// game state
let phase = 'booting';
let result = null;
let score = 0;
let ship = { x:0, y:0, vx:0, vy:0, angle:0, radius:15 };
let asteroids = [];
let bullets = [];
let lastFireTime = 0;
let keyState = {};
const FIRE_COOLDOWN = 200;
const BULLET_SPEED = 600;
const BULLET_LIFETIME = 2000;
const FRICTION = 0.99;
const SHIP_RADIUS = 15;
const BIG_ASTEROID_RADIUS = 30;
const SMALL_ASTEROID_RADIUS = 15;
const ASTEROID_SPEED_MIN = 40;
const ASTEROID_SPEED_MAX = 80;
const INITIAL_ASTEROIDS = 5;
const THRUST = 200;
const ROTATION_SPEED = 3.0;
// helpers
function wrapPos(p) {
if (p.x < 0) p.x += 390;
else if (p.x > 390) p.x -= 390;
if (p.y < 0) p.y += 844;
else if (p.y > 844) p.y -= 844;
}
function lineCircleIntersect(p1, p2, c, r) {
const dx = p2.x - p1.x, dy = p2.y - p1.y;
const fx = p1.x - c.x, fy = p1.y - c.y;
const a = dx*dx + dy*dy;
const b = 2*(fx*dx + fy*dy);
const c_ = fx*fx + fy*fy - r*r;
let disc = b*b - 4*a*c_;
if (disc < 0) return false;
disc = Math.sqrt(disc);
const t1 = (-b - disc) / (2*a);
const t2 = (-b + disc) / (2*a);
return (t1 >= 0 && t1 <= 1) || (t2 >= 0 && t2 <= 1);
}
function distance(ax, ay, bx, by) {
const dx = ax - bx, dy = ay - by;
return Math.sqrt(dx*dx + dy*dy);
}
function createAsteroid(x, y, radius, vx, vy) {
return { x, y, vx, vy, radius, isBig: radius >= BIG_ASTEROID_RADIUS };
}
function spawnInitialAsteroids() {
asteroids = [];
const rand = ctx.random;
for (let i = 0; i < INITIAL_ASTEROIDS; i++) {
let x, y;
do {
x = rand.range(0, 390);
y = rand.range(0, 844);
} while (distance(x,y,195,422) < 100);
const angle = rand.range(0, Math.PI*2);
const speed = rand.range(ASTEROID_SPEED_MIN, ASTEROID_SPEED_MAX);
const vx = Math.cos(angle)*speed;
const vy = Math.sin(angle)*speed;
asteroids.push(createAsteroid(x, y, BIG_ASTEROID_RADIUS, vx, vy));
}
}
function splitAsteroid(ast) {
const rand = ctx.random;
const angle1 = rand.range(0, Math.PI*2);
const angle2 = angle1 + Math.PI*2/3;
const speed = rand.range(ASTEROID_SPEED_MIN, ASTEROID_SPEED_MAX) * 1.2;
const vx1 = Math.cos(angle1)*speed;
const vy1 = Math.sin(angle1)*speed;
const vx2 = Math.cos(angle2)*speed;
const vy2 = Math.sin(angle2)*speed;
const a1 = createAsteroid(ast.x, ast.y, SMALL_ASTEROID_RADIUS, vx1, vy1);
const a2 = createAsteroid(ast.x, ast.y, SMALL_ASTEROID_RADIUS, vx2, vy2);
asteroids.push(a1, a2);
}
function spawnParticles(x, y, colorStart, colorEnd, count) {
const eng = ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x, y },
count: count || 20,
speed: 80,
particleTime: 0.4,
sizeStart: 6,
sizeEnd: 2,
colorStart: colorStart || { r:1, g:1, b:1, a:1 },
colorEnd: colorEnd || { r:0.5, g:0.5, b:0.5, a:0 }
});
} catch (_) {}
}
}
function playSfx(params) {
const eng = ctx.getEngine();
if (eng && eng.audio && eng.audio.synth) {
try { eng.audio.synth.synthSfx(params); } catch (_) {}
}
}
function fireBullet() {
const now = ctx.time.nowMs();
if (now - lastFireTime < FIRE_COOLDOWN) return;
lastFireTime = now;
const bx = ship.x + Math.cos(ship.angle) * (ship.radius + 5);
const by = ship.y + Math.sin(ship.angle) * (ship.radius + 5);
const bvx = Math.cos(ship.angle) * BULLET_SPEED + ship.vx;
const bvy = Math.sin(ship.angle) * BULLET_SPEED + ship.vy;
bullets.push({ x: bx, y: by, vx: bvx, vy: bvy, born: now, radius: 3 });
playSfx([1,0.02,600,,,0.1,,0.6]);
}
function resetGameState() {
ship.x = 195;
ship.y = 422;
ship.vx = 0;
ship.vy = 0;
ship.angle = -Math.PI/2;
spawnInitialAsteroids();
bullets = [];
score = 0;
lastFireTime = 0;
keyState = {};
}
function resetGame() {
resetGameState();
phase = 'playing';
result = null;
}
// lifecycle
function init(boot) {
ctx = boot.ctx;
const input = ctx.getInput();
const keydownSub = input.on('keydown', (e) => {
if (e.key) keyState[e.key] = true;
});
subHandlers.push(keydownSub);
const keyupSub = input.on('keyup', (e) => {
if (e.key) keyState[e.key] = false;
});
subHandlers.push(keyupSub);
const pointerSub = input.on('pointerdown', () => {
if (phase === 'gameover') resetGame();
});
subHandlers.push(pointerSub);
resetGameState();
phase = 'playing';
result = null;
}
function update(dt) {
if (phase === 'gameover') return;
const now = ctx.time.nowMs();
// ship rotation
if (keyState['ArrowLeft']) ship.angle -= ROTATION_SPEED * dt;
if (keyState['ArrowRight']) ship.angle += ROTATION_SPEED * dt;
// thrust
if (keyState['ArrowUp']) {
ship.vx += Math.cos(ship.angle) * THRUST * dt;
ship.vy += Math.sin(ship.angle) * THRUST * dt;
spawnParticles(
ship.x - Math.cos(ship.angle)*20,
ship.y - Math.sin(ship.angle)*20,
{ r:1, g:0.8, b:0.2, a:1 },
{ r:1, g:0.4, b:0, a:0 },
5
);
}
// fire
if (keyState['Space']) fireBullet();
// friction
ship.vx *= FRICTION;
ship.vy *= FRICTION;
// move ship
ship.x += ship.vx * dt;
ship.y += ship.vy * dt;
wrapPos(ship);
// move asteroids
for (const a of asteroids) {
a.x += a.vx * dt;
a.y += a.vy * dt;
wrapPos(a);
}
// update bullets & collision with asteroids (CCD)
for (let i = bullets.length-1; i >= 0; i--) {
const b = bullets[i];
const oldX = b.x, oldY = b.y;
b.x += b.vx * dt;
b.y += b.vy * dt;
// lifespan
if (now - b.born > BULLET_LIFETIME) {
bullets.splice(i, 1);
continue;
}
// off-screen remove (no wrap for simplicity)
if (b.x < 0 || b.x > 390 || b.y < 0 || b.y > 844) {
bullets.splice(i, 1);
continue;
}
let hit = false;
for (let j = asteroids.length-1; j >= 0; j--) {
const a = asteroids[j];
if (lineCircleIntersect({x:oldX, y:oldY}, {x:b.x, y:b.y}, {x:a.x, y:a.y}, a.radius)) {
hit = true;
score += 1;
spawnParticles(b.x, b.y, {r:1,g:0.9,b:0.5,a:1}, {r:1,g:0.3,b:0,a:0}, 30);
playSfx([1,0.1,880,,,0.2,,0.4]);
if (a.isBig) splitAsteroid(a);
asteroids.splice(j, 1);
break;
}
}
if (hit) {
bullets.splice(i, 1);
}
}
// ship vs asteroids
for (const a of asteroids) {
const dx = ship.x - a.x, dy = ship.y - a.y;
if (dx*dx + dy*dy < (ship.radius + a.radius) * (ship.radius + a.radius)) {
phase = 'gameover';
result = 'lose';
spawnParticles(ship.x, ship.y, {r:1,g:0.2,b:0.2,a:1}, {r:0.5,g:0,b:0,a:0}, 40);
playSfx([1,0.2,100,,,0.5,,1.5]);
return;
}
}
// win condition
if (asteroids.length === 0) {
phase = 'gameover';
result = 'win';
}
}
function render(g) {
// background
g.fillStyle = '#0a0a1a';
g.fillRect(0, 0, 390, 844);
// asteroids
for (const a of asteroids) {
g.fillStyle = a.isBig ? '#8b5e3c' : '#a0744a';
g.beginPath();
g.arc(a.x, a.y, a.radius, 0, Math.PI*2);
g.fill();
g.strokeStyle = '#d4a76a';
g.lineWidth = 2;
g.stroke();
}
// bullets
for (const b of bullets) {
g.fillStyle = '#ffff00';
g.beginPath();
g.arc(b.x, b.y, b.radius, 0, Math.PI*2);
g.fill();
}
// ship
g.save();
g.translate(ship.x, ship.y);
g.rotate(ship.angle);
g.fillStyle = phase === 'gameover' ? '#ff4444' : '#44aaff';
g.beginPath();
g.moveTo(20, 0);
g.lineTo(-15, -12);
g.lineTo(-10, 0);
g.lineTo(-15, 12);
g.closePath();
g.fill();
g.strokeStyle = '#ffffff';
g.lineWidth = 1.5;
g.stroke();
g.restore();
// HUD
g.fillStyle = '#ffffff';
g.font = 'bold 16px monospace';
g.fillText('Score: ' + score, 10, 30);
g.fillText('Asteroids: ' + asteroids.length, 10, 50);
g.fillText('Phase: ' + phase, 10, 70);
if (phase === 'gameover') {
g.fillStyle = result === 'win' ? '#00ff00' : '#ff0000';
g.font = 'bold 28px monospace';
g.textAlign = 'center';
g.fillText(result === 'win' ? 'YOU WIN!' : 'GAME OVER', 195, 400);
g.font = '16px monospace';
g.fillStyle = '#ffffff';
g.fillText('Tap to restart', 195, 430);
g.textAlign = 'start';
}
}
function destroy() {
for (const sub of subHandlers) {
try { sub.cancel(); } catch(_) {}
}
subHandlers = [];
asteroids = [];
bullets = [];
phase = 'booting';
result = null;
}
function _forensicsView() {
return {
seed: ctx && ctx.random ? ctx.random.seed : null,
state: () => ({
phase,
result,
score,
asteroidsRemaining: asteroids.length,
ship: { x: ship.x, y: ship.y, angle: ship.angle }
})
};
}
return { init, update, render, destroy, _forensicsView };
}

View File

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

View File

@ -0,0 +1,27 @@
{
"driver": {
"type": "key-cycle",
"keys": [
"ArrowLeft",
"ArrowUp",
"Space",
"ArrowRight",
"Space",
"Space"
],
"steps": 50,
"stepMs": 200
},
"expectedEngineCallPrefixes": [
"particles.",
"audio.synth"
],
"assertAfterPlay": [
{
"path": "score",
"op": "increased",
"why": "循环旋转+推进+开火→击碎陨石得分上升;若高速子弹穿透(CCD缺)或打不中则不升=暴露 CCD 缺口"
}
],
"expectLatch": true
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"inputs":{"src/core/plugin.js":{"bytes":44317,"imports":[],"format":"esm"},"src/plugins/runtime-probe/impl.js":{"bytes":16639,"imports":[],"format":"esm"},"host-dev/engine-caps.js":{"bytes":13897,"imports":[],"format":"esm"},"host-dev/engine-math.js":{"bytes":6155,"imports":[],"format":"esm"},"src/host/boot-game-host.js":{"bytes":27676,"imports":[{"path":"src/core/plugin.js","kind":"import-statement","original":"../core/plugin.js"},{"path":"src/plugins/runtime-probe/impl.js","kind":"import-statement","original":"../plugins/runtime-probe/impl.js"},{"path":"host-dev/engine-caps.js","kind":"import-statement","original":"../../host-dev/engine-caps.js"},{"path":"host-dev/engine-math.js","kind":"import-statement","original":"../../host-dev/engine-math.js"}],"format":"esm"},"src/plugins/collision/impl.js":{"bytes":24812,"imports":[],"format":"esm"},"src/plugins/physics-lite/impl.js":{"bytes":14507,"imports":[],"format":"esm"},"src/plugins/gamefeel/impl.js":{"bytes":21715,"imports":[],"format":"esm"},"src/plugins/particles-juice/impl.js":{"bytes":31830,"imports":[],"format":"esm"},"src/plugins/palette-post/impl.js":{"bytes":20240,"imports":[],"format":"esm"},"src/plugins/audio-music/impl.js":{"bytes":24659,"imports":[],"format":"esm"},"src/plugins/save-progress/impl.js":{"bytes":14608,"imports":[],"format":"esm"},"src/host/generic-host-config.js":{"bytes":7803,"imports":[{"path":"src/plugins/collision/impl.js","kind":"import-statement","original":"../plugins/collision/impl.js"},{"path":"src/plugins/physics-lite/impl.js","kind":"import-statement","original":"../plugins/physics-lite/impl.js"},{"path":"src/plugins/gamefeel/impl.js","kind":"import-statement","original":"../plugins/gamefeel/impl.js"},{"path":"src/plugins/particles-juice/impl.js","kind":"import-statement","original":"../plugins/particles-juice/impl.js"},{"path":"src/plugins/palette-post/impl.js","kind":"import-statement","original":"../plugins/palette-post/impl.js"},{"path":"src/plugins/audio-music/impl.js","kind":"import-statement","original":"../plugins/audio-music/impl.js"},{"path":"src/plugins/save-progress/impl.js","kind":"import-statement","original":"../plugins/save-progress/impl.js"}],"format":"esm"},"games/_wg1-gen/breakout/generated-factory.js":{"bytes":10687,"imports":[],"format":"esm"},"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytes":640153,"imports":[],"format":"esm"},"games/_wg1-gen/breakout/entry-bundle.js":{"bytes":6230,"imports":[{"path":"src/host/boot-game-host.js","kind":"import-statement","original":"../../../src/host/boot-game-host.js"},{"path":"src/host/generic-host-config.js","kind":"import-statement","original":"../../../src/host/generic-host-config.js"},{"path":"games/_wg1-gen/breakout/generated-factory.js","kind":"import-statement","original":"./generated-factory.js"},{"path":"node_modules/littlejsengine/dist/littlejs.esm.js","kind":"dynamic-import","original":"littlejsengine"}],"format":"esm"}},"outputs":{"games/_wg1-gen/breakout/bundle.iife.js":{"imports":[],"exports":[],"entryPoint":"games/_wg1-gen/breakout/entry-bundle.js","inputs":{"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytesInOutput":164172},"games/_wg1-gen/breakout/entry-bundle.js":{"bytesInOutput":507},"src/core/plugin.js":{"bytesInOutput":8989},"src/plugins/runtime-probe/impl.js":{"bytesInOutput":2794},"host-dev/engine-caps.js":{"bytesInOutput":1597},"host-dev/engine-math.js":{"bytesInOutput":498},"src/host/boot-game-host.js":{"bytesInOutput":5359},"src/plugins/collision/impl.js":{"bytesInOutput":1605},"src/plugins/physics-lite/impl.js":{"bytesInOutput":115},"src/plugins/gamefeel/impl.js":{"bytesInOutput":2533},"src/plugins/particles-juice/impl.js":{"bytesInOutput":4166},"src/plugins/palette-post/impl.js":{"bytesInOutput":4218},"src/plugins/audio-music/impl.js":{"bytesInOutput":6001},"src/plugins/save-progress/impl.js":{"bytesInOutput":3933},"src/host/generic-host-config.js":{"bytesInOutput":697},"games/_wg1-gen/breakout/generated-factory.js":{"bytesInOutput":4386}},"bytes":212141}}}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,138 @@
{
"gameId": "breakout",
"url": "http://localhost:4320/games/_wg1-gen/breakout/index.html",
"ts": null,
"guards": {
"A_boot": {
"pass": true
},
"C_frame": {
"pass": true,
"f0": 50,
"f1": 80,
"delta": 30
},
"I_control": {
"pass": true,
"paddlePath": "paddle.x",
"results": [
{
"tapX": 50,
"before": 195,
"after": 50.00000000001471,
"moved": -144.99999999998528,
"distToTarget": 1.4708234630234074e-11,
"pass": true
},
{
"tapX": 340,
"before": 50.00000000001471,
"after": 338.5467236151453,
"moved": 288.5467236151306,
"distToTarget": 1.4532763848546892,
"pass": true
}
]
},
"D_render": {
"pass": true,
"bright": 154309,
"maxCh": 255
},
"E_live": {
"pass": true,
"distinctStates": 24,
"samples": 24
},
"B_uncaught": {
"pass": true,
"uncaught": []
},
"F_wiring": {
"pass": true,
"callCount": 21,
"sample": [
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"audio.synth.synthSfx"
],
"expected": [
"particles.",
"audio.synth"
]
},
"G_input": {
"pass": true,
"inputHash": "0fd7f8bc",
"controlHash": "7dcc3da8",
"atFrame": 836,
"ctrlFrame": 838
},
"H_progress": {
"pass": true,
"checks": [
{
"path": "remaining",
"op": "decreased",
"before": 30,
"after": 29,
"pass": true,
"why": "适配性真玩(挡板自动接球)下击碎砖块→剩余下降;接不住/碰撞失效则不降"
}
],
"latch": {
"pass": true,
"dwelledMs": 600,
"after": "gameover",
"reason": ""
},
"state0": {
"phase": "playing",
"score": 0,
"remaining": 30,
"progress": 0,
"gameoverReason": "",
"ball": {
"x": 361.6493303574015,
"y": 530.5824887640108,
"vx": 199.9791964288825,
"vy": -311.3010134831855
},
"paddle": {
"x": 195,
"y": 810,
"w": 70
}
},
"state1": {
"phase": "gameover",
"score": 1,
"remaining": 29,
"progress": 0.033333333333333326,
"gameoverReason": "ball_lost",
"ball": {
"x": 352.5509849051085,
"y": 839.7168663631734,
"vx": 272.80340913561224,
"vy": 249.9565961601889
},
"paddle": {
"x": 288.28229534102184,
"y": 810,
"w": 70
}
}
}
},
"pass": true,
"notes": []
}

View File

@ -0,0 +1,385 @@
'use strict';
export default function createGame(opts) {
const o = opts || {};
let ctx = null;
let boot = null;
let gameState = null; // { phase, score, remaining, ball, paddle, bricks, targetX, ... }
let inputSubs = [];
let nowMs = 0;
// 砖块配置
const COLS = 6;
const ROWS = 5;
const BRICK_W = 62;
const BRICK_H = 20;
const BRICK_GAP = 3;
const BOARD_Y = 50;
const PADDLE_W = 70;
const PADDLE_H = 16;
const PADDLE_Y = 810;
const BALL_R = 7;
const BALL_SPEED = 370; // px/s
const MAX_DEFLECT_ANGLE = 70 * Math.PI / 180;
function init(bootData) {
boot = bootData;
ctx = boot.ctx;
nowMs = ctx.time.nowMs();
// 初始化砖块
const bricks = [];
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
const health = ctx.random.range(0, 1) > 0.7 ? 2 : 1; // 30% 需要2次
bricks.push({
x: col * (BRICK_W + BRICK_GAP) + 4,
y: BOARD_Y + row * (BRICK_H + BRICK_GAP),
w: BRICK_W,
h: BRICK_H,
health: health,
maxHealth: health
});
}
}
// 挡板(坐标为中心)
const paddle = {
x: 195,
y: PADDLE_Y,
w: PADDLE_W,
h: PADDLE_H
};
// 球(中心)
const ball = {
x: 195,
y: PADDLE_Y - 20,
vx: 0,
vy: 0
};
// 初始球方向随机(向上偏)
const angle = ctx.random.range(-45, 45) * Math.PI / 180;
ball.vx = BALL_SPEED * Math.sin(angle);
ball.vy = -BALL_SPEED * Math.cos(angle);
gameState = {
phase: 'playing',
score: 0,
remaining: bricks.length,
bricks: bricks,
paddle: paddle,
ball: ball,
targetX: 195,
tapToRestart: false,
gameoverReason: ''
};
// 输入订阅
const input = ctx.getInput();
if (input) {
const subDown = input.on('pointerdown', onPointer);
const subMove = input.on('pointermove', onPointer);
const subKey = input.on('keydown', onKey);
inputSubs = [subDown, subMove, subKey];
}
// 尝试首次触发引擎能力(常驻粒子)
const eng = ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x: 195, y: 300 },
emitRate: 15,
emitTime: 0,
particleTime: 1.0,
speed: 60,
colorStart: { r: 1, g: 0.8, b: 0.2, a: 1 },
colorEnd: { r: 1, g: 0.2, b: 0, a: 0 }
});
} catch (_) {}
}
}
function onPointer(e) {
if (!gameState) return;
if (e.type === 'pointerdown') {
// 检查是否gameover后点击重玩
if (gameState.phase === 'gameover') {
restartGame();
return;
}
}
// 更新目标位置(逻辑像素坐标)
gameState.targetX = Math.max(0, Math.min(390, e.x || 0));
}
function onKey(e) {
if (!gameState) return;
if (e.type === 'keydown' && e.key === ' ') {
if (gameState.phase === 'gameover') {
restartGame();
}
}
}
function restartGame() {
// 销毁当前订阅
destroySubs();
// 重新初始化
init(boot);
}
function update(dt) {
// 每个逻辑步刷新时间
nowMs = ctx.time.nowMs();
if (!gameState || gameState.phase !== 'playing') return;
const paddle = gameState.paddle;
const ball = gameState.ball;
const bricks = gameState.bricks;
// 挡板平滑跟随目标
paddle.x += (gameState.targetX - paddle.x) * 0.25;
paddle.x = Math.max(paddle.w / 2, Math.min(390 - paddle.w / 2, paddle.x));
// 球移动
ball.x += ball.vx * dt;
ball.y += ball.vy * dt;
// 左右墙反弹(考虑球半径)
if (ball.x - BALL_R < 0) {
ball.x = BALL_R;
ball.vx = -ball.vx;
playHitSound();
}
if (ball.x + BALL_R > 390) {
ball.x = 390 - BALL_R;
ball.vx = -ball.vx;
playHitSound();
}
// 顶墙反弹
if (ball.y - BALL_R < 0) {
ball.y = BALL_R;
ball.vy = -ball.vy;
playHitSound();
}
// 底部失败
if (ball.y + BALL_R > 844) {
gameState.phase = 'gameover';
gameState.gameoverReason = 'ball_lost';
// 爆炸粒子
spawnBurst(ball.x, ball.y, { r: 1, g: 0.2, b: 0.2, a: 1 });
playHitSound();
return;
}
// 砖块碰撞(按顺序,每块一次)
for (let i = bricks.length - 1; i >= 0; i--) {
const b = bricks[i];
// 检测矩形与圆的碰撞简化为AABB + 圆球)
const closestX = Math.max(b.x, Math.min(ball.x, b.x + b.w));
const closestY = Math.max(b.y, Math.min(ball.y, b.y + b.h));
const dx = ball.x - closestX;
const dy = ball.y - closestY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < BALL_R) {
// 确定碰撞方向(主要从哪面进入)
const overlapX = (b.w / 2 + BALL_R) - Math.abs(ball.x - (b.x + b.w/2));
const overlapY = (b.h / 2 + BALL_R) - Math.abs(ball.y - (b.y + b.h/2));
if (overlapX < overlapY) {
ball.vx = -ball.vx;
} else {
ball.vy = -ball.vy;
}
// 将球推出砖块
if (overlapX < overlapY) {
if (ball.x < b.x + b.w/2) ball.x = b.x - BALL_R;
else ball.x = b.x + b.w + BALL_R;
} else {
if (ball.y < b.y + b.h/2) ball.y = b.y - BALL_R;
else ball.y = b.y + b.h + BALL_R;
}
// 砖块耐久减1
b.health--;
if (b.health <= 0) {
bricks.splice(i, 1);
gameState.score++;
gameState.remaining = bricks.length;
// 粒子爆发
spawnBurst(b.x + b.w/2, b.y + b.h/2, { r: 0.2, g: 0.9, b: 1, a: 1 });
// 音效
playScoreSound();
} else {
// 击中但未碎,给个小型粒子
spawnBurst(b.x + b.w/2, b.y + b.h/2, { r: 0.9, g: 0.6, b: 0.2, a: 0.8 }, 8);
playHitSound();
}
break; // 每帧只处理一个砖块碰撞,避免多个同时
}
}
// 挡板碰撞
const paddleLeft = paddle.x - paddle.w/2;
const paddleRight = paddle.x + paddle.w/2;
const paddleTop = paddle.y - paddle.h/2;
const paddleBottom = paddle.y + paddle.h/2;
if (ball.y + BALL_R >= paddleTop && ball.y - BALL_R <= paddleBottom &&
ball.x + BALL_R >= paddleLeft && ball.x - BALL_R <= paddleRight) {
// 击中挡板,计算反弹角度
const offset = (ball.x - paddle.x) / (paddle.w/2);
const clampedOffset = Math.max(-1, Math.min(1, offset)); // -1~1
const angle = clampedOffset * MAX_DEFLECT_ANGLE;
ball.vx = BALL_SPEED * Math.sin(angle);
ball.vy = -BALL_SPEED * Math.cos(angle);
// 将球置于挡板上沿
ball.y = paddleTop - BALL_R;
playHitSound();
// 粒子
spawnBurst(ball.x, ball.y, { r: 0.3, g: 0.8, b: 0.3, a: 1 }, 12);
}
// 检查通关
if (gameState.bricks.length === 0) {
gameState.phase = 'gameover';
gameState.gameoverReason = 'cleared_all';
// 胜利粒子
spawnBurst(195, 400, { r: 1, g: 1, b: 0.2, a: 1 }, 30);
playScoreSound();
}
}
function spawnBurst(x, y, color, count) {
const eng = ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x, y },
count: count || 22,
speed: 80,
particleTime: 0.5,
sizeStart: 10,
sizeEnd: 2,
colorStart: color || { r: 1, g: 0.8, b: 0.2, a: 1 },
colorEnd: { r: 0.2, g: 0.5, b: 1, a: 0 }
});
} catch (_) {}
}
}
function playHitSound() {
const eng = ctx.getEngine();
if (eng && eng.audio && eng.audio.synth) {
try {
eng.audio.synth.synthSfx([0.5, 0.05, 440, , , 0.1, , 1.2]);
} catch (_) {}
}
}
function playScoreSound() {
const eng = ctx.getEngine();
if (eng && eng.audio && eng.audio.synth) {
try {
eng.audio.synth.synthSfx([1, 0.1, 880, , , 0.15, , 1.8]);
} catch (_) {}
}
}
function render(g) {
if (!gameState) return;
const state = gameState;
const paddle = state.paddle;
const ball = state.ball;
const bricks = state.bricks;
// 背景
g.fillStyle = '#121212';
g.fillRect(0, 0, 390, 844);
// 砖块
for (const b of bricks) {
const hpRatio = b.health / b.maxHealth;
let color;
if (hpRatio > 0.66) color = '#00cc66';
else if (hpRatio > 0.33) color = '#ffaa00';
else color = '#ff3333';
g.fillStyle = color;
g.fillRect(b.x, b.y, b.w, b.h);
g.strokeStyle = '#222';
g.lineWidth = 1;
g.strokeRect(b.x, b.y, b.w, b.h);
}
// 挡板
g.fillStyle = '#43c6e6';
g.fillRect(paddle.x - paddle.w/2, paddle.y - paddle.h/2, paddle.w, paddle.h);
// 球
g.fillStyle = '#ffcc00';
g.beginPath();
g.arc(ball.x, ball.y, BALL_R, 0, Math.PI * 2);
g.fill();
// HUD
g.fillStyle = '#cccccc';
g.font = '18px monospace';
g.fillText('Score: ' + state.score + ' Remaining: ' + state.remaining, 10, 30);
// 游戏结束覆盖
if (state.phase === 'gameover') {
g.fillStyle = 'rgba(0,0,0,0.6)';
g.fillRect(0, 0, 390, 844);
g.fillStyle = '#ffffff';
g.font = 'bold 36px sans-serif';
g.textAlign = 'center';
if (state.gameoverReason === 'cleared_all') {
g.fillText('YOU WIN!', 195, 380);
} else {
g.fillText('GAME OVER', 195, 380);
}
g.font = '18px sans-serif';
g.fillText('Tap to restart', 195, 440);
g.textAlign = 'start';
}
}
function destroySubs() {
inputSubs.forEach(sub => {
try { sub.cancel(); } catch(_) {}
});
inputSubs = [];
}
function destroy() {
destroySubs();
gameState = null;
boot = null;
ctx = null;
}
function _forensicsView() {
return {
seed: o.seed || 0,
state: function() {
const state = gameState;
if (!state) return { phase: 'unknown', score: 0, remaining: 0, ball: {x:0,y:0,vx:0,vy:0}, paddle: {x:0,y:0,w:0} };
return {
phase: state.phase,
score: state.score,
remaining: state.remaining,
progress: state.remaining === 0 ? 1 : (1 - state.remaining / (COLS*ROWS)),
gameoverReason: state.gameoverReason || '',
ball: { x: state.ball.x, y: state.ball.y, vx: state.ball.vx, vy: state.ball.vy },
paddle: { x: state.paddle.x, y: state.paddle.y, w: state.paddle.w }
};
}
};
}
return { init, update, render, destroy, _forensicsView };
}

View File

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

View File

@ -0,0 +1,33 @@
{
"controlCheck": {
"paddlePath": "paddle.x",
"paddleY": 800,
"tapXs": [
50,
340
],
"tapsPerTarget": 6,
"moveMin": 25,
"tolerance": 70,
"why": "挡板须平滑逼近触点 x逮「一格一跳」/卡死:连点同一 x 后挡板应明显移动且最终靠近该 x"
},
"driver": {
"type": "paddle-intercept",
"ballPath": "ball.x",
"paddleY": 800,
"steps": 60,
"stepMs": 130
},
"expectedEngineCallPrefixes": [
"particles.",
"audio.synth"
],
"assertAfterPlay": [
{
"path": "remaining",
"op": "decreased",
"why": "适配性真玩(挡板自动接球)下击碎砖块→剩余下降;接不住/碰撞失效则不降"
}
],
"expectLatch": true
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"inputs":{"src/core/plugin.js":{"bytes":44317,"imports":[],"format":"esm"},"src/plugins/runtime-probe/impl.js":{"bytes":16639,"imports":[],"format":"esm"},"host-dev/engine-caps.js":{"bytes":13897,"imports":[],"format":"esm"},"host-dev/engine-math.js":{"bytes":6155,"imports":[],"format":"esm"},"src/host/boot-game-host.js":{"bytes":27676,"imports":[{"path":"src/core/plugin.js","kind":"import-statement","original":"../core/plugin.js"},{"path":"src/plugins/runtime-probe/impl.js","kind":"import-statement","original":"../plugins/runtime-probe/impl.js"},{"path":"host-dev/engine-caps.js","kind":"import-statement","original":"../../host-dev/engine-caps.js"},{"path":"host-dev/engine-math.js","kind":"import-statement","original":"../../host-dev/engine-math.js"}],"format":"esm"},"src/plugins/collision/impl.js":{"bytes":24812,"imports":[],"format":"esm"},"src/plugins/physics-lite/impl.js":{"bytes":14507,"imports":[],"format":"esm"},"src/plugins/gamefeel/impl.js":{"bytes":21715,"imports":[],"format":"esm"},"src/plugins/particles-juice/impl.js":{"bytes":31830,"imports":[],"format":"esm"},"src/plugins/palette-post/impl.js":{"bytes":20240,"imports":[],"format":"esm"},"src/plugins/audio-music/impl.js":{"bytes":24659,"imports":[],"format":"esm"},"src/plugins/save-progress/impl.js":{"bytes":14608,"imports":[],"format":"esm"},"src/host/generic-host-config.js":{"bytes":7803,"imports":[{"path":"src/plugins/collision/impl.js","kind":"import-statement","original":"../plugins/collision/impl.js"},{"path":"src/plugins/physics-lite/impl.js","kind":"import-statement","original":"../plugins/physics-lite/impl.js"},{"path":"src/plugins/gamefeel/impl.js","kind":"import-statement","original":"../plugins/gamefeel/impl.js"},{"path":"src/plugins/particles-juice/impl.js","kind":"import-statement","original":"../plugins/particles-juice/impl.js"},{"path":"src/plugins/palette-post/impl.js","kind":"import-statement","original":"../plugins/palette-post/impl.js"},{"path":"src/plugins/audio-music/impl.js","kind":"import-statement","original":"../plugins/audio-music/impl.js"},{"path":"src/plugins/save-progress/impl.js","kind":"import-statement","original":"../plugins/save-progress/impl.js"}],"format":"esm"},"games/_wg1-gen/doodle/generated-factory.js":{"bytes":11886,"imports":[],"format":"esm"},"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytes":640153,"imports":[],"format":"esm"},"games/_wg1-gen/doodle/entry-bundle.js":{"bytes":6230,"imports":[{"path":"src/host/boot-game-host.js","kind":"import-statement","original":"../../../src/host/boot-game-host.js"},{"path":"src/host/generic-host-config.js","kind":"import-statement","original":"../../../src/host/generic-host-config.js"},{"path":"games/_wg1-gen/doodle/generated-factory.js","kind":"import-statement","original":"./generated-factory.js"},{"path":"node_modules/littlejsengine/dist/littlejs.esm.js","kind":"dynamic-import","original":"littlejsengine"}],"format":"esm"}},"outputs":{"games/_wg1-gen/doodle/bundle.iife.js":{"imports":[],"exports":[],"entryPoint":"games/_wg1-gen/doodle/entry-bundle.js","inputs":{"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytesInOutput":164205},"games/_wg1-gen/doodle/entry-bundle.js":{"bytesInOutput":507},"src/core/plugin.js":{"bytesInOutput":8989},"src/plugins/runtime-probe/impl.js":{"bytesInOutput":2794},"host-dev/engine-caps.js":{"bytesInOutput":1597},"host-dev/engine-math.js":{"bytesInOutput":498},"src/host/boot-game-host.js":{"bytesInOutput":5359},"src/plugins/collision/impl.js":{"bytesInOutput":1605},"src/plugins/physics-lite/impl.js":{"bytesInOutput":115},"src/plugins/gamefeel/impl.js":{"bytesInOutput":2533},"src/plugins/particles-juice/impl.js":{"bytesInOutput":4156},"src/plugins/palette-post/impl.js":{"bytesInOutput":4218},"src/plugins/audio-music/impl.js":{"bytesInOutput":6001},"src/plugins/save-progress/impl.js":{"bytesInOutput":3933},"src/host/generic-host-config.js":{"bytesInOutput":697},"games/_wg1-gen/doodle/generated-factory.js":{"bytesInOutput":3869}},"bytes":211647}}}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,112 @@
{
"gameId": "doodle",
"url": "http://localhost:4320/games/_wg1-gen/doodle/index.html",
"ts": null,
"guards": {
"A_boot": {
"pass": true
},
"C_frame": {
"pass": true,
"f0": 47,
"f1": 77,
"delta": 30
},
"I_control": {
"pass": true,
"skipped": "未声明 controlCheck向后兼容"
},
"D_render": {
"pass": true,
"bright": 65074,
"maxCh": 255
},
"E_live": {
"pass": true,
"distinctStates": 13,
"samples": 13
},
"B_uncaught": {
"pass": true,
"uncaught": []
},
"F_wiring": {
"pass": true,
"callCount": 13,
"sample": [
"particles.spawnEmitter",
"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",
"particles.spawnEmitter"
],
"expected": [
"particles.",
"audio.synth"
]
},
"G_input": {
"pass": true,
"inputHash": "229f8947",
"controlHash": "ebd062df",
"atFrame": 350,
"ctrlFrame": 355
},
"H_progress": {
"pass": true,
"checks": [
{
"path": "score",
"op": "increased",
"before": 259,
"after": 1003,
"pass": true,
"why": "适配性真玩(读 bird.x/nextPlatform.x 左右趋近接台)→高度分上升;落不住或计分语义错则不升"
}
],
"latch": {
"pass": true,
"dwelledMs": 600,
"after": "gameover",
"reason": ""
},
"state0": {
"phase": "playing",
"result": null,
"score": 259,
"bird": {
"x": 195,
"y": 657,
"vy": 120
},
"nextPlatform": {
"x": 38.45870539546013,
"y": 622.021542112343
}
},
"state1": {
"phase": "gameover",
"result": "lose",
"score": 1003,
"bird": {
"x": 174.1666666666667,
"y": 837,
"vy": 660
},
"nextPlatform": {
"x": 315.5195666849613,
"y": 835.9829828934744
}
}
}
},
"pass": true,
"notes": []
}

View File

@ -0,0 +1,388 @@
'use strict';
export default function createDoodleJumpGame(opts) {
const o = opts || {};
let ctx = null;
let subPointerDown = null;
let subPointerUp = null;
let subPointerMove = null;
let leftPressed = false;
let rightPressed = false;
// 游戏常数
const SCREEN_W = 390;
const SCREEN_H = 844;
const GRAVITY = 1200;
const JUMP_VY = -800;
const HORIZONTAL_SPEED = 250;
const ROLE_RADIUS = 10;
const PLATFORM_W = 70;
const PLATFORM_H = 10;
const CAMERA_FIXED_Y = 650;
const PLATFORM_SPACING_MIN = 80;
const PLATFORM_SPACING_MAX = 130;
const PLATFORM_X_OFFSET_MAX = 150;
// 游戏状态
let phase = 'booting';
let result = null;
let score = 0;
let role = null;
let platforms = [];
let cameraY = 0;
let highestWorldY = 0;
let initialWorldY = 0;
const FIXED_STEP = 1 / 60; // 固定 dt
let nowMs = 0;
// ---------- 内部重置函数 ----------
function resetGame() {
const spawnFirst = !role; // 首次调用时还没创建 role
if (!role) {
role = {
worldX: SCREEN_W / 2,
worldY: 0,
vy: 0,
vx: 0,
};
}
// 重置角色位置:初始站在第一个平台上
role.worldX = SCREEN_W / 2;
role.worldY = SCREEN_H - 200; // 初始靠近底部,但下方留空间
role.vy = 0;
role.vx = 0;
initialWorldY = role.worldY;
highestWorldY = role.worldY;
cameraY = CAMERA_FIXED_Y - role.worldY; // 使得角色屏幕 y = CAMERA_FIXED_Y
platforms = [];
leftPressed = false;
rightPressed = false;
score = 0;
phase = 'playing';
result = null;
// 生成初始平台:角色脚下平台 + 上方若干平台
// 第一个平台:位于角色脚下(角色底部刚好踩到平台顶部)
const footY = role.worldY + ROLE_RADIUS;
const firstPlat = {
x: role.worldX - PLATFORM_W / 2,
y: footY,
w: PLATFORM_W,
h: PLATFORM_H,
};
platforms.push(firstPlat);
// 向上生成 6 个平台
let curY = footY;
for (let i = 0; i < 6; i++) {
const spacing = ctx.random.range(PLATFORM_SPACING_MIN, PLATFORM_SPACING_MAX);
curY -= spacing;
if (curY < 0) curY = 0; // 防止生成到不可到达的顶端
const nextX = ctx.random.range(0, SCREEN_W - PLATFORM_W);
platforms.push({
x: nextX,
y: curY,
w: PLATFORM_W,
h: PLATFORM_H,
});
}
// 按 y 排序从高到低y 小在上)
platforms.sort((a, b) => a.y - b.y);
}
// ---------- 输入处理 ----------
function onPointerDown(e) {
if (phase === 'gameover') {
// 只处理重玩按钮区域:屏幕中央下方约 (195, 600~700)
const bx = 195, by = 650, bw = 160, bh = 50;
if (e.x >= bx - bw / 2 && e.x <= bx + bw / 2 &&
e.y >= by - bh / 2 && e.y <= by + bh / 2) {
resetGame();
}
return;
}
leftPressed = e.x < SCREEN_W / 2;
rightPressed = e.x >= SCREEN_W / 2;
}
function onPointerUp(e) {
if (phase === 'gameover') return;
// 松开时清除对应方向(简单起见,松开任意手指清除所有方向)
leftPressed = false;
rightPressed = false;
}
function onPointerMove(e) {
if (phase === 'gameover') return;
leftPressed = e.x < SCREEN_W / 2;
rightPressed = e.x >= SCREEN_W / 2;
}
// ---------- 生成平台(按需向上补充) ----------
function ensurePlatformsAbove() {
// 确保上方有足够覆盖到角色上方一个屏幕高度的平台
const topScreenWorld = role.worldY - SCREEN_H; // 屏幕上边缘对应的世界 y
// 找出当前最高平台y 最小)
let smallestY = 0;
if (platforms.length > 0) {
smallestY = platforms[0].y;
for (let i = 1; i < platforms.length; i++) {
if (platforms[i].y < smallestY) smallestY = platforms[i].y;
}
}
// 如果最高平台仍不够高(大于目标顶部),继续生成
while (smallestY > topScreenWorld - 200) {
const spacing = ctx.random.range(PLATFORM_SPACING_MIN, PLATFORM_SPACING_MAX);
const newY = smallestY - spacing;
const newX = ctx.random.range(0, SCREEN_W - PLATFORM_W);
platforms.push({
x: newX,
y: newY,
w: PLATFORM_W,
h: PLATFORM_H,
});
smallestY = newY;
}
}
// ---------- 移除屏幕下的平台 ----------
function removePlatformsBelow() {
const bottomWorld = role.worldY + SCREEN_H + 50;
platforms = platforms.filter(p => p.y < bottomWorld);
}
// ---------- 碰撞检测与反弹 ----------
function checkPlatformCollision(dt) {
if (role.vy <= 0) return false; // 只有下落才可能碰撞
const oldBottomY = role.worldY - role.vy * dt + ROLE_RADIUS;
let hit = false;
for (const p of platforms) {
// 粗略过滤:角色底部在平台上方或内部
const roleBottom = role.worldY + ROLE_RADIUS;
if (roleBottom >= p.y && roleBottom <= p.y + p.h + 30) {
// 水平检测
if (role.worldX + ROLE_RADIUS > p.x && role.worldX - ROLE_RADIUS < p.x + p.w) {
// 上一帧底部在平台顶部以上
if (oldBottomY <= p.y && roleBottom >= p.y) {
// 反弹
role.vy = JUMP_VY;
// 将角色位置置于平台顶部(避免卡入)
role.worldY = p.y - ROLE_RADIUS;
hit = true;
break;
}
}
}
}
return hit;
}
// ---------- 暴露给宿主的生命周期 ----------
function init(boot) {
ctx = boot.ctx;
const input = ctx.getInput();
subPointerDown = input.on('pointerdown', onPointerDown);
subPointerUp = input.on('pointerup', onPointerUp);
subPointerMove = input.on('pointermove', onPointerMove);
resetGame(); // 初始化状态
// 引擎预热:发射一个常驻粒子(软启动)
const eng = ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x: 195, y: 300 },
emitRate: 20,
emitTime: 0,
particleTime: 0.6,
speed: 50,
colorStart: { r: 1, g: 0.8, b: 0.3, a: 1 },
colorEnd: { r: 1, g: 0.2, b: 0, a: 0 },
});
} catch (_) {}
}
}
function update(dt) {
nowMs = ctx.time.nowMs();
if (phase !== 'playing') return; // 只有在游戏进行中才更新逻辑
// 检查失败
const screenY = role.worldY + cameraY;
if (screenY + ROLE_RADIUS > SCREEN_H) {
phase = 'gameover';
result = 'lose';
// 失败粒子爆发 + 音效
const eng = ctx.getEngine();
if (eng) {
if (eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x: role.worldX, y: role.worldY },
count: 40,
speed: 200,
particleTime: 0.7,
colorStart: { r: 1, g: 0.2, b: 0.2, a: 1 },
colorEnd: { r: 0.8, g: 0, b: 0, a: 0 },
});
} catch (_) {}
}
if (eng.audio && eng.audio.synth) {
try {
eng.audio.synth.synthSfx([1, 0.1, 150, , , 0.2, , 1.5, , , , , 0.5]);
} catch (_) {}
}
}
return;
}
// 水平移动
if (leftPressed) {
role.vx = -HORIZONTAL_SPEED;
} else if (rightPressed) {
role.vx = HORIZONTAL_SPEED;
} else {
role.vx = 0;
}
// 边界环绕
role.worldX += role.vx * dt;
if (role.worldX < -ROLE_RADIUS) role.worldX = SCREEN_W + ROLE_RADIUS;
if (role.worldX > SCREEN_W + ROLE_RADIUS) role.worldX = -ROLE_RADIUS;
// 重力
role.vy += GRAVITY * dt;
// 垂直移动
role.worldY += role.vy * dt;
// 碰撞检测
const hit = checkPlatformCollision(dt);
if (hit) {
// 踩中平台,播放反馈
const eng = ctx.getEngine();
if (eng) {
if (eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x: role.worldX, y: role.worldY + ROLE_RADIUS },
count: 20,
speed: 100,
particleTime: 0.4,
colorStart: { r: 0.2, g: 1, b: 0.5, a: 1 },
colorEnd: { r: 0, g: 0.7, b: 0.2, a: 0 },
});
} catch (_) {}
}
if (eng.audio && eng.audio.synth) {
try {
eng.audio.synth.synthSfx([1, 0.05, 300, , , 0.15, , 1.3]);
} catch (_) {}
}
}
}
// 更新相机(只上移不下移)
cameraY = Math.max(cameraY, CAMERA_FIXED_Y - role.worldY);
// 更新最高点与分数
if (role.worldY < highestWorldY) {
highestWorldY = role.worldY;
score = Math.floor(initialWorldY - highestWorldY);
}
// 平台管理
ensurePlatformsAbove();
removePlatformsBelow();
}
function render(g) {
// 清空背景
g.fillStyle = '#0d1b2a';
g.fillRect(0, 0, SCREEN_W, SCREEN_H);
// 绘制平台(所有平台的世界坐标 -> 屏幕坐标 = world + cameraY
for (const p of platforms) {
const sx = p.x;
const sy = p.y + cameraY;
if (sy + p.h < 0 || sy > SCREEN_H) continue; // 裁剪
g.fillStyle = '#e0e1dd';
g.fillRect(sx, sy, p.w, p.h);
// 平台高光线
g.fillStyle = '#778da9';
g.fillRect(sx, sy, p.w, 2);
}
// 绘制角色
const roleScreenX = role ? role.worldX : SCREEN_W / 2;
const roleScreenY = role ? role.worldY + cameraY : CAMERA_FIXED_Y;
if (role) {
g.fillStyle = '#f4a261';
g.beginPath();
g.arc(roleScreenX, roleScreenY, ROLE_RADIUS, 0, Math.PI * 2);
g.fill();
}
// 绘制 HUD顶部文字
g.fillStyle = '#ffffff';
g.font = 'bold 16px monospace';
g.textAlign = 'left';
g.fillText('Height: ' + score, 12, 30);
if (phase === 'gameover') {
g.textAlign = 'center';
g.font = 'bold 28px monospace';
g.fillStyle = '#e63946';
g.fillText('GAME OVER', SCREEN_W / 2, 400);
g.font = 'bold 18px monospace';
g.fillStyle = '#a8dadc';
g.fillText('Score: ' + score, SCREEN_W / 2, 460);
g.font = '20px monospace';
g.fillStyle = '#f1faee';
// 重玩按钮
const bx = 195, by = 650, bw = 160, bh = 50;
g.fillStyle = '#457b9d';
g.fillRect(bx - bw / 2, by - bh / 2, bw, bh);
g.fillStyle = '#1d3557';
g.font = 'bold 18px monospace';
g.fillText('Tap to Retry', bx, by + 6);
}
}
function destroy() {
try { subPointerDown && subPointerDown.cancel(); } catch (_) {}
try { subPointerUp && subPointerUp.cancel(); } catch (_) {}
try { subPointerMove && subPointerMove.cancel(); } catch (_) {}
subPointerDown = null;
subPointerUp = null;
subPointerMove = null;
platforms = [];
role = null;
}
// ---------- 可测性视图 ----------
function _forensicsView() {
return {
seed: o.seed || 0,
state: () => {
let nextPlatform = null;
if (role && platforms.length > 0) {
// 寻找角色正上方最近y 小于角色 worldY 且最小 y的平台
let best = null;
let bestY = -Infinity;
for (const p of platforms) {
if (p.y < role.worldY && p.y > bestY) {
best = p;
bestY = p.y;
}
}
if (best) {
nextPlatform = {
x: best.x + best.w / 2,
y: best.y + cameraY,
};
}
}
return {
phase,
result,
score,
bird: role ? {
x: role.worldX,
y: role.worldY + cameraY,
vy: role.vy,
} : { x: 0, y: 0, vy: 0 },
nextPlatform: nextPlatform || { x: 0, y: 0 },
};
},
};
}
return { init, update, render, destroy, _forensicsView };
}

View File

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

View File

@ -0,0 +1,20 @@
{
"driver": {
"type": "seek-x",
"steps": 70,
"stepMs": 120,
"deadzone": 16
},
"expectedEngineCallPrefixes": [
"particles.",
"audio.synth"
],
"assertAfterPlay": [
{
"path": "score",
"op": "increased",
"why": "适配性真玩(读 bird.x/nextPlatform.x 左右趋近接台)→高度分上升;落不住或计分语义错则不升"
}
],
"expectLatch": true
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"inputs":{"src/core/plugin.js":{"bytes":44317,"imports":[],"format":"esm"},"src/plugins/runtime-probe/impl.js":{"bytes":16639,"imports":[],"format":"esm"},"host-dev/engine-caps.js":{"bytes":13897,"imports":[],"format":"esm"},"host-dev/engine-math.js":{"bytes":6155,"imports":[],"format":"esm"},"src/host/boot-game-host.js":{"bytes":27676,"imports":[{"path":"src/core/plugin.js","kind":"import-statement","original":"../core/plugin.js"},{"path":"src/plugins/runtime-probe/impl.js","kind":"import-statement","original":"../plugins/runtime-probe/impl.js"},{"path":"host-dev/engine-caps.js","kind":"import-statement","original":"../../host-dev/engine-caps.js"},{"path":"host-dev/engine-math.js","kind":"import-statement","original":"../../host-dev/engine-math.js"}],"format":"esm"},"src/plugins/collision/impl.js":{"bytes":24812,"imports":[],"format":"esm"},"src/plugins/physics-lite/impl.js":{"bytes":14507,"imports":[],"format":"esm"},"src/plugins/gamefeel/impl.js":{"bytes":21715,"imports":[],"format":"esm"},"src/plugins/particles-juice/impl.js":{"bytes":31830,"imports":[],"format":"esm"},"src/plugins/palette-post/impl.js":{"bytes":20240,"imports":[],"format":"esm"},"src/plugins/audio-music/impl.js":{"bytes":24659,"imports":[],"format":"esm"},"src/plugins/save-progress/impl.js":{"bytes":14608,"imports":[],"format":"esm"},"src/host/generic-host-config.js":{"bytes":7803,"imports":[{"path":"src/plugins/collision/impl.js","kind":"import-statement","original":"../plugins/collision/impl.js"},{"path":"src/plugins/physics-lite/impl.js","kind":"import-statement","original":"../plugins/physics-lite/impl.js"},{"path":"src/plugins/gamefeel/impl.js","kind":"import-statement","original":"../plugins/gamefeel/impl.js"},{"path":"src/plugins/particles-juice/impl.js","kind":"import-statement","original":"../plugins/particles-juice/impl.js"},{"path":"src/plugins/palette-post/impl.js","kind":"import-statement","original":"../plugins/palette-post/impl.js"},{"path":"src/plugins/audio-music/impl.js","kind":"import-statement","original":"../plugins/audio-music/impl.js"},{"path":"src/plugins/save-progress/impl.js","kind":"import-statement","original":"../plugins/save-progress/impl.js"}],"format":"esm"},"games/_wg1-gen/flappy/generated-factory.js":{"bytes":9192,"imports":[],"format":"esm"},"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytes":640153,"imports":[],"format":"esm"},"games/_wg1-gen/flappy/entry-bundle.js":{"bytes":6230,"imports":[{"path":"src/host/boot-game-host.js","kind":"import-statement","original":"../../../src/host/boot-game-host.js"},{"path":"src/host/generic-host-config.js","kind":"import-statement","original":"../../../src/host/generic-host-config.js"},{"path":"games/_wg1-gen/flappy/generated-factory.js","kind":"import-statement","original":"./generated-factory.js"},{"path":"node_modules/littlejsengine/dist/littlejs.esm.js","kind":"dynamic-import","original":"littlejsengine"}],"format":"esm"}},"outputs":{"games/_wg1-gen/flappy/bundle.iife.js":{"imports":[],"exports":[],"entryPoint":"games/_wg1-gen/flappy/entry-bundle.js","inputs":{"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytesInOutput":164173},"games/_wg1-gen/flappy/entry-bundle.js":{"bytesInOutput":507},"src/core/plugin.js":{"bytesInOutput":8989},"src/plugins/runtime-probe/impl.js":{"bytesInOutput":2794},"host-dev/engine-caps.js":{"bytesInOutput":1597},"host-dev/engine-math.js":{"bytesInOutput":498},"src/host/boot-game-host.js":{"bytesInOutput":5363},"src/plugins/collision/impl.js":{"bytesInOutput":1605},"src/plugins/physics-lite/impl.js":{"bytesInOutput":115},"src/plugins/gamefeel/impl.js":{"bytesInOutput":2533},"src/plugins/particles-juice/impl.js":{"bytesInOutput":4166},"src/plugins/palette-post/impl.js":{"bytesInOutput":4218},"src/plugins/audio-music/impl.js":{"bytesInOutput":6001},"src/plugins/save-progress/impl.js":{"bytesInOutput":3933},"src/host/generic-host-config.js":{"bytesInOutput":697},"games/_wg1-gen/flappy/generated-factory.js":{"bytesInOutput":3299}},"bytes":211059}}}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,110 @@
{
"gameId": "flappy",
"url": "http://localhost:4320/games/_wg1-gen/flappy/index.html",
"ts": null,
"guards": {
"A_boot": {
"pass": true
},
"C_frame": {
"pass": true,
"f0": 48,
"f1": 78,
"delta": 30
},
"I_control": {
"pass": true,
"skipped": "未声明 controlCheck向后兼容"
},
"D_render": {
"pass": true,
"bright": 178920,
"maxCh": 255
},
"E_live": {
"pass": true,
"distinctStates": 6,
"samples": 6
},
"B_uncaught": {
"pass": true,
"uncaught": []
},
"F_wiring": {
"pass": true,
"callCount": 8,
"sample": [
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx"
],
"expected": [
"particles.",
"audio.synth"
]
},
"G_input": {
"pass": true,
"inputHash": "0e133ce8",
"controlHash": "4fb72d48",
"atFrame": 214,
"ctrlFrame": 218
},
"H_progress": {
"pass": false,
"checks": [
{
"path": "score",
"op": "increased",
"before": 0,
"after": 0,
"pass": false,
"why": "适配性真玩(读 bird.y/nextGap.centerY 拍翅穿缝)→得分上升;物理太硬秒死或穿不过则 0"
}
],
"latch": {
"pass": true,
"dwelledMs": 600,
"after": "gameover",
"reason": ""
},
"state0": {
"phase": "playing",
"result": null,
"score": 0,
"bird": {
"x": 110,
"y": 559.1999999999999,
"vy": 336
},
"nextGap": {
"x": 600,
"centerY": 174.12938179075718
},
"gameoverReason": ""
},
"state1": {
"phase": "gameover",
"result": "lose",
"score": 0,
"bird": {
"x": 110,
"y": 523.0999999999999,
"vy": -169
},
"nextGap": {
"x": 600,
"centerY": 174.12938179075718
},
"gameoverReason": "pipe_collision"
}
}
},
"pass": false,
"notes": []
}

View File

@ -0,0 +1,310 @@
'use strict';
export default function createFlappyBirdGame(opts) {
const o = opts || {};
let ctx = null;
let nowMs = 0;
let subPointer = null;
// 游戏状态
let bird = { x: 110, y: 422, vy: 0, r: 12 }; // r 用于碰撞和渲染
const GRAVITY = 420; // 像素/秒²
const FLAP_VEL = -260; // 拍翅瞬速(负=上)
const PIPE_SPEED = 150; // 像素/秒
const PIPE_WIDTH = 60;
const GAP_HEIGHT = 220; // ≥ 0.24 * 844 = 202.56
const PIPE_SPACING = 280; // 管道中心水平间隔,保证反应时间
const MIN_PIPE_CENTER_Y = GAP_HEIGHT / 2;
const MAX_PIPE_CENTER_Y = 844 - GAP_HEIGHT / 2;
const BIRD_HALF = 12; // 鸟的显示/碰撞半径(实际用矩形 24x24
let pipes = []; // { x, centerY, scored }
let score = 0;
let phase = 'booting'; // 'booting' | 'playing' | 'gameover'
let gameoverReason = '';
let result = null;
// 粒子/音效触发标记(帧内)
let pendingFlapEffect = false;
let pendingScoreEffect = false;
let pendingCrashEffect = false;
let crashPos = { x: 0, y: 0 };
function resetGame() {
bird.y = 422;
bird.vy = 0;
pipes = [];
score = 0;
phase = 'playing';
gameoverReason = '';
result = null;
pendingFlapEffect = false;
pendingScoreEffect = false;
pendingCrashEffect = false;
// 第一对管道初始位置:保证 ≥1.5 秒后与鸟相遇
const initialPipeX = 390 + PIPE_WIDTH / 2 + 180; // 速度150到达鸟时间≈(570-110)/150≈3.07s > 1.5s
pipes.push({
x: initialPipeX,
centerY: randomPipeCenterY(),
scored: false
});
}
function randomPipeCenterY() {
// ctx.random.range 防御
if (ctx && ctx.random && ctx.random.range) {
return ctx.random.range(MIN_PIPE_CENTER_Y, MAX_PIPE_CENTER_Y);
}
// 极低概率缺省时用固定值
return 422;
}
/** 触发引擎粒子 + 音效 */
function spawnEffects(pos, type) {
const eng = ctx.getEngine();
if (!eng) return;
if (eng.particles) {
try {
const emitter = eng.particles.spawnEmitter({
pos: { x: pos.x, y: pos.y },
count: type === 'crash' ? 35 : 22,
emitRate: 0,
particleTime: 0.6,
speed: type === 'crash' ? 160 : 110,
coneAngle: type === 'crash' ? Math.PI : 0.5,
sizeStart: type === 'crash' ? 16 : 12,
sizeEnd: 2,
colorStart: type === 'crash' ? { r: 1, g: 0.1, b: 0.1, a: 1 }
: type === 'score' ? { r: 0.2, g: 1, b: 0.3, a: 1 }
: { r: 0.3, g: 0.7, b: 1, a: 1 },
colorEnd: type === 'crash' ? { r: 1, g: 0.3, b: 0, a: 0 }
: { r: 0.1, g: 0.5, b: 1, a: 0 }
});
} catch (_) { }
}
if (eng.audio && eng.audio.synth) {
try {
if (type === 'flap') {
eng.audio.synth.synthSfx([1, 0.05, 340, , , 0.12, , 1.2, , , , , , , , 0.1]);
} else if (type === 'score') {
eng.audio.synth.synthSfx([1, 0.05, 880, , , 0.22, , 1.9, , , , , , , , 0.4]);
} else if (type === 'crash') {
eng.audio.synth.synthSfx([2, 0.5, 90, , , 0.5, , 0.3, , , , , , , , 0.1]);
}
} catch (_) { }
}
}
/** 点击处理拍翅playing或重玩gameover */
function onPointer(e) {
if (phase === 'playing') {
// 拍翅
bird.vy = FLAP_VEL;
pendingFlapEffect = true;
} else if (phase === 'gameover') {
// 检测重玩按钮区域(居中偏下)
const bx = 115, by = 620, bw = 160, bh = 60;
const px = e.x || 0, py = e.y || 0;
if (px >= bx && px <= bx + bw && py >= by && py <= by + bh) {
resetGame();
}
}
}
function init(boot) {
ctx = boot.ctx;
// 订阅输入
subPointer = ctx.getInput().on('pointerdown', onPointer);
// 初始随机种子
resetGame();
phase = 'playing';
}
function update(dt) {
nowMs = ctx.time.nowMs(); // 控制钟
if (phase !== 'playing') return; // 终态驻留,不更新物理
// 物理更新dt 固定 1/60
const grav = GRAVITY;
bird.vy += grav * dt;
bird.y += bird.vy * dt;
// 触顶触底判定
if (bird.y - BIRD_HALF <= 0 || bird.y + BIRD_HALF >= 844) {
gameOver('boundary');
return;
}
// 更新管道位置
for (const pipe of pipes) {
pipe.x -= PIPE_SPEED * dt;
}
// 移除屏幕外的管道
pipes = pipes.filter(p => p.x > -PIPE_WIDTH / 2 - 10);
// 生成新管道:当最右的管道 x 小于阈值时,生成新管道
const lastPipe = pipes.length > 0 ? pipes[pipes.length - 1] : null;
if (!lastPipe || lastPipe.x <= 390 + PIPE_WIDTH / 2 - PIPE_SPACING) {
const newX = lastPipe ? lastPipe.x + PIPE_SPACING : 390 + PIPE_WIDTH / 2 + 180;
pipes.push({
x: newX,
centerY: randomPipeCenterY(),
scored: false
});
}
// 碰撞检测 & 得分
const birdRect = {
x: bird.x - BIRD_HALF,
y: bird.y - BIRD_HALF,
w: BIRD_HALF * 2,
h: BIRD_HALF * 2
};
for (const pipe of pipes) {
const topH = pipe.centerY - GAP_HEIGHT / 2;
const bottomY = pipe.centerY + GAP_HEIGHT / 2;
const pipeRect = {
x: pipe.x - PIPE_WIDTH / 2,
y: 0,
w: PIPE_WIDTH,
h: topH
};
const pipeRect2 = {
x: pipe.x - PIPE_WIDTH / 2,
y: bottomY,
w: PIPE_WIDTH,
h: 844 - bottomY
};
// 碰撞(矩形相交)
const collides = (a, b) => {
return a.x < b.x + b.w && a.x + a.w > b.x &&
a.y < b.y + b.h && a.y + a.h > b.y;
};
if (collides(birdRect, pipeRect) || collides(birdRect, pipeRect2)) {
gameOver('pipe_collision');
return;
}
// 得分:管道完全通过鸟左边缘
if (!pipe.scored && pipe.x + PIPE_WIDTH / 2 < bird.x - BIRD_HALF) {
pipe.scored = true;
score++;
pendingScoreEffect = true;
}
}
// 处理本帧累积的特效
if (pendingFlapEffect) {
spawnEffects({ x: bird.x, y: bird.y }, 'flap');
pendingFlapEffect = false;
}
if (pendingScoreEffect) {
spawnEffects({ x: bird.x, y: bird.y }, 'score');
pendingScoreEffect = false;
}
if (pendingCrashEffect) {
spawnEffects(crashPos, 'crash');
pendingCrashEffect = false;
}
}
function gameOver(reason) {
phase = 'gameover';
gameoverReason = reason;
result = 'lose';
crashPos = { x: bird.x, y: bird.y };
pendingCrashEffect = true;
}
function render(g) {
// 背景
g.fillStyle = '#0a1122';
g.fillRect(0, 0, 390, 844);
if (phase === 'gameover' || phase === 'playing') {
// 绘制管道
g.fillStyle = '#3a5';
for (const pipe of pipes) {
const topH = pipe.centerY - GAP_HEIGHT / 2;
const bottomY = pipe.centerY + GAP_HEIGHT / 2;
// 顶管道
g.fillRect(pipe.x - PIPE_WIDTH / 2, 0, PIPE_WIDTH, topH);
// 底管道
g.fillRect(pipe.x - PIPE_WIDTH / 2, bottomY, PIPE_WIDTH, 844 - bottomY);
// 管道帽
g.fillStyle = '#2c4';
g.fillRect(pipe.x - PIPE_WIDTH / 2 - 4, topH - 10, PIPE_WIDTH + 8, 10);
g.fillRect(pipe.x - PIPE_WIDTH / 2 - 4, bottomY, PIPE_WIDTH + 8, 10);
g.fillStyle = '#3a5';
}
// 绘制小鸟(亮黄色方块+眼睛)
g.fillStyle = '#ffcc00';
g.fillRect(bird.x - BIRD_HALF, bird.y - BIRD_HALF, BIRD_HALF * 2, BIRD_HALF * 2);
g.fillStyle = '#000';
g.fillRect(bird.x + 4, bird.y - 6, 5, 5);
}
// HUD
g.fillStyle = '#ffffff';
g.font = '20px monospace';
g.fillText('得分: ' + score, 20, 40);
if (phase === 'gameover') {
g.fillStyle = '#ffffff';
g.font = '26px monospace';
g.fillText('失败!', 130, 300);
// 重玩按钮
const bx = 115, by = 620, bw = 160, bh = 60;
g.fillStyle = '#2255aa';
g.fillRect(bx, by, bw, bh);
g.fillStyle = '#ffffff';
g.font = '22px monospace';
g.fillText('点击重玩', bx + 10, by + 38);
}
}
function destroy() {
try { subPointer && subPointer.cancel(); } catch (_) {}
subPointer = null;
pipes = [];
bird = { x: 110, y: 422, vy: 0, r: 12 };
}
/** 可测性视图 */
function _forensicsView() {
const nextGap = (() => {
// 找到小鸟前方最近的管道
if (phase !== 'playing') return null;
let candidate = null;
for (const pipe of pipes) {
if (pipe.x > bird.x - BIRD_HALF) {
if (!candidate || pipe.x < candidate.x) {
candidate = pipe;
}
}
}
if (!candidate) return null;
return { x: candidate.x, centerY: candidate.centerY };
})();
return {
seed: opts && opts.seed !== undefined ? opts.seed : undefined,
state: () => ({
phase,
result,
score,
bird: { x: bird.x, y: bird.y, vy: bird.vy },
nextGap,
gameoverReason
})
};
}
return {
init,
update,
render,
destroy,
_forensicsView
};
}

View File

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

View File

@ -0,0 +1,20 @@
{
"driver": {
"type": "flap-to-gap",
"steps": 70,
"stepMs": 110,
"margin": 14
},
"expectedEngineCallPrefixes": [
"particles.",
"audio.synth"
],
"assertAfterPlay": [
{
"path": "score",
"op": "increased",
"why": "适配性真玩(读 bird.y/nextGap.centerY 拍翅穿缝)→得分上升;物理太硬秒死或穿不过则 0"
}
],
"expectLatch": true
}

File diff suppressed because one or more lines are too long

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,171 @@
{
"gameId": "invaders",
"url": "http://localhost:4320/games/_wg1-gen/invaders/index.html",
"ts": null,
"guards": {
"A_boot": {
"pass": true
},
"C_frame": {
"pass": true,
"f0": 47,
"f1": 80,
"delta": 33
},
"I_control": {
"pass": true,
"skipped": "未声明 controlCheck向后兼容"
},
"D_render": {
"pass": true,
"bright": 51625,
"maxCh": 255
},
"E_live": {
"pass": true,
"distinctStates": 47,
"samples": 47
},
"B_uncaught": {
"pass": true,
"uncaught": []
},
"F_wiring": {
"pass": true,
"callCount": 114,
"sample": [
"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",
"particles.spawnEmitter",
"audio.synth.synthSfx"
],
"expected": [
"particles.",
"audio.synth"
]
},
"G_input": {
"pass": true,
"inputHash": "9dc71645",
"controlHash": "27ebc378",
"atFrame": 1544,
"ctrlFrame": -1
},
"H_progress": {
"pass": true,
"checks": [
{
"path": "score",
"op": "increased",
"before": 0,
"after": 12,
"pass": true,
"why": "适配性真玩(炮台移到敌人 x 开火)下击毁敌人→得分上升;打不中/无碰撞则不升"
}
],
"latch": {
"pass": true,
"dwelledMs": 600,
"after": "gameover",
"reason": ""
},
"state0": {
"phase": "playing",
"result": null,
"score": 0,
"enemiesRemaining": 12,
"targets": [
{
"x": 151.33333333333306,
"y": 95,
"idx": 0,
"occupied": false
},
{
"x": 201.3333333333329,
"y": 95,
"idx": 1,
"occupied": false
},
{
"x": 251.3333333333329,
"y": 95,
"idx": 2,
"occupied": false
},
{
"x": 301.3333333333342,
"y": 95,
"idx": 3,
"occupied": false
},
{
"x": 151.33333333333306,
"y": 135,
"idx": 4,
"occupied": false
},
{
"x": 201.3333333333329,
"y": 135,
"idx": 5,
"occupied": false
},
{
"x": 251.3333333333329,
"y": 135,
"idx": 6,
"occupied": false
},
{
"x": 301.3333333333342,
"y": 135,
"idx": 7,
"occupied": false
},
{
"x": 151.33333333333306,
"y": 175,
"idx": 8,
"occupied": false
},
{
"x": 201.3333333333329,
"y": 175,
"idx": 9,
"occupied": false
},
{
"x": 251.3333333333329,
"y": 175,
"idx": 10,
"occupied": false
},
{
"x": 301.3333333333342,
"y": 175,
"idx": 11,
"occupied": false
}
]
},
"state1": {
"phase": "gameover",
"result": "win",
"score": 12,
"enemiesRemaining": 0,
"targets": []
}
}
},
"pass": true,
"notes": []
}

View File

@ -0,0 +1,310 @@
'use strict';
export default function createGame(opts) {
const o = opts || {};
let ctx;
let phase; // 'playing' | 'gameover'
let result; // null | 'win' | 'lose'
let score;
let enemies; // { idx, x, y, initX, initY, alive }
let enemyDir; // 1 or -1
let downTimer; // seconds
let paddleX; // 炮台中心 x
let bullets; // { x, y, active, w, h }
let pointerSub;
const ENEMY_DOWN_INTERVAL = 1.5;
const ENEMY_SPEED_HOR = 40; // px/s
const BULLET_SPEED = 400; // px/s
const STEP_DOWN = 30; // 下压步长
const PADDLE_TOP = 844 - 20; // 炮台矩形顶部 y
const RESTART_RECT = { x: 95, y: 400, w: 200, h: 50 };
// ---------- 事件处理 ----------
function handlePointerDown(e) {
const px = e.x;
const py = e.y;
if (phase === 'gameover') {
if (px >= RESTART_RECT.x && px <= RESTART_RECT.x + RESTART_RECT.w &&
py >= RESTART_RECT.y && py <= RESTART_RECT.y + RESTART_RECT.h) {
resetGame();
}
return;
}
// playing → 移动炮台 + 发射子弹
paddleX = Math.max(20, Math.min(370, px)); // 炮台宽 40 → 半宽 20
const bulletX = paddleX;
bullets.push({ x: bulletX, y: PADDLE_TOP, active: true, w: 6, h: 12 });
// 粒子(开火)
const eng = ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x: bulletX, y: PADDLE_TOP },
count: 10,
speed: 60,
particleTime: 0.3,
colorStart: { r: 0.2, g: 0.8, b: 1, a: 1 },
colorEnd: { r: 0, g: 0.3, b: 0.6, a: 0 }
});
} catch (_) {}
}
if (eng && eng.audio && eng.audio.synth) {
try { eng.audio.synth.synthSfx([1, 0.05, 440, , , 0.1, , 1.4]); } catch (_) {}
}
}
// ---------- 游戏重置 ----------
function resetGame() {
phase = 'playing';
result = null;
score = 0;
bullets = [];
paddleX = 390 / 2;
enemyDir = 1;
downTimer = ENEMY_DOWN_INTERVAL;
enemies.forEach(e => {
e.x = e.initX;
e.y = e.initY;
e.alive = true;
});
}
// ---------- 敌人下移 ----------
function shiftDown() {
enemies.forEach(e => {
if (e.alive) e.y += STEP_DOWN;
});
}
// ---------- 生命周期 ----------
function init(boot) {
ctx = boot.ctx;
phase = 'playing';
result = null;
score = 0;
bullets = [];
paddleX = 390 / 2;
enemyDir = 1;
downTimer = ENEMY_DOWN_INTERVAL;
// 初始化敌人3行×4列
enemies = [];
const startY = 80;
const rows = 3;
const cols = 4;
const enemyW = 30;
const colGap = 50;
const rowGap = 40;
const totalW = (cols - 1) * colGap + enemyW;
const offsetLeft = (390 - totalW) / 2 + enemyW / 2;
const offsetTop = startY + enemyW / 2; // 中心 Y 偏移
let idx = 0;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const ex = offsetLeft + c * colGap;
const ey = offsetTop + r * rowGap;
enemies.push({
idx,
x: ex, y: ey,
initX: ex, initY: ey,
alive: true
});
idx++;
}
}
pointerSub = ctx.getInput().on('pointerdown', handlePointerDown);
}
function update(dt) {
if (phase !== 'playing') return;
// ---------- 敌人逻辑 ----------
const aliveEnemies = enemies.filter(e => e.alive);
if (aliveEnemies.length > 0) {
// 水平移动
const dx = ENEMY_SPEED_HOR * dt * enemyDir;
aliveEnemies.forEach(e => e.x += dx);
// 边界检测 → 反转 + 下移 + 重置计时器
let minX = Infinity, maxX = -Infinity;
aliveEnemies.forEach(e => {
if (e.x < minX) minX = e.x;
if (e.x > maxX) maxX = e.x;
});
if (minX - 15 <= 0) {
enemyDir = 1;
shiftDown();
downTimer = ENEMY_DOWN_INTERVAL;
} else if (maxX + 15 >= 390) {
enemyDir = -1;
shiftDown();
downTimer = ENEMY_DOWN_INTERVAL;
}
// 定时下压
downTimer -= dt;
if (downTimer <= 0) {
downTimer += ENEMY_DOWN_INTERVAL;
shiftDown();
}
}
// ---------- 子弹逻辑 ----------
bullets.forEach(b => {
if (!b.active) return;
b.y -= BULLET_SPEED * dt;
if (b.y + b.h / 2 < 0) b.active = false;
});
// 子弹与敌人碰撞
for (const b of bullets) {
if (!b.active) continue;
for (const e of enemies) {
if (!e.alive) continue;
// 矩形相交检测
const bLeft = b.x - b.w / 2;
const bRight = b.x + b.w / 2;
const bTop = b.y - b.h / 2;
const bBottom = b.y + b.h / 2;
const eLeft = e.x - 15;
const eRight = e.x + 15;
const eTop = e.y - 15;
const eBottom = e.y + 15;
if (bRight >= eLeft && bLeft <= eRight &&
bBottom >= eTop && bTop <= eBottom) {
e.alive = false;
b.active = false;
score++;
// 命中粒子
const eng = ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x: e.x, y: e.y },
count: 20,
speed: 100,
particleTime: 0.4,
colorStart: { r: 1, g: 0.7, b: 0.2, a: 1 },
colorEnd: { r: 0.8, g: 0.2, b: 0, a: 0 }
});
} catch (_) {}
}
if (eng && eng.audio && eng.audio.synth) {
try { eng.audio.synth.synthSfx([1, 0.05, 600, , , 0.1, , 1.2]); } catch (_) {}
}
break; // 一颗子弹只打一个敌人
}
}
}
// 清除失效子弹
bullets = bullets.filter(b => b.active);
// ---------- 胜负判定 ----------
const aliveCount = enemies.filter(e => e.alive).length;
if (aliveCount === 0) {
phase = 'gameover';
result = 'win';
} else {
let maxY = -Infinity;
enemies.forEach(e => { if (e.alive && e.y > maxY) maxY = e.y; });
if (maxY + 15 >= PADDLE_TOP) {
phase = 'gameover';
result = 'lose';
}
}
}
function render(g) {
// 背景
g.fillStyle = '#0a0a1a';
g.fillRect(0, 0, 390, 844);
// 敌人(存活)
g.fillStyle = '#33ff66';
enemies.forEach(e => {
if (!e.alive) return;
g.fillRect(e.x - 15, e.y - 15, 30, 30);
});
// 子弹
g.fillStyle = '#ffcc00';
bullets.forEach(b => {
g.fillRect(b.x - b.w / 2, b.y - b.h / 2, b.w, b.h);
});
// 炮台
g.fillStyle = '#66aaff';
g.fillRect(paddleX - 20, 844 - 20, 40, 20);
// HUD
g.fillStyle = '#ffffff';
g.font = '16px monospace';
g.textAlign = 'left';
g.fillText('得分: ' + score, 10, 25);
const aliveCount = enemies.filter(e => e.alive).length;
g.fillText('剩余: ' + aliveCount, 150, 25);
if (phase === 'playing') g.fillText('状态: 战斗', 290, 25);
// 游戏结束界面
if (phase === 'gameover') {
g.fillStyle = 'rgba(0,0,0,0.5)';
g.fillRect(0, 0, 390, 844);
g.fillStyle = '#ffffff';
g.font = 'bold 32px monospace';
g.textAlign = 'center';
if (result === 'win') g.fillText('你赢了!', 195, 300);
else if (result === 'lose') g.fillText('你输了!', 195, 300);
g.fillStyle = '#228833';
g.fillRect(RESTART_RECT.x, RESTART_RECT.y, RESTART_RECT.w, RESTART_RECT.h);
g.fillStyle = '#ffffff';
g.font = '20px monospace';
g.fillText('点击重玩', RESTART_RECT.x + RESTART_RECT.w / 2, RESTART_RECT.y + RESTART_RECT.h / 2 + 6);
}
g.textAlign = 'left';
}
function destroy() {
try { pointerSub && pointerSub.cancel(); } catch (_) {}
pointerSub = null;
enemies = [];
bullets = [];
}
// ---------- 可测性视图 ----------
function state() {
const alive = enemies.filter(e => e.alive).map(e => ({
x: e.x,
y: e.y,
idx: e.idx,
occupied: false
}));
return {
phase,
result,
score,
enemiesRemaining: alive.length,
targets: alive
};
}
return {
init,
update,
render,
destroy,
_forensicsView: () => ({ seed: o.seed || 0, state })
};
}

View File

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

View File

@ -0,0 +1,19 @@
{
"driver": {
"type": "tap-targets",
"steps": 50,
"stepMs": 260
},
"expectedEngineCallPrefixes": [
"particles.",
"audio.synth"
],
"assertAfterPlay": [
{
"path": "score",
"op": "increased",
"why": "适配性真玩(炮台移到敌人 x 开火)下击毁敌人→得分上升;打不中/无碰撞则不升"
}
],
"expectLatch": true
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"inputs":{"src/core/plugin.js":{"bytes":44317,"imports":[],"format":"esm"},"src/plugins/runtime-probe/impl.js":{"bytes":16639,"imports":[],"format":"esm"},"host-dev/engine-caps.js":{"bytes":13897,"imports":[],"format":"esm"},"host-dev/engine-math.js":{"bytes":6155,"imports":[],"format":"esm"},"src/host/boot-game-host.js":{"bytes":27676,"imports":[{"path":"src/core/plugin.js","kind":"import-statement","original":"../core/plugin.js"},{"path":"src/plugins/runtime-probe/impl.js","kind":"import-statement","original":"../plugins/runtime-probe/impl.js"},{"path":"host-dev/engine-caps.js","kind":"import-statement","original":"../../host-dev/engine-caps.js"},{"path":"host-dev/engine-math.js","kind":"import-statement","original":"../../host-dev/engine-math.js"}],"format":"esm"},"src/plugins/collision/impl.js":{"bytes":24812,"imports":[],"format":"esm"},"src/plugins/physics-lite/impl.js":{"bytes":14507,"imports":[],"format":"esm"},"src/plugins/gamefeel/impl.js":{"bytes":21715,"imports":[],"format":"esm"},"src/plugins/particles-juice/impl.js":{"bytes":31830,"imports":[],"format":"esm"},"src/plugins/palette-post/impl.js":{"bytes":20240,"imports":[],"format":"esm"},"src/plugins/audio-music/impl.js":{"bytes":24659,"imports":[],"format":"esm"},"src/plugins/save-progress/impl.js":{"bytes":14608,"imports":[],"format":"esm"},"src/host/generic-host-config.js":{"bytes":7803,"imports":[{"path":"src/plugins/collision/impl.js","kind":"import-statement","original":"../plugins/collision/impl.js"},{"path":"src/plugins/physics-lite/impl.js","kind":"import-statement","original":"../plugins/physics-lite/impl.js"},{"path":"src/plugins/gamefeel/impl.js","kind":"import-statement","original":"../plugins/gamefeel/impl.js"},{"path":"src/plugins/particles-juice/impl.js","kind":"import-statement","original":"../plugins/particles-juice/impl.js"},{"path":"src/plugins/palette-post/impl.js","kind":"import-statement","original":"../plugins/palette-post/impl.js"},{"path":"src/plugins/audio-music/impl.js","kind":"import-statement","original":"../plugins/audio-music/impl.js"},{"path":"src/plugins/save-progress/impl.js","kind":"import-statement","original":"../plugins/save-progress/impl.js"}],"format":"esm"},"games/_wg1-gen/jianfeng/generated-factory.js":{"bytes":8875,"imports":[],"format":"esm"},"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytes":640153,"imports":[],"format":"esm"},"games/_wg1-gen/jianfeng/entry-bundle.js":{"bytes":6230,"imports":[{"path":"src/host/boot-game-host.js","kind":"import-statement","original":"../../../src/host/boot-game-host.js"},{"path":"src/host/generic-host-config.js","kind":"import-statement","original":"../../../src/host/generic-host-config.js"},{"path":"games/_wg1-gen/jianfeng/generated-factory.js","kind":"import-statement","original":"./generated-factory.js"},{"path":"node_modules/littlejsengine/dist/littlejs.esm.js","kind":"dynamic-import","original":"littlejsengine"}],"format":"esm"}},"outputs":{"games/_wg1-gen/jianfeng/bundle.iife.js":{"imports":[],"exports":[],"entryPoint":"games/_wg1-gen/jianfeng/entry-bundle.js","inputs":{"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytesInOutput":164141},"games/_wg1-gen/jianfeng/entry-bundle.js":{"bytesInOutput":507},"src/core/plugin.js":{"bytesInOutput":8989},"src/plugins/runtime-probe/impl.js":{"bytesInOutput":2794},"host-dev/engine-caps.js":{"bytesInOutput":1597},"host-dev/engine-math.js":{"bytesInOutput":498},"src/host/boot-game-host.js":{"bytesInOutput":5357},"src/plugins/collision/impl.js":{"bytesInOutput":1605},"src/plugins/physics-lite/impl.js":{"bytesInOutput":115},"src/plugins/gamefeel/impl.js":{"bytesInOutput":2533},"src/plugins/particles-juice/impl.js":{"bytesInOutput":4159},"src/plugins/palette-post/impl.js":{"bytesInOutput":4218},"src/plugins/audio-music/impl.js":{"bytesInOutput":6001},"src/plugins/save-progress/impl.js":{"bytesInOutput":3933},"src/host/generic-host-config.js":{"bytesInOutput":697},"games/_wg1-gen/jianfeng/generated-factory.js":{"bytesInOutput":3420}},"bytes":211135}}}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -0,0 +1,106 @@
{
"gameId": "jianfeng",
"url": "http://localhost:4320/games/_wg1-gen/jianfeng/index.html",
"ts": null,
"guards": {
"A_boot": {
"pass": true
},
"C_frame": {
"pass": true,
"f0": 56,
"f1": 87,
"delta": 31
},
"I_control": {
"pass": true,
"skipped": "未声明 controlCheck向后兼容"
},
"D_render": {
"pass": true,
"bright": 55058,
"maxCh": 255
},
"E_live": {
"pass": true,
"distinctStates": 29,
"samples": 29
},
"B_uncaught": {
"pass": true,
"uncaught": []
},
"F_wiring": {
"pass": true,
"callCount": 974,
"sample": [
"particles.spawnEmitter",
"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",
"particles.spawnEmitter"
],
"expected": [
"particles.",
"audio.synth"
]
},
"G_input": {
"pass": true,
"inputHash": "a866eaf1",
"controlHash": "2cc7cc1e",
"atFrame": 951,
"ctrlFrame": 957
},
"H_progress": {
"pass": true,
"checks": [
{
"path": "inserted",
"op": "increased",
"before": 0,
"after": 12,
"pass": true,
"why": "定时连发射针→已插入数上升;点击失灵/不响应则不升"
}
],
"latch": {
"pass": true,
"dwelledMs": 600,
"after": "gameover",
"reason": ""
},
"state0": {
"phase": "playing",
"result": null,
"inserted": 0,
"target": 12,
"score": 0,
"remaining": 12,
"progress": 0,
"gameoverReason": "",
"diskAngle": 0.5600000000000003
},
"state1": {
"phase": "gameover",
"result": "win",
"inserted": 12,
"target": 12,
"score": 12,
"remaining": 0,
"progress": 1,
"gameoverReason": "cleared_all",
"diskAngle": 1.7968146928203255
}
}
},
"pass": true,
"notes": []
}

View File

@ -0,0 +1,326 @@
'use strict';
/**
* 见缝插针游戏工厂模块
* 符合 GameHostFactory 契约支持粒子/音效/受控时钟与随机
* 可测性_forensicsView 提供实时状态快照
*/
export default function createGame(opts) {
const o = opts || {};
let ctx = null;
let seed = 0;
// 游戏配置
const TARGET = o.target || 12;
const R = 100; // 圆盘半径
const CX = 195; // 圆心x
const CY = 350; // 圆心y
const ANG_VEL = 0.6; // 弧度/秒
const COLL_THRESH = Math.PI / 18; // 10度
// 游戏状态
let phase = 'booting'; // 'booting' -> 'playing' -> 'gameover'
let result = null; // null | 'win' | 'lose'
let inserted = 0;
let needles = []; // { angle: number }[]
let needleReady = true; // 底部有待发射针
let diskAngle = 0;
let gameoverReason = '';
let subscriptions = [];
// 点击订阅句柄
let pointerSub = null;
// 重置函数(用于重玩)
function reset() {
phase = 'playing';
result = null;
inserted = 0;
needles = [];
needleReady = true;
diskAngle = 0;
gameoverReason = '';
// 预置 0 或 1 根针
const preCount = ctx.random.range(0, 1) < 0.5 ? 0 : 1;
for (let i = 0; i < preCount; i++) {
const a = ctx.random.range(0, 2 * Math.PI);
needles.push({ angle: a });
inserted++;
}
}
function init(boot) {
ctx = boot.ctx;
seed = boot.seed || 0;
// 订阅 pointerdown
pointerSub = ctx.getInput().on('pointerdown', onPointerDown);
subscriptions.push(pointerSub);
// 初始状态
reset();
// 尝试触发一次引擎能力(开场的粒子没啥用,但合规)
const eng = ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x: CX, y: CY },
emitRate: 10,
particleTime: 0.5,
speed: 50,
colorStart: { r: 0.3, g: 0.7, b: 1, a: 1 },
colorEnd: { r: 0, g: 0.2, b: 0.6, a: 0 },
});
} catch (_) {}
}
}
function onPointerDown(e) {
const now = ctx.time.nowMs();
const px = e.x;
const py = e.y;
if (phase === 'gameover') {
// 检查是否点击“重玩”按钮区域(底部中央一个矩形区域)
const btnX = CX - 60;
const btnY = 600;
const btnW = 120;
const btnH = 50;
if (px >= btnX && px <= btnX + btnW && py >= btnY && py <= btnY + btnH) {
reset();
// 重新随机预置reset中已经做
// 触发一次重置粒子
const eng = ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x: CX, y: CY },
count: 15,
speed: 80,
particleTime: 0.4,
colorStart: { r: 0.2, g: 1, b: 0.3, a: 1 },
colorEnd: { r: 0, g: 0.5, b: 0, a: 0 },
});
} catch (_) {}
}
}
return;
}
// 只有 playing 且 needleReady 才发射
if (phase !== 'playing' || !needleReady) return;
const newAngle = diskAngle; // 插入角度 = 当前圆盘角度
// 碰撞检测(与已插入针)
let collision = false;
for (let i = 0; i < needles.length; i++) {
let diff = Math.abs(newAngle - needles[i].angle);
if (diff > Math.PI) diff = 2 * Math.PI - diff;
if (diff < COLL_THRESH) {
collision = true;
break;
}
}
if (collision) {
// 失败
phase = 'gameover';
result = 'lose';
gameoverReason = 'collision';
needleReady = false; // 待发射针消失
// 触发失败粒子(在圆盘中心爆发红色粒子)
const eng = ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x: CX, y: CY },
count: 30,
speed: 120,
particleTime: 0.7,
colorStart: { r: 1, g: 0.2, b: 0.2, a: 1 },
colorEnd: { r: 0.6, g: 0, b: 0, a: 0 },
});
} catch (_) {}
}
if (eng && eng.audio && eng.audio.synth) {
try {
eng.audio.synth.synthSfx([1, 0.08, 150, , , 0.3, , 1.0]);
} catch (_) {}
}
return;
}
// 成功插入
needles.push({ angle: newAngle });
inserted++;
needleReady = true; // 底部生成新针
// 检查胜利
if (inserted >= TARGET) {
phase = 'gameover';
result = 'win';
gameoverReason = 'cleared_all';
needleReady = false;
}
// 成功粒子(在针头位置)
const headX = CX + (R + 8) * Math.cos(newAngle);
const headY = CY + (R + 8) * Math.sin(newAngle);
const eng = ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x: headX, y: headY },
count: 25,
speed: 90,
particleTime: 0.5,
colorStart: { r: 1, g: 0.8, b: 0.2, a: 1 },
colorEnd: { r: 0.8, g: 0.4, b: 0, a: 0 },
});
} catch (_) {}
}
if (eng && eng.audio && eng.audio.synth) {
try {
eng.audio.synth.synthSfx([1, 0.05, 440, , , 0.15, , 0.8]);
} catch (_) {}
}
}
function update(dt) {
const now = ctx.time.nowMs();
// 只有 playing 时旋转圆盘
if (phase === 'playing') {
diskAngle = (diskAngle + ANG_VEL * dt) % (2 * Math.PI);
}
// 每帧触发 math 调用(合规)
const eng = ctx.getEngine();
if (eng && eng.math) {
try {
const t = (now / 1000) % 1;
eng.math.lerp(0, 1, t);
} catch (_) {}
}
}
function render(g) {
// 深色背景满幅
g.fillStyle = '#0d0d1a';
g.fillRect(0, 0, 390, 844);
// 绘制 HUD顶部
g.fillStyle = '#ffffff';
g.font = '18px monospace';
g.textAlign = 'left';
g.fillText('针: ' + inserted + '/' + TARGET, 20, 30);
if (phase === 'gameover') {
g.fillStyle = result === 'win' ? '#4cff4c' : '#ff4c4c';
g.fillText(result === 'win' ? '胜利!' : '失败!', 20, 55);
}
// 绘制圆盘
g.save();
g.translate(CX, CY);
g.beginPath();
g.arc(0, 0, R, 0, 2 * Math.PI);
g.fillStyle = '#2a2a3e';
g.fill();
g.strokeStyle = '#88aacc';
g.lineWidth = 3;
g.stroke();
// 绘制已插入的针
for (const nd of needles) {
const angle = nd.angle;
const x0 = 0;
const y0 = 0;
const x1 = (R + 20) * Math.cos(angle);
const y1 = (R + 20) * Math.sin(angle);
g.beginPath();
g.moveTo(x0, y0);
g.lineTo(x1, y1);
g.strokeStyle = '#ffcc33';
g.lineWidth = 3;
g.stroke();
// 针头小圆点
g.beginPath();
g.arc(x1, y1, 3, 0, 2 * Math.PI);
g.fillStyle = '#ffaa00';
g.fill();
}
// 绘制待发射针(如果 needleReady 且 playing
if (needleReady && phase === 'playing') {
// 从底部指向圆心
const bx = 0;
const by = 430; // 相对于圆心偏移 (CY=350, 底部y约780 -> 偏移430)
const ex = 0;
const ey = 0; // 圆心
g.beginPath();
g.moveTo(bx, by);
g.lineTo(ex, ey);
g.strokeStyle = '#66ff66';
g.lineWidth = 4;
g.stroke();
// 针尾小圆
g.beginPath();
g.arc(bx, by, 4, 0, 2 * Math.PI);
g.fillStyle = '#66ff66';
g.fill();
}
// 绘制重玩按钮gameover时
if (phase === 'gameover') {
g.translate(-CX, -CY); // 先平移回原点
const btnX = CX - 60;
const btnY = 600;
const btnW = 120;
const btnH = 50;
g.fillStyle = '#335566';
g.fillRect(btnX, btnY, btnW, btnH);
g.strokeStyle = '#aaccdd';
g.lineWidth = 2;
g.strokeRect(btnX, btnY, btnW, btnH);
g.fillStyle = '#ffffff';
g.font = '20px monospace';
g.textAlign = 'center';
g.fillText('重玩', CX, btnY + 33);
} else {
g.restore(); // 只 restore 一次?注意上面有多次 save/restore这里简单处理直接重置变换
g.setTransform(1, 0, 0, 1, 0, 0);
}
}
function destroy() {
for (const sub of subscriptions) {
try { sub.cancel(); } catch (_) {}
}
subscriptions = [];
pointerSub = null;
needles = [];
}
function _forensicsView() {
return {
seed: seed,
state: () => ({
phase: phase,
result: result,
inserted: inserted,
target: TARGET,
score: inserted, // 通用字段
remaining: TARGET - inserted,
progress: TARGET > 0 ? inserted / TARGET : 0,
gameoverReason: gameoverReason,
diskAngle: diskAngle,
}),
};
}
return { init, update, render, destroy, _forensicsView };
}

View File

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

View File

@ -0,0 +1,137 @@
{
"inputs": [
{
"t": "wait",
"ms": 700
},
{
"t": "tap",
"x": 195,
"y": 760
},
{
"t": "wait",
"ms": 520
},
{
"t": "tap",
"x": 195,
"y": 760
},
{
"t": "wait",
"ms": 520
},
{
"t": "tap",
"x": 195,
"y": 760
},
{
"t": "wait",
"ms": 520
},
{
"t": "tap",
"x": 195,
"y": 760
},
{
"t": "wait",
"ms": 520
},
{
"t": "tap",
"x": 195,
"y": 760
},
{
"t": "wait",
"ms": 520
},
{
"t": "tap",
"x": 195,
"y": 760
},
{
"t": "wait",
"ms": 520
},
{
"t": "tap",
"x": 195,
"y": 760
},
{
"t": "wait",
"ms": 520
},
{
"t": "tap",
"x": 195,
"y": 760
},
{
"t": "wait",
"ms": 520
},
{
"t": "tap",
"x": 195,
"y": 760
},
{
"t": "wait",
"ms": 520
},
{
"t": "tap",
"x": 195,
"y": 760
},
{
"t": "wait",
"ms": 520
},
{
"t": "tap",
"x": 195,
"y": 760
},
{
"t": "wait",
"ms": 520
},
{
"t": "tap",
"x": 195,
"y": 760
},
{
"t": "wait",
"ms": 520
},
{
"t": "tap",
"x": 195,
"y": 760
},
{
"t": "wait",
"ms": 520
}
],
"expectedEngineCallPrefixes": [
"particles.",
"audio.synth"
],
"assertAfterPlay": [
{
"path": "inserted",
"op": "increased",
"why": "定时连发射针→已插入数上升;点击失灵/不响应则不升"
}
],
"expectLatch": true
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"inputs":{"src/core/plugin.js":{"bytes":44317,"imports":[],"format":"esm"},"src/plugins/runtime-probe/impl.js":{"bytes":16639,"imports":[],"format":"esm"},"host-dev/engine-caps.js":{"bytes":13897,"imports":[],"format":"esm"},"host-dev/engine-math.js":{"bytes":6155,"imports":[],"format":"esm"},"src/host/boot-game-host.js":{"bytes":27676,"imports":[{"path":"src/core/plugin.js","kind":"import-statement","original":"../core/plugin.js"},{"path":"src/plugins/runtime-probe/impl.js","kind":"import-statement","original":"../plugins/runtime-probe/impl.js"},{"path":"host-dev/engine-caps.js","kind":"import-statement","original":"../../host-dev/engine-caps.js"},{"path":"host-dev/engine-math.js","kind":"import-statement","original":"../../host-dev/engine-math.js"}],"format":"esm"},"src/plugins/collision/impl.js":{"bytes":24812,"imports":[],"format":"esm"},"src/plugins/physics-lite/impl.js":{"bytes":14507,"imports":[],"format":"esm"},"src/plugins/gamefeel/impl.js":{"bytes":21715,"imports":[],"format":"esm"},"src/plugins/particles-juice/impl.js":{"bytes":31830,"imports":[],"format":"esm"},"src/plugins/palette-post/impl.js":{"bytes":20240,"imports":[],"format":"esm"},"src/plugins/audio-music/impl.js":{"bytes":24659,"imports":[],"format":"esm"},"src/plugins/save-progress/impl.js":{"bytes":14608,"imports":[],"format":"esm"},"src/host/generic-host-config.js":{"bytes":7803,"imports":[{"path":"src/plugins/collision/impl.js","kind":"import-statement","original":"../plugins/collision/impl.js"},{"path":"src/plugins/physics-lite/impl.js","kind":"import-statement","original":"../plugins/physics-lite/impl.js"},{"path":"src/plugins/gamefeel/impl.js","kind":"import-statement","original":"../plugins/gamefeel/impl.js"},{"path":"src/plugins/particles-juice/impl.js","kind":"import-statement","original":"../plugins/particles-juice/impl.js"},{"path":"src/plugins/palette-post/impl.js","kind":"import-statement","original":"../plugins/palette-post/impl.js"},{"path":"src/plugins/audio-music/impl.js","kind":"import-statement","original":"../plugins/audio-music/impl.js"},{"path":"src/plugins/save-progress/impl.js","kind":"import-statement","original":"../plugins/save-progress/impl.js"}],"format":"esm"},"games/_wg1-gen/match3/generated-factory.js":{"bytes":8400,"imports":[],"format":"esm"},"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytes":640153,"imports":[],"format":"esm"},"games/_wg1-gen/match3/entry-bundle.js":{"bytes":6230,"imports":[{"path":"src/host/boot-game-host.js","kind":"import-statement","original":"../../../src/host/boot-game-host.js"},{"path":"src/host/generic-host-config.js","kind":"import-statement","original":"../../../src/host/generic-host-config.js"},{"path":"games/_wg1-gen/match3/generated-factory.js","kind":"import-statement","original":"./generated-factory.js"},{"path":"node_modules/littlejsengine/dist/littlejs.esm.js","kind":"dynamic-import","original":"littlejsengine"}],"format":"esm"}},"outputs":{"games/_wg1-gen/match3/bundle.iife.js":{"imports":[],"exports":[],"entryPoint":"games/_wg1-gen/match3/entry-bundle.js","inputs":{"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytesInOutput":164219},"games/_wg1-gen/match3/entry-bundle.js":{"bytesInOutput":507},"src/core/plugin.js":{"bytesInOutput":8989},"src/plugins/runtime-probe/impl.js":{"bytesInOutput":2794},"host-dev/engine-caps.js":{"bytesInOutput":1597},"host-dev/engine-math.js":{"bytesInOutput":498},"src/host/boot-game-host.js":{"bytesInOutput":5351},"src/plugins/collision/impl.js":{"bytesInOutput":1605},"src/plugins/physics-lite/impl.js":{"bytesInOutput":115},"src/plugins/gamefeel/impl.js":{"bytesInOutput":2533},"src/plugins/particles-juice/impl.js":{"bytesInOutput":4166},"src/plugins/palette-post/impl.js":{"bytesInOutput":4214},"src/plugins/audio-music/impl.js":{"bytesInOutput":6001},"src/plugins/save-progress/impl.js":{"bytesInOutput":3933},"src/host/generic-host-config.js":{"bytesInOutput":697},"games/_wg1-gen/match3/generated-factory.js":{"bytesInOutput":3296}},"bytes":211086}}}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,306 @@
'use strict';
/**
* Match-3 三消游戏工厂模块
* 竖屏 390×8448×8 宝石网格6 种颜色 20
*/
export default function createGame(opts) {
const o = opts || {};
let ctx = null;
let engine = null;
let nowMs = 0;
let sub = null;
const ROWS = 8, COLS = 8;
const CELL_SIZE = 40;
const BOARD_LEFT = 35;
const BOARD_TOP = 120;
const COLORS = 6;
const COLOR_STYLES = [
'#FF5733', '#33FF57', '#3357FF', '#F333FF', '#33FFF3', '#FFD733'
];
let board = []; // [row][col] = 0..5
let score = 0;
let movesLeft = 20;
let phase = 'booting';
let selected = null; // {row, col} | null
let gameoverReason = '';
let targets = [];
function cellCenter(row, col) {
return {
x: BOARD_LEFT + col * CELL_SIZE + CELL_SIZE / 2,
y: BOARD_TOP + row * CELL_SIZE + CELL_SIZE / 2
};
}
function updateTargets() {
targets = [];
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const cc = cellCenter(r, c);
targets.push({ x: cc.x, y: cc.y, idx: r * COLS + c, occupied: false });
}
}
}
// --------------------- 棋盘初始化(无三连) ---------------------
function initBoard() {
board = [];
for (let r = 0; r < ROWS; r++) {
board[r] = [];
for (let c = 0; c < COLS; c++) {
let color, tries = 0;
do {
color = Math.floor(ctx.random.range(0, COLORS));
tries++;
if (tries > 100) break;
} while (
(c >= 2 && board[r][c-1] === color && board[r][c-2] === color) ||
(r >= 2 && board[r-1][c] === color && board[r-2][c] === color)
);
board[r][c] = color;
}
}
updateTargets();
}
// --------------------- 消除检测 ---------------------
function findMatches() {
const set = new Set();
// 水平
for (let r = 0; r < ROWS; r++) {
let c = 0;
while (c < COLS) {
const col = board[r][c];
let end = c + 1;
while (end < COLS && board[r][end] === col) end++;
if (end - c >= 3) {
for (let i = c; i < end; i++) set.add(r * COLS + i);
}
c = end;
}
}
// 垂直
for (let c = 0; c < COLS; c++) {
let r = 0;
while (r < ROWS) {
const col = board[r][c];
let end = r + 1;
while (end < ROWS && board[end][c] === col) end++;
if (end - r >= 3) {
for (let i = r; i < end; i++) set.add(i * COLS + c);
}
r = end;
}
}
const out = [];
for (const idx of set) out.push({ row: Math.floor(idx / COLS), col: idx % COLS });
return out;
}
// 重力下落 + 顶部填充随机(不预防三连)
function gravityAndFill() {
let changed = false;
for (let c = 0; c < COLS; c++) {
let write = ROWS - 1;
for (let r = ROWS - 1; r >= 0; r--) {
if (board[r][c] !== -1) {
if (r !== write) {
board[write][c] = board[r][c];
board[r][c] = -1;
changed = true;
}
write--;
}
}
for (let r = write; r >= 0; r--) {
board[r][c] = Math.floor(ctx.random.range(0, COLORS));
changed = true;
}
}
return changed;
}
// 在指定位置爆发粒子的辅助函数
function spawnParticlesAt(x, y) {
if (!engine || !engine.particles) return;
try {
engine.particles.spawnEmitter({
pos: { x, y },
count: 20,
speed: 80,
particleTime: 0.5,
sizeStart: 10,
sizeEnd: 2,
colorStart: { r: 1, g: 0.9, b: 0.2, a: 1 },
colorEnd: { r: 1, g: 0.4, b: 0, a: 0 }
});
} catch (_) { /* ignore */ }
}
function playMatchSound() {
if (!engine || !engine.audio || !engine.audio.synth) return;
try {
engine.audio.synth.synthSfx([1, 0.02, 440, 0, 0, 0.12, 0, 1.4]);
} catch (_) { /* ignore */ }
}
// 执行消除闭环(尝试交换,若有消除则连锁消除;否则换回)
// 返回是否发生了消除
function trySwap(r1, c1, r2, c2) {
// 交换
[board[r1][c1], board[r2][c2]] = [board[r2][c2], board[r1][c1]];
let matches = findMatches();
if (matches.length === 0) {
// 换回
[board[r1][c1], board[r2][c2]] = [board[r2][c2], board[r1][c1]];
return false;
}
// 有消除 → 进入连锁循环
while (matches.length > 0) {
// 粒子 + 音效
for (const m of matches) {
const cc = cellCenter(m.row, m.col);
spawnParticlesAt(cc.x, cc.y);
}
playMatchSound();
// 计分(每块 10 分)
score += matches.length * 10;
// 标记移除
for (const m of matches) board[m.row][m.col] = -1;
// 下落填充
gravityAndFill();
// 再次检测
matches = findMatches();
}
updateTargets();
return true;
}
// --------------------- 输入处理 ---------------------
function onPointerDown(e) {
if (phase !== 'playing') return;
const col = Math.floor((e.x - BOARD_LEFT) / CELL_SIZE);
const row = Math.floor((e.y - BOARD_TOP) / CELL_SIZE);
if (row < 0 || row >= ROWS || col < 0 || col >= COLS) {
selected = null;
return;
}
if (selected === null) {
selected = { row, col };
return;
}
const sr = selected.row, sc = selected.col;
selected = null;
// 检查相邻
const dr = Math.abs(row - sr), dc = Math.abs(col - sc);
if (!((dr === 1 && dc === 0) || (dr === 0 && dc === 1))) {
// 非相邻 -> 选中新格子
selected = { row, col };
return;
}
// 消耗一步
movesLeft--;
// 尝试交换
const didMatch = trySwap(sr, sc, row, col);
if (!didMatch) {
// 无效交换,棋盘已恢复
}
// 检查步数是否耗尽
if (movesLeft <= 0) {
phase = 'gameover';
gameoverReason = 'no_moves';
}
}
// --------------------- 生命周期 ---------------------
function init(boot) {
ctx = boot.ctx;
engine = ctx.getEngine(); // 可能 null
phase = 'playing';
score = 0;
movesLeft = 20;
selected = null;
gameoverReason = '';
initBoard();
sub = ctx.getInput().on('pointerdown', onPointerDown);
}
function update(dt) {
nowMs = ctx.time.nowMs();
}
function render(g) {
// 背景
g.fillStyle = '#1a1a2e';
g.fillRect(0, 0, 390, 844);
// HUD
g.fillStyle = '#ffffff';
g.font = '20px monospace';
g.fillText('Score: ' + score, 20, 40);
g.fillText('Moves: ' + movesLeft, 20, 70);
if (phase === 'gameover') {
g.fillStyle = '#ff3333';
g.font = '28px monospace';
g.fillText('GAME OVER', 80, 420);
g.fillStyle = '#aaaaaa';
g.font = '16px monospace';
g.fillText('Tap to restart (not available)', 60, 450);
}
// 绘制宝石
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const val = board[r][c];
if (val === -1) continue;
const cx = BOARD_LEFT + c * CELL_SIZE + CELL_SIZE / 2;
const cy = BOARD_TOP + r * CELL_SIZE + CELL_SIZE / 2;
const radius = CELL_SIZE / 2 - 2;
// 圆形宝石
g.fillStyle = COLOR_STYLES[val];
g.beginPath();
g.arc(cx, cy, radius, 0, Math.PI * 2);
g.fill();
// 高光
g.fillStyle = 'rgba(255,255,255,0.25)';
g.beginPath();
g.arc(cx - 4, cy - 4, 5, 0, Math.PI * 2);
g.fill();
// 选中边框
if (selected && selected.row === r && selected.col === c) {
g.strokeStyle = '#ffffff';
g.lineWidth = 3;
g.beginPath();
g.arc(cx, cy, radius, 0, Math.PI * 2);
g.stroke();
}
}
}
}
function destroy() {
try { if (sub) sub.cancel(); } catch (_) {}
sub = null;
board = [];
selected = null;
}
// --------------------- 可测性视图 ---------------------
function _forensicsView() {
return {
seed: 0, // 宿主可忽略
state: () => ({
phase,
result: phase === 'gameover' ? 'done' : null,
score,
movesLeft,
board: board.map(row => row.slice()), // 深拷贝
targets: targets.map(t => ({ ...t }))
})
};
}
return { init, update, render, destroy, _forensicsView };
}

View File

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

View File

@ -0,0 +1,19 @@
{
"driver": {
"type": "tap-pairs",
"steps": 24,
"stepMs": 300
},
"expectedEngineCallPrefixes": [
"particles.",
"audio.synth"
],
"assertAfterPlay": [
{
"path": "score",
"op": "increased",
"why": "盲态交换相邻宝石→偶成三连得分上升;若全程凑不出三连(需读盘找可消)则不升=暴露 swipe/match 缺口"
}
],
"expectLatch": true
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"inputs":{"src/core/plugin.js":{"bytes":44317,"imports":[],"format":"esm"},"src/plugins/runtime-probe/impl.js":{"bytes":16639,"imports":[],"format":"esm"},"host-dev/engine-caps.js":{"bytes":13897,"imports":[],"format":"esm"},"host-dev/engine-math.js":{"bytes":6155,"imports":[],"format":"esm"},"src/host/boot-game-host.js":{"bytes":27676,"imports":[{"path":"src/core/plugin.js","kind":"import-statement","original":"../core/plugin.js"},{"path":"src/plugins/runtime-probe/impl.js","kind":"import-statement","original":"../plugins/runtime-probe/impl.js"},{"path":"host-dev/engine-caps.js","kind":"import-statement","original":"../../host-dev/engine-caps.js"},{"path":"host-dev/engine-math.js","kind":"import-statement","original":"../../host-dev/engine-math.js"}],"format":"esm"},"src/plugins/collision/impl.js":{"bytes":24812,"imports":[],"format":"esm"},"src/plugins/physics-lite/impl.js":{"bytes":14507,"imports":[],"format":"esm"},"src/plugins/gamefeel/impl.js":{"bytes":21715,"imports":[],"format":"esm"},"src/plugins/particles-juice/impl.js":{"bytes":31830,"imports":[],"format":"esm"},"src/plugins/palette-post/impl.js":{"bytes":20240,"imports":[],"format":"esm"},"src/plugins/audio-music/impl.js":{"bytes":24659,"imports":[],"format":"esm"},"src/plugins/save-progress/impl.js":{"bytes":14608,"imports":[],"format":"esm"},"src/host/generic-host-config.js":{"bytes":7803,"imports":[{"path":"src/plugins/collision/impl.js","kind":"import-statement","original":"../plugins/collision/impl.js"},{"path":"src/plugins/physics-lite/impl.js","kind":"import-statement","original":"../plugins/physics-lite/impl.js"},{"path":"src/plugins/gamefeel/impl.js","kind":"import-statement","original":"../plugins/gamefeel/impl.js"},{"path":"src/plugins/particles-juice/impl.js","kind":"import-statement","original":"../plugins/particles-juice/impl.js"},{"path":"src/plugins/palette-post/impl.js","kind":"import-statement","original":"../plugins/palette-post/impl.js"},{"path":"src/plugins/audio-music/impl.js","kind":"import-statement","original":"../plugins/audio-music/impl.js"},{"path":"src/plugins/save-progress/impl.js","kind":"import-statement","original":"../plugins/save-progress/impl.js"}],"format":"esm"},"games/_wg1-gen/multiball/generated-factory.js":{"bytes":12098,"imports":[],"format":"esm"},"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytes":640153,"imports":[],"format":"esm"},"games/_wg1-gen/multiball/entry-bundle.js":{"bytes":6230,"imports":[{"path":"src/host/boot-game-host.js","kind":"import-statement","original":"../../../src/host/boot-game-host.js"},{"path":"src/host/generic-host-config.js","kind":"import-statement","original":"../../../src/host/generic-host-config.js"},{"path":"games/_wg1-gen/multiball/generated-factory.js","kind":"import-statement","original":"./generated-factory.js"},{"path":"node_modules/littlejsengine/dist/littlejs.esm.js","kind":"dynamic-import","original":"littlejsengine"}],"format":"esm"}},"outputs":{"games/_wg1-gen/multiball/bundle.iife.js":{"imports":[],"exports":[],"entryPoint":"games/_wg1-gen/multiball/entry-bundle.js","inputs":{"node_modules/littlejsengine/dist/littlejs.esm.js":{"bytesInOutput":164197},"games/_wg1-gen/multiball/entry-bundle.js":{"bytesInOutput":507},"src/core/plugin.js":{"bytesInOutput":8989},"src/plugins/runtime-probe/impl.js":{"bytesInOutput":2794},"host-dev/engine-caps.js":{"bytesInOutput":1597},"host-dev/engine-math.js":{"bytesInOutput":498},"src/host/boot-game-host.js":{"bytesInOutput":5355},"src/plugins/collision/impl.js":{"bytesInOutput":1605},"src/plugins/physics-lite/impl.js":{"bytesInOutput":115},"src/plugins/gamefeel/impl.js":{"bytesInOutput":2533},"src/plugins/particles-juice/impl.js":{"bytesInOutput":4166},"src/plugins/palette-post/impl.js":{"bytesInOutput":4218},"src/plugins/audio-music/impl.js":{"bytesInOutput":6001},"src/plugins/save-progress/impl.js":{"bytesInOutput":3933},"src/host/generic-host-config.js":{"bytesInOutput":697},"games/_wg1-gen/multiball/generated-factory.js":{"bytesInOutput":4945}},"bytes":212721}}}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,167 @@
{
"gameId": "multiball",
"url": "http://localhost:4320/games/_wg1-gen/multiball/index.html",
"ts": null,
"guards": {
"A_boot": {
"pass": true
},
"C_frame": {
"pass": true,
"f0": 46,
"f1": 75,
"delta": 29
},
"I_control": {
"pass": true,
"paddlePath": "paddle.x",
"results": [
{
"tapX": 50,
"before": 194.99999999990354,
"after": 50,
"moved": -144.99999999990354,
"distToTarget": 0,
"pass": true
},
{
"tapX": 340,
"before": 50,
"after": 310,
"moved": 260,
"distToTarget": 30,
"pass": true
}
]
},
"D_render": {
"pass": true,
"bright": 146201,
"maxCh": 255
},
"E_live": {
"pass": true,
"distinctStates": 64,
"samples": 72
},
"B_uncaught": {
"pass": true,
"uncaught": []
},
"F_wiring": {
"pass": true,
"callCount": 15,
"sample": [
"audio.synth.synthSfx",
"audio.synth.synthSfx",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"particles.spawnEmitter"
],
"expected": [
"particles.",
"audio.synth"
]
},
"G_input": {
"pass": true,
"inputHash": "9cd794ce",
"controlHash": "6afc48cc",
"atFrame": 1984,
"ctrlFrame": -1
},
"H_progress": {
"pass": false,
"checks": [
{
"path": "remaining",
"op": "decreased",
"before": 40,
"after": 36,
"pass": true,
"why": "多球+挡板接球击碎砖→剩余下降;碰撞失效/高速穿透(CCD缺)则不降"
}
],
"latch": {
"pass": false,
"reason": "真玩未到达 gameover 终态(终态不可达/无失败态/play-spec 未驱动到结束)",
"phaseNow": "playing"
},
"state0": {
"phase": "playing",
"result": null,
"score": 0,
"remaining": 40,
"progress": 0,
"gameoverReason": "",
"ball": {
"x": 171,
"y": 793,
"vx": -103.2459330675854,
"vy": 317.75830848240577
},
"paddle": {
"x": 194.99999700380667,
"y": 804,
"w": 80
},
"balls": [
{
"x": 171,
"y": 793
},
{
"x": 187,
"y": 793
},
{
"x": 203,
"y": 793
}
]
},
"state1": {
"phase": "playing",
"result": null,
"score": 4,
"remaining": 36,
"progress": 0,
"gameoverReason": "",
"ball": {
"x": 171,
"y": 793,
"vx": -103.2459330675854,
"vy": 317.75830848240577
},
"paddle": {
"x": 171.00000000000003,
"y": 804,
"w": 80
},
"balls": [
{
"x": 171,
"y": 793
},
{
"x": 187,
"y": 793
},
{
"x": 203,
"y": 793
}
]
}
}
},
"pass": false,
"notes": []
}

View File

@ -0,0 +1,388 @@
'use strict';
/**
* 多球打砖块变体工厂模块
* 竖屏390×844底部挡板2-3砖块网格道具掉落胜负终态驻留
*/
export default function createGame(opts) {
const o = opts || {};
let ctx = null;
let phase = 'booting';
let gameoverReason = '';
let score = 0;
let remaining = 0;
let bricks = [];
let balls = [];
let items = [];
let paddle = { x: 0, y: 0, w: 80, h: 16 };
let pointerTargetX = 195;
let inputSubs = [];
let particles = null;
let audioSynth = null;
let math = null;
let paddleWidenTimer = 0;
const PADDLE_ORIG_W = 80;
const WIDTH = 390, HEIGHT = 844;
const BALL_RADIUS = 6;
const BALL_SPEED = 350;
const MAX_BALLS = 5;
const BRICK_ROWS = 5;
const BRICK_COLS = 8;
const BRICK_W = 44, BRICK_H = 18, BRICK_GAP = 2;
const BRICK_TOP_Y = 50;
const BRICK_LEFT_X = (WIDTH - (BRICK_COLS * (BRICK_W + BRICK_GAP) - BRICK_GAP)) / 2;
function init(boot) {
ctx = boot.ctx;
phase = 'playing';
gameoverReason = '';
score = 0;
remaining = 0;
bricks = [];
balls = [];
items = [];
paddleWidenTimer = 0;
paddle.w = PADDLE_ORIG_W;
paddle.x = (WIDTH - PADDLE_ORIG_W) / 2;
paddle.y = HEIGHT - 40;
// 砖块网格
for (let row = 0; row < BRICK_ROWS; row++) {
for (let col = 0; col < BRICK_COLS; col++) {
const x = BRICK_LEFT_X + col * (BRICK_W + BRICK_GAP);
const y = BRICK_TOP_Y + row * (BRICK_H + BRICK_GAP);
bricks.push({ x, y, w: BRICK_W, h: BRICK_H, alive: true });
remaining++;
}
}
// 初始球 (2~3)
const ballCount = 2 + (boot.seed ? (boot.seed % 2) : 0); // 2或3
for (let i = 0; i < ballCount; i++) {
const angle = Math.PI * (0.6 + 0.3 * i / ballCount);
const spd = BALL_SPEED + ctx.random.range(-20, 20);
const vx = Math.cos(angle) * spd;
const vy = Math.sin(angle) * spd;
if (vy > 0) {
balls.push({
x: paddle.x + paddle.w * (0.2 + 0.6 * i / ballCount),
y: paddle.y - BALL_RADIUS - 5,
vx, vy,
radius: BALL_RADIUS,
alive: true
});
}
}
// 引擎接口
const eng = ctx.getEngine();
if (eng) {
particles = eng.particles || null;
if (eng.audio && eng.audio.synth) audioSynth = eng.audio.synth;
math = eng.math || null;
}
// 输入订阅
const input = ctx.getInput();
if (input) {
const subDown = input.on('pointerdown', (e) => {
pointerTargetX = e.x;
});
const subMove = input.on('pointermove', (e) => {
pointerTargetX = e.x;
});
inputSubs.push(subDown, subMove);
}
pointerTargetX = WIDTH / 2;
}
function update(dt) {
if (phase !== 'playing') return;
// 挡板平滑跟随触摸点
const diff = pointerTargetX - paddle.x;
paddle.x += diff * 0.3;
paddle.x = Math.max(0, Math.min(WIDTH - paddle.w, paddle.x));
// 扩板计时
if (paddleWidenTimer > 0) {
paddleWidenTimer -= dt;
if (paddleWidenTimer <= 0) {
paddle.w = PADDLE_ORIG_W;
paddle.x = Math.max(0, Math.min(WIDTH - paddle.w, paddle.x));
}
}
// 球移动 + CCD
const SUB_STEP = 2;
for (let ball of balls) {
if (!ball.alive) continue;
const dx = ball.vx * dt;
const dy = ball.vy * dt;
const dist = Math.sqrt(dx * dx + dy * dy);
const steps = Math.max(1, Math.ceil(dist / SUB_STEP));
const stepDx = dx / steps;
const stepDy = dy / steps;
for (let s = 0; s < steps; s++) {
ball.x += stepDx;
ball.y += stepDy;
// 墙壁边界
if (ball.x - ball.radius < 0) { ball.x = ball.radius; ball.vx = -ball.vx; }
else if (ball.x + ball.radius > WIDTH) { ball.x = WIDTH - ball.radius; ball.vx = -ball.vx; }
if (ball.y - ball.radius < 0) { ball.y = ball.radius; ball.vy = -ball.vy; }
// 球掉出底部
if (ball.y + ball.radius > HEIGHT) {
ball.alive = false;
if (particles) {
try {
particles.spawnEmitter({
pos: { x: ball.x, y: HEIGHT },
count: 15, speed: 80, particleTime: 0.3,
colorStart: { r: 1, g: 0.2, b: 0.2, a: 1 },
colorEnd: { r: 0.8, g: 0, b: 0, a: 0 }
});
} catch (_) {}
}
break;
}
// 挡板碰撞
if (ball.vy > 0 &&
ball.y + ball.radius >= paddle.y - 1 &&
ball.y - ball.radius <= paddle.y + paddle.h + 1 &&
ball.x + ball.radius > paddle.x &&
ball.x - ball.radius < paddle.x + paddle.w) {
const hitPos = (ball.x - (paddle.x + paddle.w / 2)) / (paddle.w / 2);
const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
const newVx = Math.max(-speed, Math.min(speed, hitPos * 300));
const newVy = -Math.sqrt(speed * speed - newVx * newVx);
ball.vx = newVx;
ball.vy = newVy;
ball.y = paddle.y - ball.radius;
if (audioSynth) {
try { audioSynth.synthSfx([1, 0.02, 300, , , 0.1, , 0.8]); } catch (_) {}
}
}
// 砖块碰撞
for (let brick of bricks) {
if (!brick.alive) continue;
const cx = ball.x, cy = ball.y, r = ball.radius;
const rx = brick.x, ry = brick.y, rw = brick.w, rh = brick.h;
const nearestX = Math.max(rx, Math.min(cx, rx + rw));
const nearestY = Math.max(ry, Math.min(cy, ry + rh));
const dxb = cx - nearestX, dyb = cy - nearestY;
const distSq = dxb * dxb + dyb * dyb;
if (distSq <= r * r) {
brick.alive = false;
remaining--;
score++;
// 粒子爆发
if (particles) {
try {
particles.spawnEmitter({
pos: { x: brick.x + brick.w / 2, y: brick.y + brick.h / 2 },
count: 20, speed: 100, particleTime: 0.5,
colorStart: { r: 1, g: 0.9, b: 0.4, a: 1 },
colorEnd: { r: 1, g: 0.5, b: 0, a: 0 }
});
} catch (_) {}
}
if (audioSynth) {
try { audioSynth.synthSfx([1, 0.05, 440, , , 0.1, , 1.2]); } catch (_) {}
}
// 反弹
if (distSq > 0.01) {
const nl = Math.sqrt(distSq);
const nx = dxb / nl, ny = dyb / nl;
ball.x += nx * (r - nl);
ball.y += ny * (r - nl);
const dot = ball.vx * nx + ball.vy * ny;
ball.vx -= 2 * dot * nx;
ball.vy -= 2 * dot * ny;
} else {
ball.y = brick.y - r;
ball.vy = -ball.vy;
}
// 道具掉落 (20%)
if (ctx.random.next() < 0.2) {
const type = ctx.random.next() < 0.5 ? 'extra_ball' : 'widen';
items.push({
x: brick.x + brick.w / 2,
y: brick.y + brick.h / 2,
type,
radius: 6,
speed: 150
});
}
break; // 每子步只处理一块砖
}
}
}
}
balls = balls.filter(b => b.alive);
// 道具移动与收集
for (let i = items.length - 1; i >= 0; i--) {
const item = items[i];
item.y += item.speed * dt;
// 挡板收集
if (item.y + item.radius >= paddle.y &&
item.y - item.radius <= paddle.y + paddle.h &&
item.x + item.radius > paddle.x &&
item.x - item.radius < paddle.x + paddle.w) {
if (item.type === 'extra_ball') {
if (balls.length < MAX_BALLS) {
const angle = Math.PI * (0.6 + ctx.random.range(-0.15, 0.15));
const spd = BALL_SPEED + ctx.random.range(-20, 20);
balls.push({
x: paddle.x + paddle.w / 2,
y: paddle.y - BALL_RADIUS - 5,
vx: Math.cos(angle) * spd,
vy: Math.sin(angle) * spd,
radius: BALL_RADIUS,
alive: true
});
}
} else if (item.type === 'widen') {
paddle.w = PADDLE_ORIG_W * 1.5;
paddleWidenTimer = 7;
paddle.x = Math.max(0, Math.min(WIDTH - paddle.w, paddle.x));
}
// 收集粒子
if (particles) {
try {
particles.spawnEmitter({
pos: { x: item.x, y: item.y },
count: 10, speed: 60, particleTime: 0.3,
colorStart: { r: 0.2, g: 1, b: 0.2, a: 1 },
colorEnd: { r: 0, g: 0.8, b: 0, a: 0 }
});
} catch (_) {}
}
items.splice(i, 1);
continue;
}
if (item.y > HEIGHT + 20) items.splice(i, 1);
}
// 胜负判定
if (remaining === 0) {
phase = 'gameover';
gameoverReason = 'cleared_all';
} else if (balls.length === 0) {
phase = 'gameover';
gameoverReason = 'ball_lost';
}
}
function render(g) {
// 背景
g.fillStyle = '#0a0a1a';
g.fillRect(0, 0, WIDTH, HEIGHT);
// 砖块
for (let brick of bricks) {
if (!brick.alive) continue;
g.fillStyle = '#44aaff';
g.fillRect(brick.x, brick.y, brick.w, brick.h);
g.strokeStyle = '#3399ee';
g.lineWidth = 1;
g.strokeRect(brick.x, brick.y, brick.w, brick.h);
}
// 道具
for (let item of items) {
g.fillStyle = item.type === 'extra_ball' ? '#00ff88' : '#ffaa00';
g.beginPath();
g.arc(item.x, item.y, item.radius, 0, Math.PI * 2);
g.fill();
}
// 球
for (let ball of balls) {
if (!ball.alive) continue;
g.fillStyle = '#ffffff';
g.beginPath();
g.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
g.fill();
g.fillStyle = '#cccccc';
g.beginPath();
g.arc(ball.x - 2, ball.y - 2, ball.radius * 0.5, 0, Math.PI * 2);
g.fill();
}
// 挡板
g.fillStyle = '#ffaa44';
g.fillRect(paddle.x, paddle.y, paddle.w, paddle.h);
g.fillStyle = '#ffcc88';
g.fillRect(paddle.x, paddle.y, paddle.w, paddle.h / 2);
// HUD
g.fillStyle = '#ffffff';
g.font = '16px monospace';
g.fillText('Score: ' + score, 10, 28);
g.fillText('Remaining: ' + remaining, 10, 52);
g.fillText('Balls: ' + balls.length, 10, 76);
// 游戏结束画面
if (phase === 'gameover') {
g.fillStyle = 'rgba(0,0,0,0.6)';
g.fillRect(0, 0, WIDTH, HEIGHT);
g.fillStyle = '#ffcc00';
g.font = 'bold 32px sans-serif';
g.textAlign = 'center';
if (gameoverReason === 'cleared_all') {
g.fillText('YOU WIN!', WIDTH / 2, HEIGHT / 2 - 20);
} else {
g.fillText('GAME OVER', WIDTH / 2, HEIGHT / 2 - 20);
}
g.font = '18px sans-serif';
g.fillStyle = '#ffffff';
g.fillText('Tap to restart (not auto)', WIDTH / 2, HEIGHT / 2 + 40);
g.textAlign = 'left';
}
}
function destroy() {
for (let sub of inputSubs) {
try { sub.cancel(); } catch (_) {}
}
inputSubs = [];
phase = 'booting';
balls = [];
bricks = [];
items = [];
paddleWidenTimer = 0;
paddle.w = PADDLE_ORIG_W;
pointerTargetX = WIDTH / 2;
}
function _forensicsView() {
const mainBall = balls.length > 0
? { x: balls[0].x, y: balls[0].y, vx: balls[0].vx, vy: balls[0].vy }
: { x: 0, y: 0, vx: 0, vy: 0 };
const ballPositions = balls.map(b => ({ x: b.x, y: b.y }));
return {
seed: boot?.seed,
state: () => ({
phase,
result: gameoverReason === 'cleared_all' ? 'win' : (gameoverReason === 'ball_lost' ? 'lose' : null),
score,
remaining,
progress: 0,
gameoverReason,
ball: mainBall,
paddle: { x: paddle.x, y: paddle.y, w: paddle.w },
balls: ballPositions,
})
};
}
let boot;
return { init: (b) => { boot = b; return init(b); }, update, render, destroy, _forensicsView };
}

View File

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

View File

@ -0,0 +1,32 @@
{
"controlCheck": {
"paddlePath": "paddle.x",
"paddleY": 800,
"tapXs": [
50,
340
],
"tapsPerTarget": 6,
"moveMin": 25,
"tolerance": 70
},
"driver": {
"type": "paddle-intercept",
"ballPath": "ball.x",
"paddleY": 800,
"steps": 70,
"stepMs": 130
},
"expectedEngineCallPrefixes": [
"particles.",
"audio.synth"
],
"assertAfterPlay": [
{
"path": "remaining",
"op": "decreased",
"why": "多球+挡板接球击碎砖→剩余下降;碰撞失效/高速穿透(CCD缺)则不降"
}
],
"expectLatch": true
}

File diff suppressed because one or more lines are too long

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,118 @@
{
"gameId": "runner",
"url": "http://localhost:4320/games/_wg1-gen/runner/index.html",
"ts": null,
"guards": {
"A_boot": {
"pass": true
},
"C_frame": {
"pass": true,
"f0": 48,
"f1": 78,
"delta": 30
},
"I_control": {
"pass": true,
"skipped": "未声明 controlCheck向后兼容"
},
"D_render": {
"pass": true,
"bright": 304621,
"maxCh": 255
},
"E_live": {
"pass": true,
"distinctStates": 19,
"samples": 25
},
"B_uncaught": {
"pass": true,
"uncaught": []
},
"F_wiring": {
"pass": true,
"callCount": 47,
"sample": [
"particles.spawnEmitter",
"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",
"particles.spawnEmitter"
],
"expected": [
"particles.",
"audio.synth"
]
},
"G_input": {
"pass": true,
"inputHash": "4bf24e76",
"controlHash": "3cd12b81",
"atFrame": 813,
"ctrlFrame": 816
},
"H_progress": {
"pass": true,
"checks": [
{
"path": "score",
"op": "increased",
"before": 80,
"after": 90,
"pass": true,
"why": "自动跑→距离分上升(稳过);G 门验跳跃是否真改变存活、latch 验真撞死"
}
],
"latch": {
"pass": true,
"dwelledMs": 600,
"after": "gameover",
"reason": ""
},
"state0": {
"seed": null,
"phase": "playing",
"score": 80,
"remaining": null,
"progress": null,
"gameoverReason": "",
"result": null,
"player": {
"x": 80,
"y": 688,
"vy": 0,
"onGround": true
},
"nextObstacle": null
},
"state1": {
"seed": null,
"phase": "gameover",
"score": 90,
"remaining": null,
"progress": null,
"gameoverReason": "collision",
"result": "lose",
"player": {
"x": 80,
"y": 688,
"vy": 0,
"onGround": true
},
"nextObstacle": {
"x": 90.83333333333361
}
}
}
},
"pass": true,
"notes": []
}

View File

@ -0,0 +1,280 @@
'use strict';
/**
* 无尽跑酷工厂模块
* 逻辑视口 390×844竖屏
*/
export default function createGame(opts) {
const o = opts || {};
let ctx = null;
let bootData = null;
let phase = 'playing'; // 'playing' | 'gameover'
let score = 0;
let gameoverReason = '';
let player = { x: 80, y: 688, vy: 0, onGround: true }; // 固定xy为地面时圆心
let obstacles = [];
let lastSpawnTime = 0;
let spawnInterval = 1.2; // 秒
const SPEED = 350; // 障碍物左移 px/s
const SCORE_RATE = 100; // 分数增加速度 分/秒
const GRAVITY = 2000; // 重力 px/s²
const JUMP_VY = -620; // 跳跃初速度
const GROUND_Y = 688; // 玩家落地时圆心y
const PLAYER_R = 12;
const OBS_W = 30;
const OBS_H = 40;
const RESTART_BTN = { x: 100, y: 400, w: 190, h: 80 };
let sub = null;
let nowMs = 0;
// ---------- 引擎能力封装(防御) ----------
function emitParticles(x, y, colorStart, count) {
const eng = ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x, y },
count: count || 20,
speed: 120,
particleTime: 0.5,
colorStart: colorStart || { r: 1, g: 1, b: 1, a: 1 },
colorEnd: { r: 0, g: 0, b: 0, a: 0 },
});
} catch (_) {}
}
}
function playSfx(params) {
const eng = ctx.getEngine();
if (eng && eng.audio && eng.audio.synth) {
try { eng.audio.synth.synthSfx(params); } catch (_) {}
}
}
// ---------- 游戏状态重置 ----------
function resetGame() {
phase = 'playing';
score = 0;
gameoverReason = '';
player = { x: 80, y: GROUND_Y, vy: 0, onGround: true };
obstacles = [];
lastSpawnTime = 0;
spawnInterval = 1.2;
}
// ---------- 初始 ----------
function init(boot) {
ctx = boot.ctx;
bootData = boot;
resetGame();
// 订阅输入
sub = ctx.getInput().on('pointerdown', onTap);
// 演示引擎能力:常驻一个几乎不可见的粒子(可选)
const eng = ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x: -100, y: -100 }, // 屏幕外
emitRate: 0.1,
particleTime: 0.1,
speed: 1,
colorStart: { r: 0, g: 0, b: 0, a: 0 }
});
} catch (_) {}
}
}
// ---------- 输入处理 ----------
function onTap(e) {
const x = e.x, y = e.y;
if (phase === 'gameover') {
// 点击重玩区域
const rb = RESTART_BTN;
if (x >= rb.x && x <= rb.x + rb.w && y >= rb.y && y <= rb.y + rb.h) {
resetGame();
emitParticles(player.x, player.y, { r: 0, g: 1, b: 0.5, a: 1 }, 25);
playSfx([1, 0.05, 600, , , 0.1, , 1.5]);
}
return;
}
// playing 状态 → 跳跃
if (player.onGround) {
player.vy = JUMP_VY;
player.onGround = false;
emitParticles(player.x, player.y + PLAYER_R, { r: 1, g: 1, b: 0, a: 1 }, 16);
playSfx([1, 0.05, 400, , , 0.12, , 1.3]);
}
}
// ---------- 更新 ----------
function update(dt) {
nowMs = ctx.time.nowMs();
if (phase !== 'playing') return;
// 分数
score += dt * SCORE_RATE;
// 玩家垂直运动
if (!player.onGround) {
player.vy += GRAVITY * dt;
player.y += player.vy * dt;
// 落地检测
if (player.y >= GROUND_Y) {
player.y = GROUND_Y;
player.vy = 0;
player.onGround = true;
// 落地轻反馈(可选)
emitParticles(player.x, player.y + PLAYER_R, { r: 0.5, g: 0.7, b: 1, a: 0.6 }, 8);
}
}
// 移动障碍物
for (let i = obstacles.length - 1; i >= 0; i--) {
obstacles[i].x -= SPEED * dt;
if (obstacles[i].x < -OBS_W) {
obstacles.splice(i, 1);
}
}
// 生成新障碍物
const tSec = nowMs / 1000;
if (tSec - lastSpawnTime >= spawnInterval) {
const interval = ctx.random.range(0.6, 1.0); // 密度:不跳必撞
spawnInterval = interval;
lastSpawnTime = tSec;
obstacles.push({
x: 390 + 10,
y: 700 - OBS_H, // 地面顶部 y=700矩形底部对齐
w: OBS_W,
h: OBS_H
});
}
// 碰撞检测(圆形 vs 矩形)
const px = player.x, py = player.y;
for (let obs of obstacles) {
// 矩形中心
const ox = obs.x + obs.w / 2;
const oy = obs.y + obs.h / 2;
const dx = Math.abs(px - ox);
const dy = Math.abs(py - oy);
if (dx < PLAYER_R + obs.w / 2 && dy < PLAYER_R + obs.h / 2) {
// 碰撞
phase = 'gameover';
gameoverReason = 'collision';
emitParticles(px, py, { r: 1, g: 0.2, b: 0.2, a: 1 }, 40);
playSfx([1, 0.2, 80, , , 0.15, , 0.7]);
break;
}
}
}
// ---------- 渲染 ----------
function render(g) {
// 背景
g.fillStyle = '#1a1a2e';
g.fillRect(0, 0, 390, 844);
// 地面
g.fillStyle = '#2d4059';
g.fillRect(0, 700, 390, 144);
g.strokeStyle = '#4ecca3';
g.lineWidth = 3;
g.beginPath();
g.moveTo(0, 700);
g.lineTo(390, 700);
g.stroke();
// 障碍物
g.fillStyle = '#ff5722';
for (let obs of obstacles) {
g.fillRect(obs.x, obs.y, obs.w, obs.h);
// 高光
g.fillStyle = '#ff8a65';
g.fillRect(obs.x + 4, obs.y + 4, obs.w - 8, 6);
g.fillStyle = '#ff5722';
}
// 角色(圆形)
g.fillStyle = '#e23e57';
g.beginPath();
g.arc(player.x, player.y, PLAYER_R, 0, Math.PI * 2);
g.fill();
// 眼睛
g.fillStyle = '#ffffff';
g.beginPath();
g.arc(player.x - 4, player.y - 4, 3, 0, Math.PI * 2);
g.arc(player.x + 4, player.y - 4, 3, 0, Math.PI * 2);
g.fill();
g.fillStyle = '#000';
g.beginPath();
g.arc(player.x - 4, player.y - 4, 1.5, 0, Math.PI * 2);
g.arc(player.x + 4, player.y - 4, 1.5, 0, Math.PI * 2);
g.fill();
// HUD 顶部信息
g.fillStyle = '#ffffff';
g.font = '18px monospace';
g.fillText('Score: ' + Math.floor(score), 10, 30);
g.fillText('Status: ' + (phase === 'playing' ? 'Running' : 'Game Over'), 10, 55);
// Game Over 画面 + 重玩按钮
if (phase === 'gameover') {
g.fillStyle = 'rgba(0,0,0,0.5)';
g.fillRect(0, 350, 390, 200);
g.fillStyle = '#e94560';
const rb = RESTART_BTN;
g.fillRect(rb.x, rb.y, rb.w, rb.h);
g.fillStyle = '#ffffff';
g.font = '22px monospace';
g.fillText('Tap to Restart', rb.x + 18, rb.y + 50);
g.font = '16px monospace';
g.fillText('You crashed! Score: ' + Math.floor(score), 100, rb.y - 20);
}
}
// ---------- 销毁 ----------
function destroy() {
try { sub && sub.cancel(); } catch (_) {}
sub = null;
obstacles = [];
}
// ---------- 可测性接口 ----------
function _forensicsView() {
function state() {
// 前方最近障碍物
let nextObs = null;
for (let obs of obstacles) {
if (obs.x > player.x) {
if (nextObs === null || obs.x < nextObs.x) {
nextObs = { x: obs.x };
}
}
}
return {
seed: null,
phase: phase,
score: Math.floor(score),
remaining: null,
progress: null,
gameoverReason: gameoverReason,
result: phase === 'gameover' ? 'lose' : null,
player: {
x: player.x,
y: player.y,
vy: player.vy,
onGround: player.onGround
},
nextObstacle: nextObs
};
}
return { seed: null, state };
}
return { init, update, render, destroy, _forensicsView };
}

View File

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

View File

@ -0,0 +1,119 @@
{
"inputs": [
{
"t": "wait",
"ms": 500
},
{
"t": "tap",
"x": 195,
"y": 420
},
{
"t": "wait",
"ms": 500
},
{
"t": "tap",
"x": 195,
"y": 420
},
{
"t": "wait",
"ms": 500
},
{
"t": "tap",
"x": 195,
"y": 420
},
{
"t": "wait",
"ms": 500
},
{
"t": "tap",
"x": 195,
"y": 420
},
{
"t": "wait",
"ms": 500
},
{
"t": "tap",
"x": 195,
"y": 420
},
{
"t": "wait",
"ms": 500
},
{
"t": "tap",
"x": 195,
"y": 420
},
{
"t": "wait",
"ms": 500
},
{
"t": "tap",
"x": 195,
"y": 420
},
{
"t": "wait",
"ms": 500
},
{
"t": "tap",
"x": 195,
"y": 420
},
{
"t": "wait",
"ms": 500
},
{
"t": "tap",
"x": 195,
"y": 420
},
{
"t": "wait",
"ms": 500
},
{
"t": "tap",
"x": 195,
"y": 420
},
{
"t": "wait",
"ms": 500
},
{
"t": "tap",
"x": 195,
"y": 420
},
{
"t": "wait",
"ms": 500
}
],
"expectedEngineCallPrefixes": [
"particles.",
"audio.synth"
],
"assertAfterPlay": [
{
"path": "score",
"op": "increased",
"why": "自动跑→距离分上升(稳过);G 门验跳跃是否真改变存活、latch 验真撞死"
}
],
"expectLatch": true
}

File diff suppressed because one or more lines are too long

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More