feat(wg1): driver-coverage 闭口 + P2 三档对照——flappy/multiball/asteroids 翻绿,0 模型造不出

创始人 2026-06-15 定档「driver 残留闭口 + P2 对照(L1 自主)」;P1 校准放量 blocked-on-founder。

- play.cdp.cjs driver 库 1→8 型:+flap-to-gap vy 前瞻 / paddle-intercept 弃守排空 / drag-aiming(第7) / aim-fire(第8)
- flappy (重生成 score 0→5,vy 前瞻+可达缝隙 brief)
- multiball (重生成首发 retries=0,弃守排空 driver+底部丢球语义 brief)
- asteroids (enrich exportState 暴露陨石坐标+aim-fire driver,score 0→6=可测性边界转确定性闭口范式)
- t2048(8/9,键盘桥已修 score 0→4;latch=2048 抗死锁体裁边界)/ angrybirds(8/9,隐藏弹道物理可测性边界)honest 留界
- 三分类铁论:0 模型造不出 / 0 3D 硬界——全是宿主键盘桥(L0 已修)/driver 覆盖(我域已增强)/可测性边界(游戏 exportState)
- P2:同款 Breakout 三档对照(flash ¥0.194/M2.7 ¥0.514/pro ¥0.417)+ 创始人锚(pro 碰撞 bug 被两便宜 player 假绿=人锚不可替代)
- 蒸馏:game-e2e §8(driver 1→8 型 + H 假阴四因归因铁律)、cheap-model §9(重生成区分实例bug/可测性/模型短板);W-G1 spec 执行进展回填;新增 L1 闭口报告
- 顺带:run.py/prompt.py 注释 六门→九门 对齐(我域,trivial)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-15 01:58:43 -07:00
parent 3c6e3d4ad0
commit cc5fa486ac
41 changed files with 1607 additions and 1401 deletions

View File

@ -78,3 +78,4 @@ flash 打底放量 14 款(主力 8 + 压测 6),Opus 对抗审计核实(2 个 ult
- **★ 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`)。
- **★ 规避/推理族:题面须自产「安全位」+ 断言用胜利终态(saolei borderline 修复,2026-06-15)**:tap-targets 盲点首个对扫雷会盲踩雷秒负、核心玩法零暴露(门弱断言被 lose 路径满足→ false-borderline)。修法 = 题面令 `_forensicsView` 自产 `safe`(测试专用 x-ray,=`!cell.mine`,**勿渲染/勿给玩家避雷提示**)→ harness driver `safeOnly:true` 只揭安全格到 win → 断言改 `result=="win"`。**flash 一次过且自产 safe 正确**(`generated-factory.js:337`)→ **坐实缺口在题面、非模型能力**。通则:凡「盲点首个会致负」的家族(扫雷类),题面给 oracle 安全位 + driver 避负 + 断言用胜利终态;门只证「机制点亮+可赢可达」,好不好玩归 M3+创始人。详见 [`2026-06-15-WG1-tap-targets-driver-自产缺口-L1brief.md`](../../docs/agent-specs/2026-06-15-WG1-tap-targets-driver-自产缺口-L1brief.md)。
- **★ driver-coverage 6 款闭口 + 重生成归因(2026-06-15)**:scale-20 余 6 款 driver-coverage 失败全部归因(详 [`driver-coverage 闭口报告`](../../docs/agent-specs/2026-06-15-WG1-driver-coverage闭口与P2对照-L1报告.md) + [harness skill §8 四因铁律](./game-e2e-cdp-harness.md))——**0 个模型造不出 / 0 个 3D 硬界**:flappy/multiball **重生成即过**(原件『缝隙贴顶』/『底部丢球未实现』=**实例缺陷非模型短板**,收紧 brief + 增强 drivervy 前瞻 / 弃守排空〕后 flash 翻绿,multiball retries=0);t2048/asteroids 宿主键盘桥(L0 已修)重打包即活;angrybirds/asteroids 盲玩=可测性边界(asteroids 已 enrich `asteroids:[{x,y}]` + aim-fire driver 转确定性瞄准)。**生成侧铁律:单件失败禁直接归『模型造不出』,必重生成区分『实例 bug / 可测性缺口 / 模型短板』**——『模型造不出』是最大伪报源(原 flappy『升 pro 仍挂=模型天花板』被本轮证伪=driver+实例)。

View File

@ -76,18 +76,28 @@
## 8. scale-20 放量实证:driver 库泛化 + 门承重件 + 对抗审计硬化(W-G1 lane v3,2026-06-14)
**适配 driver 库 1→6 型 + drag 输入事件**(`play.cdp.cjs`,`spec.driver.type` 分发;盲打打不动技巧/实时游戏=H 假阴根因,故按机制族建 driver):
**适配 driver 库 1→8 型 + drag 输入事件**(`play.cdp.cjs`,`spec.driver.type` 分发;盲打打不动技巧/实时游戏=H 假阴根因,故按机制族建 driver):
| driver | 机制族 | 读什么自动出招 |
|---|---|---|
| `paddle-intercept` | 挡板接球 | `ball.x` → 挡板对齐 |
| `paddle-intercept` | 挡板接球 | `ball.x` → 挡板对齐;**`drainAfter` 声明则跑满后【弃守排空】(挡板停 `drainX` 角落)让球漏光→打出 lose 终态(接球类靠一直接球永不失败→latch 不可达的解,multiball)** |
| `tap-targets` | 点击离散目标 | `state.targets[{x,y,occupied[,safe]}]` → 点未占用;**规避/推理族(扫雷)配 `safeOnly:true` 只点 `safe===true`、确定性避负把核心循环跑到 win**(井字棋/扫雷/打地鼠/Simon/invaders…) |
| `flap-to-gap` | 重力穿缝 | `bird.y` vs `nextGap.centerY` → 偏低拍翅 |
| `flap-to-gap` | 重力穿缝 | `bird.y` + **`bird.vy` 速度前瞻**(`位置+vy×lookahead` 预测仍偏低才拍、`aimBias` 瞄缝心上方留下落余量)→解高位缝/快下落『追不上·过冲』(flappy);缺 vy 退化为纯位置反应式 |
| `seek-x` | 横向趋近接台 | `bird.x` vs `nextPlatform.x` → 点左/右半 |
| `tap-pairs` | 相邻交换(三消) | `targets[k]`/`[k+1]` 成对点 |
| `key-cycle` | 纯按键 | 循环按 `keys[]` 到 gameover(**依赖宿主键盘桥,见下** ) |
| `key-cycle` | 纯按键 | 循环按 `keys[]` 到 gameover(**依赖宿主键盘桥**);填充型(2048)用 2 向键(如 `Up+Right`)比 4 向更快逼满盘,但**抗死锁类游戏仍难确定性到 latch=边界** |
| `drag-aiming` | 抛射瞄准(愤怒小鸟) | 读 `targets[{x,y}]` 按 origin→target 反解拖拽矢量发射 + `powers[]` 力度扫描兜未知射程 + `upLead` 补抛物下坠;**发射物理常数(力度↔速度比/重力)未暴露→盲扫无法连中=可测性边界** |
| `aim-fire` | 射击瞄准(Asteroids) | 读 `ship{x,y,angle}`+`targets[{x,y}]` 选最近,旋转使 `|atan2方位-angle|` 收敛(**旋向自适应**:diff 变大翻向,免猜 CW/CCW)→对齐即开火;**须游戏 exportState 暴露目标坐标**(无坐标=可测性边界) |
固定输入新增 `{t:'drag',from,to,ms}`(touchStart→插值 touchMove→touchEnd,合成 swipe/拖拽)。**driver 覆盖度 = 门能否兜住的前提**:shooter/填充型 latch 需足够步数驱动到终局(invaders/tetris/multiball 的 latch 假阴皆此因,非游戏坏;invaders 步数 30→50 即翻绿,实证)。
固定输入新增 `{t:'drag',from,to,ms}`(touchStart→插值 touchMove→touchEnd,合成 swipe/拖拽)。**driver 覆盖度 = 门能否兜住的前提**:shooter/填充型 latch 需足够步数驱动到终局(invaders 步数 30→50 即翻绿,实证)。
**★ driver-coverage v2 闭口:H 假阴四因归因铁律(2026-06-15,5 款实测)**。技巧/射击/填充类 H 假阴必先分因、**禁『模型造不出』笼统归因**(spec §9):
1. **宿主门面缺失**(系统性,L0 域):如键盘桥 `d754b71`——**失败严格沿『键盘 vs 指针』分界=最强信号**;宿主经 esbuild 打进 bundle,故修后须**重打包** bundle 才生效(`node scripts/build.mjs <id>/entry-bundle.js <id>/bundle.iife.js --global-name=__GameBundle`)→实证 t2048 `score 0→4`、asteroids `F_wiring 0→26`
2. **driver 太朴素**(我域,改 `play.cdp.cjs`):本轮增 flap-to-gap vy 前瞻 / paddle-intercept 弃守排空 / drag-aiming / aim-fire 四件;flappy・multiball 经此 + 重生成翻绿。
3. **可测性边界**(游戏 `exportState` 不足以确定性盲玩):asteroids 无陨石坐标、angrybirds 隐藏弹道物理。**正解=enrich exportState 暴露目标坐标 + 适配 driver**(asteroids 已 enrich `asteroids:[{x,y}]` + aim-fire 转确定性瞄准);弹道物理类(angrybirds)须暴露物理常数,否则留边界。**抗死锁类(2048)的 latch 本身是设计性边界**(游戏意在不结束),宜放宽为强 progress(score↑∧board变)+G 兜。
4. **生成实例缺陷 ≠ 模型能力**:multiball 原件底部丢球未实现(latch 不可达)、flappy 原件缝隙贴顶,但 flash 在**收紧 brief**(『球越底沿即出局不反弹』/『缝心落可达区间』)下**重生成即过**(multiball retries=0)。**区分『此实例 bug』与『模型短板』必须重生成验证,不可凭单件下结论**——这是『模型造不出』的最大伪报源。
**结论(scale-20 的 driver-coverage 失败款)**:0 个是模型能力天花板,0 个是 3D 硬界——真难点恒在 **harness driver 覆盖 + 宿主门面完备 + 游戏可测性导出**三者。
**★ 地板真承重件 = `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 兜。

View File

@ -171,3 +171,15 @@ flowchart TB
- **[assumption·核心待验]** 便宜 player 判好玩跟人类相关——靠 §6 校准定论;不相关则退亲玩。
- **[assumption]** agent-scope lift 显著为正(harness 复利)——单变量/held-out 测;证伪则记模型天花板。
- **[守护栏·创始人确认]** 只改我方 agent 层、AgentScope 可替换不 fork、Opus 建好即退不进生产、便宜模型生产 in-loop、模型 per-agent 可配置可路由。
---
## 执行进展回填(L1,2026-06-15)
> 本轮创始人定档「driver 残留闭口 + P2 对照(L1 自主)」;P1 校准放量 blocked-on-founder。详见 [`driver-coverage 闭口与 P2 对照报告`](2026-06-15-WG1-driver-coverage闭口与P2对照-L1报告.md)。
- **P0 跑通 ✅**:studio 多角色闭环(设计→代码→七门→player panel→自修复)Breakout 端到端真跑通(非桩),Stage1/Stage1_mmx/Stage2 三档均有数据。
- **P1 校准 ⚠️(N=4 命门结论已出,放量 blocked-on-founder)**:`calibration/report.json` 实证 **M3视觉/flash文本 player `gatekeeper_ok=false`**——便宜 player 高估碰撞 bug 件(pro 件两 player 给 fun4,创始人亲玩 fun2/fix),坐实 review §8「player panel 有结构性天花板、人锚不可替代」。放量需创始人对更多款亲玩补 `labels.json` 锚。
- **P2 对比 ✅(N=1 Breakout)**:同款三档对照表(flash ¥0.194 / M2.7 ¥0.514 / pro ¥0.417;过门率/自修复轮/¥/player 分 + 创始人锚)已成。多题面横比留 P3。
- **driver-coverage 6 款闭口 ✅**:scale-20 余 6 款失败全部归因,**0 个模型造不出 / 0 个 3D 硬界**。harness driver 1→8 型(新增 flap-to-gap vy 前瞻 / paddle-intercept 弃守排空 / drag-aiming / aim-fire),**四因归因铁律**(宿主门面 / driver 太朴素 / 可测性边界 / 实例缺陷≠模型)入 [`game-e2e skill §8`](../../.agents/skills/game-e2e-cdp-harness.md)。
- **P3 评分矩阵 / agent-loop-v1 处置 / 全 20 款补全**:留后续波次。

View File

@ -0,0 +1,86 @@
# 《W-G1 · driver-coverage 残留闭口 + P2 三档对照》L1 闭口报告
> **状态**:执行记录 · 2026-06-15 · L1(lili-mac)本地真玩取证 · lane `wg1/bakeoff-studio`(基于 origin/dev/2.0.0 c77f7a1)。
> **范围**:创始人 2026-06-15 定档「driver 残留闭口 + P2 对照(L1 自主)」,P1 校准放量标 blocked-on-founder(已有 N=4 命门结论,见 calibration/report.json)。
> **铁律**:零碰 L0(`game-cloud`/`game-runtime/src`/契约/前端/feed);只改我域 `game-runtime/games/_wg1-gen/**`(harness `play.cdp.cjs` + 各款 play-spec)与 `wg1/gen-worker/**`(briefs);宿主键盘桥(`boot-game-host.js`)只**消费重打包**、不修改。每条结论标【实测/推断/假设】。
---
## 1. 结论先行
1. **5 款 driver-coverage 失败件,0 个是「模型造不出」、0 个是「3D 硬界」**——全部是 ① 宿主键盘桥(L0 已修,重打包消费即生效)、② harness driver 太朴素(我域,已增强)、③ 可测性边界(游戏 exportState 不足)。**终局:flappy/multiball/asteroids 3 款翻绿(✅九门全过);t2048(8/9,latch=2048 抗死锁体裁边界)+ angrybirds(8/9,隐藏弹道物理边界)honest 留界**。【实测】
2. **共享 driver 库 `play.cdp.cjs` 增四件**(可复用、向后兼容):flap-to-gap 加**速度前瞻**、paddle-intercept 加**弃守排空相位**、新增 **drag-aiming 第 7 型 + aim-fire 第 8 型**。【实测】
3. **键盘款重打包即翻活**:t2048 `score 0→4`(真合并)、asteroids `F_wiring 0→41`(真旋向瞄准击碎 6 颗)——证 d754b71 键盘桥修复有效,旧产物是「修复前」陈值;**asteroids 再经 enrich 陨石坐标 + aim-fire driver 转确定性闭口(score 0→6 ✅)**。【实测】
4. **P2 同款 Breakout 三档(flash/M2.7/pro)对照已成表**;叠加创始人亲玩锚——**强档 pro 反而被判碰撞 bug(fun2/fix)**,印证「过门率/便宜 player 高分 ≠ 真手感」,人锚不可替代。【实测】
---
## 2. driver-coverage 5 款归因(逐款实测)
| 款 | 输入族 | 修前卡点 | 根因归类 | 处置 | 结果 |
|---|---|---|---|---|---|
| **flappy** | 触摸·重力穿缝 | H:score 0(撞下管) | driver(纯反应式无前瞻)+原实例缝隙过高 | flap-to-gap 加 vy 前瞻 + brief 补可达缝隙约束 + 重生成 | ✅ flash 重生成 **score 0→5**、缝隙 centerY 174→302 可达、latch✓(retries=2,wall 502s) |
| **multiball** | 触摸·挡板接球 | H:progress✓ latch✗(球不漏) | 原实例底部丢球未实现 + driver 无排空 | paddle-intercept 加弃守排空 + brief 补丢球语义 + 重生成 | ✅ flash 重生成**首发过**(retries=0)、remaining 78→69、弃守排空→lose latch✓ |
| **angrybirds** | 触摸·抛射 | H:score 0(盲拖打不中) | **可测性边界**(发射物理常数未暴露) | 新增 drag-aiming(读猪坐标反解拖拽矢量+力度扫描) | ❌ 全鸟发射仍 0 命中→边界坐实 |
| **t2048** | 键盘·合并 | H:键盘死→score 0 | 宿主键盘桥(L0 已修)+ **终态可达边界** | 重打包(键盘活)+ 2 向填充逼死锁 | ⚠️ `score 0→4`(progress✓)、latch✗(2048 设计抗死锁,盲态难逼满盘) |
| **asteroids** | 键盘·射击 | H:键盘死→score 0、F 0 | 宿主键盘桥(L0 已修)+ 可测性边界(无陨石坐标) | 重打包 + **enrich exportState 暴露陨石坐标 + aim-fire driver** | ✅ flash 重生成**首发过**(retries=0)、**score 0→6**(F 0→41 真旋向瞄准命中)、latch✓ |
**逐款实测细节:**
- **angrybirds**【实测·边界】:drag-aiming 真发射全部 4 鸟(birdsLeft 4→0、F_wiring=18、latch✓),但加密力度扫描([150,200,240,180,220,250]×upLead95)仍 0 命中——`_forensicsView` 只暴露 `targets[{x,y}]`,**不暴露发射力度↔速度比例 / 重力 / 鸟飞行中位置**,确定性瞄准无解、盲扫无法连中。**游戏本体 sound(发射/抛物/倒塌/用尽→lose 驻留全对),非模型造不出**。闭口需游戏侧暴露弹道物理或提供「按目标发射」affordance(可测性红线),列后续。
- **t2048**【实测·边界】:重打包到已修宿主后 `score 0→4`(真键盘合并)、G_input✓、余 8 门✓——**键盘桥(原 P0 阻塞)已解**。仅 latch✗:2048 设计上玩家**不想**到 gameover,确定性盲态 driver(4 向循环/2 向填充)难在步数内逼出「满盘且四向无解」。**游戏 sound**;latch 需近解级填盘策略或放宽为「progress(score↑/board变)+ G」兜——后者证真已足。
- **asteroids**【实测·✅已闭】:重打包后键盘真生效(`F_wiring 0→26`、撞陨石死→latch✓),但旋转扫射 score 0(无陨石坐标无法瞄准)。**正确闭法=enrich `exportState` 暴露 `asteroids:[{x,y}]` + aim-fire driver(读 ship.angle 自适应旋向最近陨石→对齐开火)**:flash 重生成**首发过**(score 0→6、F 0→41、latch✓)。**这是「可测性边界 → 暴露目标坐标 + 适配 driver → 确定性闭口」的范式**(优于盲扫,可复用于一切射击/瞄准类)。
---
## 3. 可复用 driver 改进(`play.cdp.cjs`,向后兼容)
1. **flap-to-gap 速度前瞻**:新增 `vyPath`(默认 `bird.vy`)+ `lookahead`(默认 0.18s)+ `aimBias`(默认 24px)。判据从「当前位置偏低就拍」升级为「`位置 + 速度×lookahead` 预测仍偏低才拍」——早拍防追不上、晚拍防过冲;`aimBias` 瞄缝心上方留下落余量。缺 vy 自动退化为纯位置反应式(老行为)。
2. **paddle-intercept 弃守排空**:新增 `drainAfter`(默认 null=不排空,打砖块照旧)+ `drainX`。跑满 `drainAfter` 步证进展后,挡板停角落不再追球→球漏光→触发 lose/gameover,把「接球类靠一直接球永不失败」的真终态(latch)打出来。
3. **drag-aiming 第 7 型(新)**:抛射瞄准(愤怒小鸟类)。读 `targets[{x,y}]` 选最近目标,按 `origin→target` 方向**反解拖拽矢量**发射,`powers[]` 力度扫描兜未知射程 + `upLead` 补抛物下坠。**确定性尽力,非保证**;全 miss=可测性边界(物理常数未暴露),非模型造不出。
4. **aim-fire 第 8 型(新,Asteroids/射击类)**:读 `ship{x,y,angle}`+`targets[{x,y}]` 选最近目标,旋转使 `|目标方位(atan2)-angle|` 收敛→对齐即开火;旋向**自适应**(上拍 diff 变大就翻向,免猜 CW/CCW 角度约定)。**依赖游戏 exportState 暴露目标坐标**——把「无坐标盲射=可测性边界」转成「暴露坐标即可确定性瞄准」的真闭口(asteroids brief 已 enrich 出 `asteroids:[{x,y}]`)。
> driver 六律承袭(game-e2e skill §2):坐标走 rect 映射、触摸成对+步进、zone/phase 名以源码为真相、覆盖度=门能否兜住的前提。新增**第 7 律(本轮)**:技巧/射击类的 H 假阴须先分「宿主门面缺失(系统性,如键盘桥)/ driver 太朴素(我域改) / 状态导出不足(可测性边界,游戏侧补 exportState)」三因,**禁用『模型造不出』笼统归因**(spec §9)。
---
## 4. P2 Breakout 同款三档对照(已有数据成表)
同 brief(breakout)、同 studio 多角色闭环,仅换模型阶段:
| 阶段 | 文本模型 | 视觉 | 过门 | 自修复轮 | player 轮 | M3视觉 fun/判 | flash/pro文本 fun/判 | ¥/款 | 墙钟 |
|---|---|---|---|---|---|---|---|---|---|
| stage1 | deepseek-v4-flash | M3 | ✅ | 5 | 3 | 3/fix | 3/pass | **0.194** | 616s |
| stage1_mmx | MiniMax-M2.7 | M3 | ✅ | 6 | 3 | 2/fix | 4/pass | **0.514** | 296s |
| stage2 | deepseek-v4-pro | M3 | ✅ | 3 | 3 | 4/fix | 4/pass | **0.417** | 745s |
**读表**【实测】:
- 三档都过九门(地板达标);**pro 自修复轮最少(3 vs 5/6)**=强档少返工;flash 单价最低(¥0.194,M2.7 因 completion_ratio=4 最贵 ¥0.514)。
- **便宜 player(M3视觉/文本)给分与真手感脱节**:三档文本位都给 fun≥3/pass;但叠加**创始人亲玩锚**(calibration/labels.json):flash-v3 物理最好(fun4)、**pro 碰撞有明显问题(fun2/fix,两 player 却都给 4=假阳)**、M2.7 滑块一格一跳控制 bug(fun2)。→ **过门率/便宜 player 高分 ≠ 真好玩**,人锚是最终纠偏(命门结论,P1)。
> 样本=同款 Breakout(N=1 题面)。多题面横比(过门率梯度/¥每过门)属 P3 评分矩阵,本轮不做。
---
## 5. 三分类归因小结(spec §9)
| 归因 | 命中款 | 性质 |
|---|---|---|
| 3D 硬界(源码证伪) | **0** | 本批无 3D |
| 模型造不出(真失败) | **0** | 5 款全能生成可装载/渲染/接线的运行件 |
| 宿主门面缺失(L0,系统性) | t2048 / asteroids(键盘桥) | **L0 已修 d754b71**,重打包即活 |
| driver-coverage(我域,已闭) | flappy / multiball / asteroids | harness driver 增强(vy前瞻/弃守排空/aim-fire)+ 重生成,**3 款翻绿** |
| 可测性边界(游戏侧 exportState,留界) | angrybirds(隐藏弹道) / t2048(抗死锁 latch) | angrybirds 需暴露发射物理常数;t2048 latch 体裁性边界(建议放宽为强 progress+G) |
**结论**:scale-20 暴露的「失败」无一是模型能力天花板——**真难点在 harness driver 覆盖 + 宿主门面完备 + 游戏可测性导出**,与 cheap-model skill §9 画像一致并强化。
---
## 6. 后续项
- **angrybirds 完全闭口**:游戏侧暴露发射力度↔速度比例/重力(或「按目标发射」affordance),drag-aiming 改闭环反馈瞄准。
- **t2048 latch**:近解级填盘策略,或就 2048 类「抗死锁」游戏把 expectLatch 放宽为强 progress(score↑∧board变)+ G 兜——需在 spec 立规。
- **flappy/multiball/asteroids ✅ 已闭**:flash 重生成在增强 driver 下全过(flappy retries=2 / multiball・asteroids retries=0)——证「实例缺陷 / 可测性缺口」非模型短板。`results/*.json` + `evidence/verdict.json` 为四件套证据。
- **P1 校准放量**:待创始人对新款亲玩补 labels.json 锚(blocked-on-founder)。

View File

@ -207,6 +207,8 @@ async function runDriver(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);
if (driver.type === 'drag-aiming') return runDragAiming(cdp, driver, hashes);
if (driver.type === 'aim-fire') return runAimFire(cdp, driver, hashes);
return runPaddleIntercept(cdp, driver, hashes);
}
@ -245,23 +247,33 @@ async function runTapTargets(cdp, driver, hashes) {
return { drove, steps };
}
/** type='flap-to-gap':读控制体 y 与目标缝隙中心 y——体在缝下方y 更大=更低)就拍翅上升,否则任重力下落。驱动 Flappy 类穿缝。 */
/** type='flap-to-gap' y + vy
* 原纯位置反应式对高位缝/快下落追不上或过冲flappy 假阴根因前瞻=早拍防追不上晚拍防过冲驱动 Flappy 类穿缝 */
async function runFlapToGap(cdp, driver, hashes) {
const yPath = driver.entityPath || 'bird.y';
const vyPath = driver.vyPath || 'bird.vy'; // 竖直速度y 向下为正→vy>0=下落);缺失则退化为纯位置反应式
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;
const lookahead = driver.lookahead != null ? driver.lookahead : 0.18; // 速度前瞻秒数:预测 lookahead 秒后位置vy 单位 px/s
const aimBias = driver.aimBias != null ? driver.aimBias : 24; // 瞄准缝心上方 aimBias pxy 越小越高),留下落余量→落入缝心而非贴下沿
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);
const vy = getPath(s, vyPath);
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); } // 偏高/已对齐→不拍,让重力下落
const target = gy - aimBias; // 目标点略高于缝心
// 前瞻预测:当前位置 + 速度×lookahead = 下一拍预计位置;预测仍偏低于 target+margin 才拍翅(早拍/不过冲)。
const predicted = (typeof by === 'number')
? by + (typeof vy === 'number' ? vy * lookahead : 0)
: null;
if (predicted != null && predicted > target + margin) { await tap(cdp, tapX, 420); drove++; } // 预测偏低→拍翅上升
else { await delay(20); } // 预测偏高/已对齐→不拍,让重力下落
try { hashes.push(await sampleHash(cdp, '#game-engine')); } catch (_) {}
await delay(stepMs);
}
@ -348,16 +360,121 @@ async function runKeyCycle(cdp, driver, hashes) {
return { drove, steps };
}
/** type='paddle-intercept':每步读球 x、tap 到球正下方把挡板移过去接(真玩到清砖);到 gameover 即停。 */
/** type='drag-aiming' targets[{x,y}] origintarget
* 发射物理常数重力/力度比例游戏未暴露 力度扫描 powers[] + 上抬补下坠 upLead逐发兜命中确定性尽力,非保证
* 若仍 0 命中=可测性边界:需游戏暴露弹道/物理或发射 affordance,非模型造不出 */
async function runDragAiming(cdp, driver, hashes) {
const origin = driver.origin || { x: 80, y: 600 }; // 弹弓发射点(逻辑像素)
const targetsPath = driver.targetsPath || 'targets';
const steps = driver.steps || 6; // 最多发射次数(通常=birdsLeft
const settleMs = driver.settleMs != null ? driver.settleMs : 1700; // 每发后等飞行+结算
const powers = driver.powers || [70, 95, 120, 150, 180, 210, 240]; // 拖拽距离扫描(力∝距离,常数未知→扫描兜不同射程)
const upLead = driver.upLead != null ? driver.upLead : 60; // 抛物线下坠补偿:瞄准点上抬 px
const dragMs = driver.dragMs != null ? driver.dragMs : 360;
let drove = 0, shot = 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);
// 选最近的活目标(猪/关键砖);无合法目标→停。
let aim = null;
if (Array.isArray(targets) && targets.length) {
for (const t of targets) {
if (!t || typeof t.x !== 'number' || typeof t.y !== 'number') continue;
if (!aim) { aim = t; continue; }
const da = Math.hypot(aim.x - origin.x, aim.y - origin.y);
const db = Math.hypot(t.x - origin.x, t.y - origin.y);
if (db < da) aim = t;
}
}
if (!aim) break;
const aimY = aim.y - upLead; // 上抬补抛物线下坠
const dx = aim.x - origin.x, dy = aimY - origin.y;
const len = Math.hypot(dx, dy) || 1;
const power = powers[shot % powers.length];
// 拖拽矢量 = 发射反方向 × 本发力度;夹取到屏内合法范围(受控面只给 pointer,拖拽手势 drag() 自合成)。
const to = {
x: Math.max(4, Math.min(386, origin.x - (dx / len) * power)),
y: Math.max(4, Math.min(840, origin.y - (dy / len) * power)),
};
await drag(cdp, origin, to, dragMs);
drove++; shot++;
try { hashes.push(await sampleHash(cdp, '#game-engine')); } catch (_) {}
await delay(settleMs);
}
return { drove, steps };
}
/** type='aim-fire'Asteroids ship{x,y,angle} + targets[{x,y}] 使
* |目标方位(atan2) - ship.angle| 收敛对齐即开火旋向自适应(上拍 |diff| 变大就翻向,免猜角度约定 CW/CCW)
* 依赖游戏 exportState 暴露目标坐标(可测性红线)盲射无坐标=可测性边界,暴露后即可确定性瞄准 */
async function runAimFire(cdp, driver, hashes) {
const shipPath = driver.shipPath || 'ship';
const targetsPath = driver.targetsPath || 'asteroids';
const fireKey = driver.fireKey || 'Space';
const cwKey = driver.cwKey || 'ArrowRight';
const ccwKey = driver.ccwKey || 'ArrowLeft';
const tol = driver.tol != null ? driver.tol : 0.20; // 对齐容差(弧度,~11°)
const steps = driver.steps || 80;
const stepMs = driver.stepMs != null ? driver.stepMs : 130;
const downMs = driver.downMs != null ? driver.downMs : 70;
const norm = (a) => { while (a > Math.PI) a -= 2 * Math.PI; while (a < -Math.PI) a += 2 * Math.PI; return a; };
let drove = 0, lastDiff = null, rotKey = cwKey;
for (let i = 0; i < steps; i++) {
const s = await readGameState(cdp);
if (s && s.phase === 'gameover') break; // 终局即停(清光/被撞死)→交 latch 校验
const ship = getPath(s, shipPath);
const targets = getPath(s, targetsPath);
let aim = null;
if (ship && typeof ship.x === 'number' && Array.isArray(targets)) {
for (const t of targets) {
if (!t || typeof t.x !== 'number' || typeof t.y !== 'number') continue;
if (!aim) { aim = t; continue; }
const da = Math.hypot(aim.x - ship.x, aim.y - ship.y);
const db = Math.hypot(t.x - ship.x, t.y - ship.y);
if (db < da) aim = t;
}
}
if (aim && ship && typeof ship.angle === 'number') {
const bearing = Math.atan2(aim.y - ship.y, aim.x - ship.x);
const diff = Math.abs(norm(bearing - ship.angle));
if (diff <= tol) { // 已对齐→开火
await key(cdp, fireKey, downMs); drove++; lastDiff = null;
} else { // 未对齐→旋转(自适应旋向)
if (lastDiff != null && diff > lastDiff) rotKey = (rotKey === cwKey ? ccwKey : cwKey);
await key(cdp, rotKey, downMs); drove++; lastDiff = diff;
}
} else {
// 读不到 ship/targets(游戏未暴露坐标)→退化盲扫(旋转+开火),H 门会判(可测性红线)
await key(cdp, i % 2 ? fireKey : cwKey, downMs); drove++;
}
try { hashes.push(await sampleHash(cdp, '#game-engine')); } catch (_) {}
await delay(stepMs);
}
return { drove, steps };
}
/** type='paddle-intercept' xtap gameover
* drainAfter 声明时跑满 drainAfter 已证进展后转弃守排空相位挡板停角落不再追球让球漏光触发 lose/gameover
* 验真终态接球类如 multiball 靠一直接球永不失败latch 不可达drain 把真失败态打出来 */
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;
const stepMs = driver.stepMs || 130;
const drainAfter = driver.drainAfter != null ? driver.drainAfter : null; // 此步后弃守排空null=不排空,向后兼容打砖块)
const drainX = driver.drainX != null ? driver.drainX : 10; // 排空相位挡板停泊 x远离球→球漏掉
let drove = 0;
for (let i = 0; i < steps; i++) {
const s = await readGameState(cdp);
if (s && s.phase === 'gameover') break; // 已结束,停(剩交给 latch 校验)
if (drainAfter != null && i >= drainAfter) {
// 弃守排空相位挡板停角落、不再追球等球漏光→失败终态latch
await tap(cdp, drainX, paddleY);
try { hashes.push(await sampleHash(cdp, '#game-engine')); } catch (_) {}
await delay(stepMs);
continue;
}
const bx = getPath(s, ballPath);
if (typeof bx === 'number') { await tap(cdp, Math.max(8, Math.min(382, bx)), paddleY); drove++; }
else { await delay(stepMs); } // 读不到球位=游戏没导出位置空转H 门会判红线)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -1,6 +1,6 @@
{
"gameId": "angrybirds",
"url": "http://localhost:4320/games/_wg1-gen/angrybirds/index.html",
"url": "http://localhost:4322/games/_wg1-gen/angrybirds/index.html",
"ts": null,
"guards": {
"A_boot": {
@ -8,8 +8,8 @@
},
"C_frame": {
"pass": true,
"f0": 48,
"f1": 78,
"f0": 49,
"f1": 79,
"delta": 30
},
"I_control": {
@ -18,13 +18,13 @@
},
"D_render": {
"pass": true,
"bright": 34719,
"bright": 35274,
"maxCh": 255
},
"E_live": {
"pass": true,
"distinctStates": 10,
"samples": 15
"distinctStates": 6,
"samples": 6
},
"B_uncaught": {
"pass": true,
@ -54,7 +54,10 @@
},
"G_input": {
"pass": true,
"skipped": "无 tap/key 输入"
"inputHash": "d66e1b3e",
"controlHash": "e30f433f",
"atFrame": 712,
"ctrlFrame": 712
},
"H_progress": {
"pass": false,
@ -65,7 +68,7 @@
"before": 0,
"after": 0,
"pass": false,
"why": "盲态固定拖拽发射→偶中目标得分;若固定角度打不中(需读目标算拖拽矢量)则不升=暴露拖拽瞄准缺口"
"why": "drag-aiming 读猪坐标反解拖拽矢量发射(力度扫描兜射程)→命中得分上升;若发射物理常数未暴露致全 miss 则 0=可测性边界,非模型造不出"
}
],
"latch": {

View File

@ -1,106 +1,13 @@
{
"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
}
],
"driver": {
"type": "drag-aiming",
"origin": { "x": 80, "y": 600 },
"targetsPath": "targets",
"steps": 6,
"settleMs": 1700,
"powers": [150, 200, 240, 180, 220, 250],
"upLead": 95
},
"expectedEngineCallPrefixes": [
"particles.",
"audio.synth"
@ -109,8 +16,8 @@
{
"path": "score",
"op": "increased",
"why": "盲态固定拖拽发射→偶中目标得分;若固定角度打不中(需读目标算拖拽矢量)则不升=暴露拖拽瞄准缺口"
"why": "drag-aiming 读猪坐标反解拖拽矢量发射(力度扫描兜射程)→命中得分上升;若发射物理常数未暴露致全 miss 则 0=可测性边界,非模型造不出"
}
],
"expectLatch": true
}
}

File diff suppressed because one or more lines are too long

View File

@ -1 +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}}}
{"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":12427,"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":164259},"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":5498},"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/asteroids/generated-factory.js":{"bytesInOutput":5137}},"bytes":213109}}}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

@ -8,9 +8,9 @@
},
"C_frame": {
"pass": true,
"f0": 48,
"f1": 78,
"delta": 30
"f0": 49,
"f1": 80,
"delta": 31
},
"I_control": {
"pass": true,
@ -18,13 +18,13 @@
},
"D_render": {
"pass": true,
"bright": 66491,
"bright": 73961,
"maxCh": 255
},
"E_live": {
"pass": true,
"distinctStates": 37,
"samples": 37
"distinctStates": 81,
"samples": 81
},
"B_uncaught": {
"pass": true,
@ -32,19 +32,19 @@
},
"F_wiring": {
"pass": true,
"callCount": 27,
"callCount": 41,
"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",
"particles.spawnEmitter",
"particles.spawnEmitter",
"particles.spawnEmitter",
"particles.spawnEmitter",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter"
],
"expected": [
@ -54,21 +54,21 @@
},
"G_input": {
"pass": true,
"inputHash": "d2d3585d",
"controlHash": "f13eb61f",
"atFrame": 901,
"ctrlFrame": 905
"inputHash": "abb370ce",
"controlHash": "38e0cc7c",
"atFrame": 1581,
"ctrlFrame": -1
},
"H_progress": {
"pass": false,
"pass": true,
"checks": [
{
"path": "score",
"op": "increased",
"before": 0,
"after": 0,
"pass": false,
"why": "循环旋转+推进+开火→击碎陨石得分上升;若高速子弹穿透(CCD缺)或打不中则不升=暴露 CCD 缺口"
"after": 6,
"pass": true,
"why": "aim-fire 读陨石坐标旋向对齐开火→击碎得分上升;若游戏不暴露陨石坐标致盲射则不升=可测性边界"
}
],
"latch": {
@ -81,26 +81,78 @@
"phase": "playing",
"result": null,
"score": 0,
"asteroidsRemaining": 5,
"asteroidsRemaining": 6,
"ship": {
"x": 195,
"y": 422,
"angle": -1.5707963267948966
}
},
"asteroids": [
{
"x": 96.99717124909638,
"y": 519.2675040247642
},
{
"x": 319.4950147635825,
"y": 629.6736080402688
},
{
"x": 1.5485410220474345,
"y": 718.9562386134752
},
{
"x": 73.61813029544744,
"y": 761.5286812129356
},
{
"x": 337.5254555930884,
"y": 529.2215852456798
},
{
"x": 324.4826751629315,
"y": 697.42908304362
}
]
},
"state1": {
"phase": "gameover",
"result": "lose",
"score": 0,
"asteroidsRemaining": 5,
"score": 6,
"asteroidsRemaining": 6,
"ship": {
"x": 152.8208837988882,
"y": 301.88159315392096,
"angle": -1.7707963267948967
}
"x": 195,
"y": 422,
"angle": -3.120796326794893
},
"asteroids": [
{
"x": -27.856391456713904,
"y": 735.1075081715107
},
{
"x": 116.74067642420584,
"y": 770.5545437859361
},
{
"x": 166.3536431747016,
"y": 455.6770210725548
},
{
"x": 272.7968611275529,
"y": 641.2863973557977
},
{
"x": 171.07105373838698,
"y": 848.8609648800336
},
{
"x": 213.6553002398836,
"y": 87.47766219533963
}
]
}
}
},
"pass": false,
"pass": true,
"notes": []
}

View File

@ -1,107 +1,95 @@
'use strict';
export default function createAsteroidsGame(opts) {
export default function createGame(opts) {
const o = opts || {};
let ctx = null;
let subHandlers = [];
let nowMs = 0;
let t = 0;
// input held state
const held = { left: false, right: false, up: false, space: false };
let inputSubs = [];
// game state
let phase = 'booting';
let result = null;
let phase = 'booting'; // 'playing' | 'gameover'
let result = null; // 'win' | 'lose' | 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;
let ship = { x: 195, y: 422, angle: -Math.PI / 2, vx: 0, vy: 0 };
const SHIP_ACCEL = 300;
const SHIP_FRICTION = 0.99;
const SHIP_ROTATION_SPEED = 3;
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;
const BULLET_SPEED = 500;
const BULLET_LIFETIME = 2;
const BULLET_RADIUS = 3;
const FIRE_COOLDOWN = 0.15; // seconds
let fireTimer = 0;
const ASTEROID_LARGE_RADIUS = 30;
const ASTEROID_SMALL_RADIUS = 15;
const ASTEROID_SPEED_MIN = 50;
const ASTEROID_SPEED_MAX = 120;
const SCREEN_W = 390;
const SCREEN_H = 844;
// sub-step for bullet CCD
const SUB_STEPS = 4;
// for forensics
function _forensicsView() {
return {
seed: o.seed || null,
state: () => ({
phase,
result,
score,
asteroidsRemaining: asteroids.filter(a => a.active).length,
ship: { x: ship.x, y: ship.y, angle: ship.angle },
asteroids: asteroids.filter(a => a.active).map(a => ({ x: a.x, y: a.y }))
})
};
}
// --- collision helpers ---
function dist(a, b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
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_;
const a = dx * dx + dy * dy;
if (a === 0) return dist(p1, c) <= r;
const b = 2 * (fx * dx + fy * dy);
const cVal = fx * fx + fy * fy - r * r;
let disc = b * b - 4 * a * cVal;
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);
const t1 = (-b - disc) / (2 * a);
const t2 = (-b + disc) / (2 * a);
if (t1 >= 0 && t1 <= 1) return true;
if (t2 >= 0 && t2 <= 1) return true;
return false;
}
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) {
// --- audio & particles ---
function emitParticles(x, y, count, color) {
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 }
speed: 100,
particleTime: 0.6,
sizeStart: 8,
sizeEnd: 0,
colorStart: color || { r: 1, g: 0.8, b: 0.2, a: 1 },
colorEnd: { r: 1, g: 0.3, b: 0, a: 0 }
});
} catch (_) {}
}
@ -114,170 +102,267 @@ export default function createAsteroidsGame(opts) {
}
}
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 = {};
}
// --- game logic ---
function resetGame() {
resetGameState();
phase = 'playing';
result = null;
score = 0;
bullets = [];
ship = { x: 195, y: 422, angle: -Math.PI / 2, vx: 0, vy: 0 };
fireTimer = 0;
// generate asteroids
asteroids = [];
for (let i = 0; i < 6; i++) {
let x, y;
do {
x = ctx.random.range(30, SCREEN_W - 30);
y = ctx.random.range(30, SCREEN_H - 30);
} while (dist({ x, y }, ship) < 150);
const angle = ctx.random.range(0, Math.PI * 2);
const speed = ctx.random.range(ASTEROID_SPEED_MIN, ASTEROID_SPEED_MAX);
asteroids.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
radius: ASTEROID_LARGE_RADIUS,
active: true
});
}
}
// lifecycle
function spawnBullet() {
const cos = Math.cos(ship.angle);
const sin = Math.sin(ship.angle);
const gunX = ship.x + cos * (SHIP_RADIUS + 5);
const gunY = ship.y + sin * (SHIP_RADIUS + 5);
bullets.push({
x: gunX, y: gunY,
prevX: gunX, prevY: gunY,
vx: cos * BULLET_SPEED,
vy: sin * BULLET_SPEED,
life: BULLET_LIFETIME
});
emitParticles(gunX, gunY, 12, { r: 0.5, g: 0.9, b: 1, a: 1 });
playSfx([1, 0.02, 880, , 0.02, 0.1, , 0.4, -0.1]); // fire sfx
}
// --- lifecycle ---
function init(boot) {
ctx = boot.ctx;
const input = ctx.getInput();
const seed = o.seed;
if (seed != null) ctx.random.reseed(seed);
const keydownSub = input.on('keydown', (e) => {
if (e.key) keyState[e.key] = true;
});
subHandlers.push(keydownSub);
// input subscriptions
const onKeyDown = (e) => {
switch (e.key) {
case 'ArrowLeft': held.left = true; break;
case 'ArrowRight': held.right = true; break;
case 'ArrowUp': held.up = true; break;
case ' ': held.space = true; break;
}
};
const onKeyUp = (e) => {
switch (e.key) {
case 'ArrowLeft': held.left = false; break;
case 'ArrowRight': held.right = false; break;
case 'ArrowUp': held.up = false; break;
case ' ': held.space = false; break;
}
};
const onPointerDown = (e) => {
if (phase === 'gameover') {
resetGame();
}
};
const keyupSub = input.on('keyup', (e) => {
if (e.key) keyState[e.key] = false;
});
subHandlers.push(keyupSub);
const inp = ctx.getInput();
const sub1 = inp.on('keydown', onKeyDown);
const sub2 = inp.on('keyup', onKeyUp);
const sub3 = inp.on('pointerdown', onPointerDown);
inputSubs = [sub1, sub2, sub3];
const pointerSub = input.on('pointerdown', () => {
if (phase === 'gameover') resetGame();
});
subHandlers.push(pointerSub);
resetGameState();
phase = 'playing';
result = null;
resetGame();
// initial particles (ambient)
const eng = ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x: 195, y: 400 }, emitRate: 8, emitTime: 0, particleTime: 0.8,
speed: 30, sizeStart: 4, sizeEnd: 0,
colorStart: { r: 0.2, g: 0.6, b: 1, a: 0.5 }, colorEnd: { r: 0.1, g: 0.3, b: 0.5, a: 0 }
});
} catch (_) {}
}
}
function update(dt) {
if (phase === 'gameover') return;
const now = ctx.time.nowMs();
nowMs = ctx.time.nowMs();
t = nowMs / 1000;
// ship rotation
if (keyState['ArrowLeft']) ship.angle -= ROTATION_SPEED * dt;
if (keyState['ArrowRight']) ship.angle += ROTATION_SPEED * dt;
if (phase !== 'playing') return;
// 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
);
// --- input handling ---
if (held.left) ship.angle -= SHIP_ROTATION_SPEED * dt;
if (held.right) ship.angle += SHIP_ROTATION_SPEED * dt;
if (held.up) {
ship.vx += Math.cos(ship.angle) * SHIP_ACCEL * dt;
ship.vy += Math.sin(ship.angle) * SHIP_ACCEL * dt;
}
// held.space for continuous fire with cooldown
if (held.space && fireTimer <= 0) {
spawnBullet();
fireTimer = FIRE_COOLDOWN;
}
fireTimer -= dt;
// fire
if (keyState['Space']) fireBullet();
// --- friction ---
ship.vx *= SHIP_FRICTION;
ship.vy *= SHIP_FRICTION;
// friction
ship.vx *= FRICTION;
ship.vy *= FRICTION;
// move ship
// --- update ship position ---
ship.x += ship.vx * dt;
ship.y += ship.vy * dt;
wrapPos(ship);
// wrap
if (ship.x < 0) ship.x += SCREEN_W;
if (ship.x > SCREEN_W) ship.x -= SCREEN_W;
if (ship.y < 0) ship.y += SCREEN_H;
if (ship.y > SCREEN_H) ship.y -= SCREEN_H;
// move asteroids
for (const a of asteroids) {
a.x += a.vx * dt;
a.y += a.vy * dt;
wrapPos(a);
// --- update bullets with sub-steps for CCD ---
const subDt = dt / SUB_STEPS;
for (let bi = 0; bi < bullets.length; bi++) {
const b = bullets[bi];
b.prevX = b.x;
b.prevY = b.y;
// move in sub-steps (we only store final position for wrapping & collisions)
for (let s = 0; s < SUB_STEPS; s++) {
b.x += b.vx * subDt;
b.y += b.vy * subDt;
// wrap (optional: bullets wrap)
// We choose NOT to wrap bullets for simplicity delete when off screen
}
b.life -= dt;
}
// 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;
// --- bullet-asteroid collision (CCD using line segment) ---
const bulletsToRemove = [];
const newAsteroids = [];
let scoreDelta = 0;
// lifespan
if (now - b.born > BULLET_LIFETIME) {
bullets.splice(i, 1);
for (let bi = 0; bi < bullets.length; bi++) {
const b = bullets[bi];
if (b.life <= 0) {
bulletsToRemove.push(bi);
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);
// wrap check: if bullet left screen, remove
if (b.x < -100 || b.x > SCREEN_W + 100 || b.y < -100 || b.y > SCREEN_H + 100) {
bulletsToRemove.push(bi);
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)) {
for (let ai = 0; ai < asteroids.length; ai++) {
const a = asteroids[ai];
if (!a.active) continue;
if (lineCircleIntersect(
{ x: b.prevX, y: b.prevY },
{ x: b.x, y: b.y },
{ x: a.x, y: a.y },
a.radius
)) {
// hit
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;
// destroy asteroid
a.active = false;
// particles at hit point (use asteroid center)
emitParticles(a.x, a.y, 25, { r: 1, g: 0.6, b: 0.2, a: 1 });
playSfx([1, 0.05, 260, , 0.1, 0.15, , 0.7]); // explosion sfx
scoreDelta += 1;
if (a.radius === ASTEROID_LARGE_RADIUS) {
// spawn two small asteroids
const spd = Math.hypot(a.vx, a.vy) * 1.2;
for (let i = 0; i < 2; i++) {
const ang = ctx.random.range(0, Math.PI * 2);
const speed = spd * (0.8 + 0.4 * ctx.random.next());
newAsteroids.push({
x: a.x, y: a.y,
vx: Math.cos(ang) * speed,
vy: Math.sin(ang) * speed,
radius: ASTEROID_SMALL_RADIUS,
active: true
});
}
}
break; // one bullet hits at most one asteroid
}
}
if (hit) {
bullets.splice(i, 1);
}
if (hit) bulletsToRemove.push(bi);
}
// ship vs asteroids
// --- apply bullet removal ---
for (let i = bulletsToRemove.length - 1; i >= 0; i--) {
bullets.splice(bulletsToRemove[i], 1);
}
// --- add new asteroids ---
for (const na of newAsteroids) {
asteroids.push(na);
}
// --- update score ---
score += scoreDelta;
// --- update asteroids (wrap) ---
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)) {
if (!a.active) continue;
a.x += a.vx * dt;
a.y += a.vy * dt;
if (a.x < -a.radius) a.x += SCREEN_W + 2 * a.radius;
if (a.x > SCREEN_W + a.radius) a.x -= SCREEN_W + 2 * a.radius;
if (a.y < -a.radius) a.y += SCREEN_H + 2 * a.radius;
if (a.y > SCREEN_H + a.radius) a.y -= SCREEN_H + 2 * a.radius;
}
// --- ship-asteroid collision ---
for (const a of asteroids) {
if (!a.active) continue;
if (dist(ship, a) < SHIP_RADIUS + a.radius) {
// game over - lose
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;
emitParticles(ship.x, ship.y, 50, { r: 1, g: 0.2, b: 0.2, a: 1 });
playSfx([1, 0.2, 100, , 0.3, 0.3, , 0.8]); // crash sfx
// also destroy ship? keep ship visible
break;
}
}
// win condition
if (asteroids.length === 0) {
// --- check win (all asteroids gone) ---
const hasActive = asteroids.some(a => a.active);
if (!hasActive && asteroids.length > 0) {
phase = 'gameover';
result = 'win';
emitParticles(ship.x, ship.y, 60, { r: 0.3, g: 1, b: 0.5, a: 1 });
playSfx([1, 0.05, 523, , 0.05, 0.2, , 0.5]); // win sound
}
}
function render(g) {
// background
g.fillStyle = '#0a0a1a';
g.fillRect(0, 0, 390, 844);
g.fillRect(0, 0, SCREEN_W, SCREEN_H);
// asteroids
for (const a of asteroids) {
g.fillStyle = a.isBig ? '#8b5e3c' : '#a0744a';
if (!a.active) continue;
g.fillStyle = '#888899';
g.beginPath();
g.arc(a.x, a.y, a.radius, 0, Math.PI*2);
g.arc(a.x, a.y, a.radius, 0, Math.PI * 2);
g.fill();
g.strokeStyle = '#d4a76a';
g.strokeStyle = '#aabbcc';
g.lineWidth = 2;
g.stroke();
}
@ -286,69 +371,66 @@ export default function createAsteroidsGame(opts) {
for (const b of bullets) {
g.fillStyle = '#ffff00';
g.beginPath();
g.arc(b.x, b.y, b.radius, 0, Math.PI*2);
g.arc(b.x, b.y, BULLET_RADIUS, 0, Math.PI * 2);
g.fill();
}
// ship
// ship (triangle)
g.save();
g.translate(ship.x, ship.y);
g.rotate(ship.angle);
g.fillStyle = phase === 'gameover' ? '#ff4444' : '#44aaff';
g.fillStyle = '#00ddff';
g.beginPath();
g.moveTo(20, 0);
g.lineTo(-15, -12);
g.lineTo(-10, 0);
g.lineTo(-15, 12);
g.moveTo(0, -SHIP_RADIUS);
g.lineTo(-SHIP_RADIUS * 0.7, SHIP_RADIUS * 0.8);
g.lineTo(SHIP_RADIUS * 0.7, SHIP_RADIUS * 0.8);
g.closePath();
g.fill();
g.strokeStyle = '#ffffff';
g.lineWidth = 1.5;
g.stroke();
// engine glow if accelerating
if (held.up) {
g.fillStyle = '#ff8800';
g.beginPath();
g.moveTo(-5, SHIP_RADIUS * 0.5);
g.lineTo(0, SHIP_RADIUS * 1.2);
g.lineTo(5, SHIP_RADIUS * 0.5);
g.closePath();
g.fill();
}
g.restore();
// HUD
g.fillStyle = '#ffffff';
g.font = 'bold 16px monospace';
g.font = '18px monospace';
g.fillText('Score: ' + score, 10, 30);
g.fillText('Asteroids: ' + asteroids.length, 10, 50);
g.fillText('Phase: ' + phase, 10, 70);
const remaining = asteroids.filter(a => a.active).length;
g.fillText('Asteroids: ' + remaining, 10, 55);
if (phase === 'gameover') {
g.fillStyle = result === 'win' ? '#00ff00' : '#ff0000';
g.font = 'bold 28px monospace';
g.font = 'bold 36px monospace';
g.fillStyle = result === 'win' ? '#66ff88' : '#ff6666';
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';
g.fillText(result === 'win' ? 'YOU WIN!' : 'GAME OVER', SCREEN_W / 2, SCREEN_H / 2 - 40);
g.font = '20px monospace';
g.fillStyle = '#aaaaaa';
g.fillText('Click to restart', SCREEN_W / 2, SCREEN_H / 2 + 20);
g.textAlign = 'left';
}
}
function destroy() {
for (const sub of subHandlers) {
try { sub.cancel(); } catch(_) {}
for (const sub of inputSubs) {
try { sub.cancel(); } catch (_) {}
}
subHandlers = [];
asteroids = [];
inputSubs = [];
bullets = [];
phase = 'booting';
result = null;
asteroids = [];
}
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 };
return {
init,
update,
render,
destroy,
_forensicsView
};
}

View File

@ -1,16 +1,12 @@
{
"driver": {
"type": "key-cycle",
"keys": [
"ArrowLeft",
"ArrowUp",
"Space",
"ArrowRight",
"Space",
"Space"
],
"steps": 50,
"stepMs": 200
"type": "aim-fire",
"shipPath": "ship",
"targetsPath": "asteroids",
"fireKey": "Space",
"steps": 90,
"stepMs": 120,
"downMs": 80
},
"expectedEngineCallPrefixes": [
"particles.",
@ -20,7 +16,7 @@
{
"path": "score",
"op": "increased",
"why": "循环旋转+推进+开火→击碎陨石得分上升;若高速子弹穿透(CCD缺)或打不中则不升=暴露 CCD 缺口"
"why": "aim-fire 读陨石坐标旋向对齐开火→击碎得分上升;若游戏不暴露陨石坐标致盲射则不升=可测性边界"
}
],
"expectLatch": true

File diff suppressed because one or more lines are too long

View File

@ -1 +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}}}
{"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/flappy/generated-factory.js":{"bytes":7665,"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":164160},"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":5508},"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":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":3555}},"bytes":211442}}}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -8,8 +8,8 @@
},
"C_frame": {
"pass": true,
"f0": 48,
"f1": 78,
"f0": 51,
"f1": 81,
"delta": 30
},
"I_control": {
@ -18,13 +18,13 @@
},
"D_render": {
"pass": true,
"bright": 178920,
"bright": 189671,
"maxCh": 255
},
"E_live": {
"pass": true,
"distinctStates": 6,
"samples": 6
"distinctStates": 40,
"samples": 41
},
"B_uncaught": {
"pass": true,
@ -32,7 +32,7 @@
},
"F_wiring": {
"pass": true,
"callCount": 8,
"callCount": 44,
"sample": [
"particles.spawnEmitter",
"audio.synth.synthSfx",
@ -41,6 +41,10 @@
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx"
],
"expected": [
@ -50,20 +54,20 @@
},
"G_input": {
"pass": true,
"inputHash": "0e133ce8",
"controlHash": "4fb72d48",
"atFrame": 214,
"ctrlFrame": 218
"inputHash": "4ae5c2e6",
"controlHash": "aad4cfb2",
"atFrame": 749,
"ctrlFrame": 750
},
"H_progress": {
"pass": false,
"pass": true,
"checks": [
{
"path": "score",
"op": "increased",
"before": 0,
"after": 0,
"pass": false,
"after": 5,
"pass": true,
"why": "适配性真玩(读 bird.y/nextGap.centerY 拍翅穿缝)→得分上升;物理太硬秒死或穿不过则 0"
}
],
@ -79,32 +83,33 @@
"score": 0,
"bird": {
"x": 110,
"y": 559.1999999999999,
"vy": 336
},
"nextGap": {
"x": 600,
"centerY": 174.12938179075718
"y": 422,
"vy": 0
},
"nextGap": null,
"remaining": 0,
"progress": null,
"gameoverReason": ""
},
"state1": {
"phase": "gameover",
"result": "lose",
"score": 0,
"score": 5,
"bird": {
"x": 110,
"y": 523.0999999999999,
"vy": -169
"y": 405.6666666666665,
"vy": -116.66666666666673
},
"nextGap": {
"x": 600,
"centerY": 174.12938179075718
"x": 120,
"centerY": 302.81019813381135
},
"gameoverReason": "pipe_collision"
"remaining": 1,
"progress": null,
"gameoverReason": "pipe"
}
}
},
"pass": false,
"pass": true,
"notes": []
}

View File

@ -3,308 +3,271 @@ export default function createFlappyBirdGame(opts) {
const o = opts || {};
let ctx = null;
let nowMs = 0;
let subPointer = null;
let sub = null;
let seed = null;
// 游戏常量
const W = 390, H = 844;
const BIRD_X = 110;
const BIRD_RADIUS = 12;
const GRAVITY = 700; // px/s²
const FLAP_VEL = -280; // px/s 向上
const PIPE_SPEED = 180; // px/s
const PIPE_WIDTH = 60;
const GAP_HEIGHT = H * 0.24; // 约 202.56 px
const PIPE_SPACING = 270; // 管道左边缘间距1.5s 反应时间)
const GAP_CENTER_MIN = 300;
const GAP_CENTER_MAX = 560;
const SPACING_TIME = PIPE_SPACING / PIPE_SPEED; // 1.5s
// 游戏状态
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 phase = 'booting';
let started = false;
let score = 0;
let phase = 'booting'; // 'booting' | 'playing' | 'gameover'
let bird = { x: BIRD_X, y: 422, vy: 0 };
let pipes = [];
let gameoverReason = '';
let result = null;
let result = null; // 'lose' | null
let pipeTimer = 0;
// 粒子/音效触发标记(帧内)
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();
}
}
}
// 重玩按钮区域
const RESTART_X = (W - 160) / 2;
const RESTART_Y = H/2 + 50;
const RESTART_W = 160;
const RESTART_H = 50;
function init(boot) {
ctx = boot.ctx;
// 订阅输入
subPointer = ctx.getInput().on('pointerdown', onPointer);
// 初始随机种子
resetGame();
seed = boot.seed || 0;
sub = ctx.getInput().on('pointerdown', onPointerDown);
phase = 'playing';
started = false;
score = 0;
bird.y = 422;
bird.vy = 0;
pipes = [];
gameoverReason = '';
result = null;
pipeTimer = 0;
}
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');
function onPointerDown(e) {
nowMs = ctx.time.nowMs();
if (phase === 'gameover') {
// 只接受重玩按钮
if (e.x >= RESTART_X && e.x <= RESTART_X + RESTART_W &&
e.y >= RESTART_Y && e.y <= RESTART_Y + RESTART_H) {
restart();
}
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;
// playing 阶段
if (!started) {
started = true;
bird.vy = FLAP_VEL;
pipeTimer = 0;
// 粒子 + 音效
const eng = ctx.getEngine();
if (eng && eng.particles) {
try { eng.particles.spawnEmitter({ pos: {x: bird.x, y: bird.y}, count: 10, speed: 80, particleTime: 0.3,
colorStart: {r:1,g:1,b:0.6,a:1}, colorEnd: {r:1,g:0.5,b:0,a:0} }); } catch(_) {}
}
// 得分:管道完全通过鸟左边缘
if (!pipe.scored && pipe.x + PIPE_WIDTH / 2 < bird.x - BIRD_HALF) {
pipe.scored = true;
score++;
pendingScoreEffect = true;
if (eng && eng.audio && eng.audio.synth) {
try { eng.audio.synth.synthSfx([1,0.05,400,,,0.1,,1.2]); } catch(_) {}
}
} else {
bird.vy = FLAP_VEL;
const eng = ctx.getEngine();
if (eng && eng.particles) {
try { eng.particles.spawnEmitter({ pos: {x: bird.x, y: bird.y}, count: 10, speed: 80, particleTime: 0.3,
colorStart: {r:1,g:1,b:0.6,a:1}, colorEnd: {r:1,g:0.5,b:0,a:0} }); } catch(_) {}
}
if (eng && eng.audio && eng.audio.synth) {
try { eng.audio.synth.synthSfx([1,0.05,300,,,0.1,,1.0]); } catch(_) {}
}
}
// 处理本帧累积的特效
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) {
function restart() {
phase = 'playing';
started = false;
score = 0;
bird.y = 422;
bird.vy = 0;
pipes = [];
gameoverReason = '';
result = null;
pipeTimer = 0;
}
function enterGameover(reason) {
phase = 'gameover';
gameoverReason = reason;
result = 'lose';
crashPos = { x: bird.x, y: bird.y };
pendingCrashEffect = true;
const eng = ctx.getEngine();
if (eng && eng.particles) {
try { eng.particles.spawnEmitter({ pos: {x: bird.x, y: bird.y}, count: 40, speed: 150, particleTime: 0.6,
colorStart: {r:1,g:0.2,b:0.2,a:1}, colorEnd: {r:0.5,g:0,b:0,a:0} }); } catch(_) {}
}
if (eng && eng.audio && eng.audio.synth) {
try { eng.audio.synth.synthSfx([1,0.05,150,0.5,0.3,0.1,,1.3]); } catch(_) {}
}
}
function update(dt) {
nowMs = ctx.time.nowMs();
if (phase !== 'playing') return;
if (!started) return;
// 物理更新鸟
bird.vy += GRAVITY * dt;
bird.y += bird.vy * dt;
// 触顶/触底
if (bird.y - BIRD_RADIUS < 0 || bird.y + BIRD_RADIUS > H) {
enterGameover('boundary');
return;
}
// 移动管道
const move = PIPE_SPEED * dt;
for (let i = pipes.length - 1; i >= 0; i--) {
const p = pipes[i];
p.x -= move;
if (p.x + PIPE_WIDTH < 0) {
pipes.splice(i, 1);
continue;
}
// 碰撞检测
if (bird.x + BIRD_RADIUS > p.x && bird.x - BIRD_RADIUS < p.x + PIPE_WIDTH) {
if (bird.y - BIRD_RADIUS < p.gapTop || bird.y + BIRD_RADIUS > p.gapBottom) {
enterGameover('pipe');
return;
}
}
// 得分检测
if (!p.scored && bird.x - BIRD_RADIUS > p.x + PIPE_WIDTH) {
p.scored = true;
score++;
const eng = ctx.getEngine();
if (eng && eng.particles) {
try { eng.particles.spawnEmitter({ pos: {x: bird.x, y: bird.y}, count: 25, speed: 120, particleTime: 0.4,
colorStart: {r:0.2,g:1,b:0.2,a:1}, colorEnd: {r:0,g:0.5,b:0,a:0} }); } catch(_) {}
}
if (eng && eng.audio && eng.audio.synth) {
try { eng.audio.synth.synthSfx([1,0.05,800,,,0.15,,0.5]); } catch(_) {}
}
}
}
// 生成新管道
pipeTimer += dt;
if (pipeTimer >= SPACING_TIME) {
pipeTimer = 0;
const centerY = ctx.random.range(GAP_CENTER_MIN, GAP_CENTER_MAX);
const halfGap = GAP_HEIGHT / 2;
const gapTop = centerY - halfGap;
const gapBottom = centerY + halfGap;
pipes.push({
x: W,
centerY,
gapTop,
gapBottom,
passed: false,
scored: false
});
}
}
function render(g) {
// 背景
g.fillStyle = '#0a1122';
g.fillRect(0, 0, 390, 844);
g.fillStyle = '#1a1a2e';
g.fillRect(0, 0, W, H);
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);
// 管道
g.fillStyle = '#4caf50';
for (const p of pipes) {
// 上管道
g.fillRect(p.x, 0, PIPE_WIDTH, p.gapTop);
// 下管道
g.fillRect(p.x, p.gapBottom, PIPE_WIDTH, H - p.gapBottom);
// 管道边框(装饰)
g.strokeStyle = '#2e7d32';
g.lineWidth = 2;
g.strokeRect(p.x, 0, PIPE_WIDTH, p.gapTop);
g.strokeRect(p.x, p.gapBottom, PIPE_WIDTH, H - p.gapBottom);
}
// 鸟
g.fillStyle = '#ffcc00';
g.beginPath();
g.arc(bird.x, bird.y, BIRD_RADIUS, 0, Math.PI * 2);
g.fill();
g.strokeStyle = '#cc8800';
g.lineWidth = 2;
g.stroke();
// HUD
g.fillStyle = '#ffffff';
g.font = '20px monospace';
g.fillText('得分: ' + score, 20, 40);
if (phase === 'gameover') {
g.font = '24px monospace';
g.textAlign = 'center';
if (phase === 'playing') {
g.fillText('Score: ' + score, W/2, 40);
if (!started) {
g.font = '20px monospace';
g.fillText('Tap to Start', W/2, 80);
}
} else if (phase === 'gameover') {
g.fillStyle = '#ff4444';
g.font = '36px monospace';
g.fillText('GAME OVER', W/2, 200);
g.fillStyle = '#ffffff';
g.font = '26px monospace';
g.fillText('失败!', 130, 300);
g.font = '24px monospace';
g.fillText('Score: ' + score, W/2, 260);
// 重玩按钮
const bx = 115, by = 620, bw = 160, bh = 60;
g.fillStyle = '#2255aa';
g.fillRect(bx, by, bw, bh);
g.fillStyle = '#3366ff';
g.fillRect(RESTART_X, RESTART_Y, RESTART_W, RESTART_H);
g.strokeStyle = '#ffffff';
g.lineWidth = 2;
g.strokeRect(RESTART_X, RESTART_Y, RESTART_W, RESTART_H);
g.fillStyle = '#ffffff';
g.font = '22px monospace';
g.fillText('点击重玩', bx + 10, by + 38);
g.font = '20px monospace';
g.fillText('Restart', W/2, RESTART_Y + RESTART_H/2 + 7);
}
}
function destroy() {
try { subPointer && subPointer.cancel(); } catch (_) {}
subPointer = null;
try { sub && sub.cancel(); } catch(_) {}
sub = 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;
return {
seed,
state: () => {
// 动态计算 nextGap
let nextGap = null;
for (const p of pipes) {
if (bird.x - BIRD_RADIUS < p.x + PIPE_WIDTH && !p.scored) {
nextGap = { x: p.x, centerY: p.centerY };
break;
}
}
return {
phase,
result,
score,
bird: { x: bird.x, y: bird.y, vy: bird.vy },
nextGap,
remaining: pipes.filter(p => !p.scored).length,
progress: null,
gameoverReason
};
}
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
};
return { init, update, render, destroy, _forensicsView };
}

View File

@ -1,9 +1,12 @@
{
"driver": {
"type": "flap-to-gap",
"steps": 70,
"stepMs": 110,
"margin": 14
"steps": 100,
"stepMs": 90,
"margin": 14,
"lookahead": 0.18,
"aimBias": 24,
"vyPath": "bird.vy"
},
"expectedEngineCallPrefixes": [
"particles.",

File diff suppressed because one or more lines are too long

View File

@ -1 +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}}}
{"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/multiball/generated-factory.js":{"bytes":13530,"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":164177},"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":5497},"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":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/multiball/generated-factory.js":{"bytesInOutput":6312}},"bytes":214199}}}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -8,9 +8,9 @@
},
"C_frame": {
"pass": true,
"f0": 46,
"f1": 75,
"delta": 29
"f0": 49,
"f1": 79,
"delta": 30
},
"I_control": {
"pass": true,
@ -18,31 +18,31 @@
"results": [
{
"tapX": 50,
"before": 194.99999999990354,
"after": 50,
"moved": -144.99999999990354,
"distToTarget": 0,
"before": 195,
"after": 54.09589111049999,
"moved": -140.9041088895,
"distToTarget": 4.095891110499991,
"pass": true
},
{
"tapX": 340,
"before": 50,
"after": 310,
"moved": 260,
"distToTarget": 30,
"before": 54.09589111049999,
"after": 306.3636674932592,
"moved": 252.2677763827592,
"distToTarget": 33.636332506740814,
"pass": true
}
]
},
"D_render": {
"pass": true,
"bright": 146201,
"bright": 154512,
"maxCh": 255
},
"E_live": {
"pass": true,
"distinctStates": 64,
"samples": 72
"distinctStates": 15,
"samples": 15
},
"B_uncaught": {
"pass": true,
@ -50,10 +50,10 @@
},
"F_wiring": {
"pass": true,
"callCount": 15,
"callCount": 27,
"sample": [
"audio.synth.synthSfx",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
@ -61,8 +61,8 @@
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter"
],
"expected": [
@ -72,96 +72,80 @@
},
"G_input": {
"pass": true,
"inputHash": "9cd794ce",
"controlHash": "6afc48cc",
"atFrame": 1984,
"ctrlFrame": -1
"inputHash": "7a86cc2e",
"controlHash": "b60e0fa8",
"atFrame": 615,
"ctrlFrame": 620
},
"H_progress": {
"pass": false,
"pass": true,
"checks": [
{
"path": "remaining",
"op": "decreased",
"before": 40,
"after": 36,
"before": 78,
"after": 69,
"pass": true,
"why": "多球+挡板接球击碎砖→剩余下降;碰撞失效/高速穿透(CCD缺)则不降"
}
],
"latch": {
"pass": false,
"reason": "真玩未到达 gameover 终态(终态不可达/无失败态/play-spec 未驱动到结束)",
"phaseNow": "playing"
"pass": true,
"dwelledMs": 600,
"after": "gameover",
"reason": ""
},
"state0": {
"phase": "playing",
"result": null,
"score": 0,
"remaining": 40,
"progress": 0,
"gameoverReason": "",
"remaining": 78,
"ball": {
"x": 171,
"y": 793,
"vx": -103.2459330675854,
"vy": 317.75830848240577
"x": 178.39632164686918,
"y": 772.8069677809253,
"vx": -149.90107628982514,
"vy": -312.5555661244529
},
"paddle": {
"x": 194.99999700380667,
"y": 804,
"x": 195,
"y": 810,
"w": 80
},
"balls": [
{
"x": 171,
"y": 793
"x": 55.977109343511984,
"y": 517.5532554459528
},
{
"x": 187,
"y": 793
"x": 212.95210833312012,
"y": 465.970216069029
},
{
"x": 203,
"y": 793
"x": 199.6740988851525,
"y": 475.6221190751453
}
]
},
"state1": {
"phase": "playing",
"result": null,
"score": 4,
"remaining": 36,
"progress": 0,
"gameoverReason": "",
"phase": "gameover",
"result": "lose",
"score": 9,
"remaining": 69,
"ball": {
"x": 171,
"y": 793,
"vx": -103.2459330675854,
"vy": 317.75830848240577
"x": 178.39632164686918,
"y": 772.8069677809253,
"vx": -149.90107628982514,
"vy": -312.5555661244529
},
"paddle": {
"x": 171.00000000000003,
"y": 804,
"x": 179.63617952826013,
"y": 810,
"w": 80
},
"balls": [
{
"x": 171,
"y": 793
},
{
"x": 187,
"y": 793
},
{
"x": 203,
"y": 793
}
]
"balls": []
}
}
},
"pass": false,
"pass": true,
"notes": []
}

View File

@ -1,388 +1,432 @@
'use strict';
/**
* 多球打砖块变体工厂模块
* 竖屏390×844底部挡板2-3砖块网格道具掉落胜负终态驻留
*/
export default function createGame(opts) {
const o = opts || {};
let ctx = null;
let nowMs = 0;
let seed = 0;
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;
let paddle = { x: 195, y: 810, w: 80, h: 14 };
let powerups = [];
let touchX = null;
let subs = [];
let gameoverReason = '';
let flashTimer = 0;
const FLASH_DUR = 0.1;
const BALL_RADIUS = 8;
function init(boot) {
ctx = boot.ctx;
phase = 'playing';
gameoverReason = '';
score = 0;
remaining = 0;
function initBricks() {
const cols = 13;
const rows = 6;
const bw = 28;
const bh = 15;
const gap = 2;
const startX = (390 - (cols * (bw + gap) - gap)) / 2;
const startY = 60;
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
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
bricks.push({
x: startX + c * (bw + gap),
y: startY + r * (bh + gap),
w: bw,
h: bh,
alive: true,
color: `hsl(${(r * 30 + c * 12) % 360}, 80%, 60%)`,
dropRate: 0.25
});
}
}
}
// 引擎接口
function initPaddle() {
paddle.x = 195;
paddle.y = 810;
paddle.w = 80;
paddle.h = 14;
}
function initBalls() {
balls = [];
const count = 3;
for (let i = 0; i < count; i++) {
const baseSpeed = ctx.random.range(320, 420);
const vx = ctx.random.range(-150, 150);
const vy = -Math.sqrt(Math.max(0, baseSpeed * baseSpeed - vx * vx));
const x = paddle.x + ctx.random.range(-30, 30);
const y = paddle.y - 35 - ctx.random.range(0, 15);
balls.push({ x, y, vx, vy, radius: BALL_RADIUS, dead: false });
}
}
function resetGame() {
phase = 'playing';
score = 0;
gameoverReason = '';
flashTimer = 0;
initBricks();
initPaddle();
initBalls();
powerups = [];
touchX = null;
}
function handlePointer(e) {
if (e.type === 'pointerdown' && phase === 'gameover') {
resetGame();
return;
}
if (e.type === 'pointermove' || e.type === 'pointerdown') {
touchX = e.x;
}
if (e.type === 'pointerup') {
touchX = null;
}
}
function init(boot) {
ctx = boot.ctx;
seed = (opts && opts.seed) || 0;
ctx.random.reseed(seed);
phase = 'playing';
score = 0;
gameoverReason = '';
flashTimer = 0;
initBricks();
initPaddle();
initBalls();
powerups = [];
touchX = null;
subs.push(ctx.getInput().on('pointermove', handlePointer));
subs.push(ctx.getInput().on('pointerdown', handlePointer));
subs.push(ctx.getInput().on('pointerup', handlePointer));
const eng = ctx.getEngine();
if (eng) {
particles = eng.particles || null;
if (eng.audio && eng.audio.synth) audioSynth = eng.audio.synth;
math = eng.math || null;
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x: 195, y: 300 },
emitRate: 5,
emitTime: 0,
particleTime: 0.6,
speed: 30,
colorStart: { r: 1, g: 0.8, b: 0.2, a: 1 },
colorEnd: { r: 1, g: 0.3, b: 0, a: 0 }
});
} catch (_) { /* ignore */ }
}
// 输入订阅
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));
}
nowMs = ctx.time.nowMs();
if (phase !== 'playing') {
if (flashTimer > 0) flashTimer -= dt;
return;
}
if (flashTimer > 0) flashTimer -= dt;
// 球移动 + 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;
// 挡板跟踪
if (touchX !== null) {
const targetX = Math.max(paddle.w / 2, Math.min(390 - paddle.w / 2, touchX));
paddle.x += (targetX - paddle.x) * 0.3;
}
paddle.x = Math.max(paddle.w / 2, Math.min(390 - paddle.w / 2, paddle.x));
// 球移动・分段
const SUB_STEP = 5;
for (let bi = 0; bi < balls.length; bi++) {
const ball = balls[bi];
if (ball.dead) continue;
const speed = Math.hypot(ball.vx, ball.vy);
const steps = Math.max(1, Math.ceil(speed * dt / SUB_STEP));
const sx = ball.vx * dt / steps;
const sy = ball.vy * dt / steps;
let outOfBounds = false;
for (let s = 0; s < steps; s++) {
ball.x += stepDx;
ball.y += stepDy;
// 墙壁边界
ball.x += sx;
ball.y += sy;
// 墙碰撞
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.x + ball.radius > 390) { ball.x = 390 - 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) {
if (ball.vy > 0 && ball.y + ball.radius >= paddle.y - paddle.h / 2 && ball.y - ball.radius <= paddle.y + paddle.h / 2) {
if (ball.x > paddle.x - paddle.w / 2 && ball.x < paddle.x + paddle.w / 2) {
ball.vy = -Math.abs(ball.vy);
const offset = (ball.x - paddle.x) / (paddle.w / 2);
ball.vx += offset * 180;
const sp = Math.hypot(ball.vx, ball.vy);
if (sp < 300) { const f = 300 / sp; ball.vx *= f; ball.vy *= f; }
const eng = ctx.getEngine();
if (eng && eng.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 }
eng.particles.spawnEmitter({
pos: { x: ball.x, y: ball.y + ball.radius },
count: 8,
speed: 70,
particleTime: 0.4,
colorStart: { r: 1, g: 1, b: 1, a: 1 },
colorEnd: { r: 1, g: 0.6, b: 0.2, a: 0 }
});
} catch (_) {}
}
if (audioSynth) {
try { audioSynth.synthSfx([1, 0.05, 440, , , 0.1, , 1.2]); } catch (_) {}
if (eng && eng.audio && eng.audio.synth) {
try { eng.audio.synth.synthSfx([1, 0.02, 420, , , 0.1, , 0.7]); } 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
}
}
// 底部出界
if (ball.y - ball.radius > 844) {
ball.dead = true;
outOfBounds = true;
const eng = ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x: ball.x, y: 844 },
count: 12,
speed: 55,
particleTime: 0.5,
colorStart: { r: 1, g: 0.2, b: 0.2, a: 1 },
colorEnd: { r: 0.5, g: 0, b: 0, a: 0 }
});
}
break; // 每子步只处理一块砖
} catch (_) {}
}
if (eng && eng.audio && eng.audio.synth) {
try { eng.audio.synth.synthSfx([1, 0.08, 180, , , 0.15, , 1.1]); } catch (_) {}
}
break;
}
}
}
balls = balls.filter(b => !b.dead);
balls = balls.filter(b => b.alive);
// 球与砖块碰撞
for (let bi = balls.length - 1; bi >= 0; bi--) {
const ball = balls[bi];
for (let ri = bricks.length - 1; ri >= 0; ri--) {
const brick = bricks[ri];
if (!brick.alive) continue;
const cx = Math.max(brick.x, Math.min(ball.x, brick.x + brick.w));
const cy = Math.max(brick.y, Math.min(ball.y, brick.y + brick.h));
const dx = ball.x - cx;
const dy = ball.y - cy;
if (dx * dx + dy * dy < ball.radius * ball.radius) {
brick.alive = false;
score++;
const bCx = brick.x + brick.w / 2;
const bCy = brick.y + brick.h / 2;
const nx = ball.x - bCx;
const ny = ball.y - bCy;
const len = Math.hypot(nx, ny) || 1;
const push = ball.radius - Math.sqrt(dx * dx + dy * dy) + 1;
ball.x += (nx / len) * push;
ball.y += (ny / len) * push;
const dot = ball.vx * (nx / len) + ball.vy * (ny / len);
ball.vx -= 2 * dot * (nx / len);
ball.vy -= 2 * dot * (ny / len);
flashTimer = FLASH_DUR;
const eng = ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x: brick.x + brick.w / 2, y: brick.y + brick.h / 2 },
count: 22,
speed: 90,
particleTime: 0.5,
colorStart: { r: 1, g: 1, b: 1, a: 1 },
colorEnd: { r: 0.2, g: 0.8, b: 1, a: 0 }
});
} catch (_) {}
}
if (eng && eng.audio && eng.audio.synth) {
try { eng.audio.synth.synthSfx([1, 0.03, 700, , , 0.12, , 0.6]); } catch (_) {}
}
if (eng && eng.particles && eng.particles.shakeScreen) {
try { eng.particles.shakeScreen(4, 90); } catch (_) {}
}
if (ctx.random.next() < brick.dropRate) {
const type = ctx.random.next() < 0.5 ? 'extraBall' : 'widePaddle';
powerups.push({ x: brick.x + brick.w / 2, y: brick.y + brick.h / 2, w: 20, h: 12, type, speed: 180 });
}
break;
}
}
}
bricks = bricks.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,
// 道具更新
for (let pi = powerups.length - 1; pi >= 0; pi--) {
const pu = powerups[pi];
pu.y += pu.speed * dt;
if (pu.y + pu.h / 2 >= paddle.y - paddle.h / 2 && pu.y - pu.h / 2 <= paddle.y + paddle.h / 2) {
if (pu.x > paddle.x - paddle.w / 2 && pu.x < paddle.x + paddle.w / 2) {
if (pu.type === 'extraBall') {
const nb = {
x: paddle.x + ctx.random.range(-40, 40),
y: paddle.y - 35,
vx: ctx.random.range(-150, 150),
vy: -340,
radius: BALL_RADIUS,
alive: true
});
dead: false
};
balls.push(nb);
} else if (pu.type === 'widePaddle') {
paddle.w = Math.min(150, paddle.w + 20);
}
} 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));
const eng = ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x: pu.x, y: pu.y },
count: 14,
speed: 60,
particleTime: 0.4,
colorStart: { r: 0, g: 1, b: 0.6, a: 1 },
colorEnd: { r: 0, g: 0.5, b: 0, a: 0 }
});
} catch (_) {}
}
if (eng && eng.audio && eng.audio.synth) {
try { eng.audio.synth.synthSfx([1, 0.04, 600, , , 0.12, , 0.8]); } catch (_) {}
}
powerups.splice(pi, 1);
continue;
}
// 收集粒子
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 (pu.y + pu.h / 2 > 844) {
powerups.splice(pi, 1);
}
}
// 胜负判定
if (remaining === 0) {
// 终态判断
if (bricks.length === 0 && phase === 'playing') {
phase = 'gameover';
gameoverReason = 'cleared_all';
} else if (balls.length === 0) {
const eng = ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x: 195, y: 422 },
count: 50,
speed: 140,
particleTime: 1.0,
colorStart: { r: 1, g: 1, b: 0.2, a: 1 },
colorEnd: { r: 1, g: 0.5, b: 0, a: 0 }
});
} catch (_) {}
}
if (eng && eng.audio && eng.audio.synth) {
try { eng.audio.synth.synthSfx([1, 0.2, 880, , , 0.3, , 1.5]); } catch (_) {}
}
}
if (balls.length === 0 && phase === 'playing') {
phase = 'gameover';
gameoverReason = 'ball_lost';
const eng = ctx.getEngine();
if (eng && eng.particles) {
try {
eng.particles.spawnEmitter({
pos: { x: 195, y: 422 },
count: 30,
speed: 80,
particleTime: 0.7,
colorStart: { r: 1, g: 0.2, b: 0.2, a: 1 },
colorEnd: { r: 0.5, g: 0, b: 0, a: 0 }
});
} catch (_) {}
}
if (eng && eng.audio && eng.audio.synth) {
try { eng.audio.synth.synthSfx([1, 0.15, 200, , , 0.2, , 0.8]); } catch (_) {}
}
}
}
function render(g) {
// 背景
g.fillStyle = '#0a0a1a';
g.fillRect(0, 0, WIDTH, HEIGHT);
g.fillRect(0, 0, 390, 844);
// 砖块
for (let brick of bricks) {
if (!brick.alive) continue;
g.fillStyle = '#44aaff';
for (const brick of bricks) {
g.fillStyle = brick.color;
g.fillRect(brick.x, brick.y, brick.w, brick.h);
g.strokeStyle = '#3399ee';
g.lineWidth = 1;
g.strokeStyle = '#ffffff';
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();
}
// 挡板
g.fillStyle = '#88ddff';
g.fillRect(paddle.x - paddle.w / 2, paddle.y - paddle.h / 2, paddle.w, paddle.h);
// 球
for (let ball of balls) {
if (!ball.alive) continue;
g.fillStyle = '#ffffff';
g.fillStyle = '#ffffff';
for (const ball of balls) {
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);
// 道具
for (const pu of powerups) {
g.fillStyle = pu.type === 'extraBall' ? '#44ff88' : '#4488ff';
g.fillRect(pu.x - pu.w / 2, pu.y - pu.h / 2, pu.w, pu.h);
}
// 闪白
if (flashTimer > 0) {
const alpha = Math.min(0.35, flashTimer / FLASH_DUR * 0.35);
g.fillStyle = `rgba(255,255,255,${alpha.toFixed(3)})`;
g.fillRect(0, 0, 390, 844);
}
// 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);
g.font = '18px monospace';
g.fillText('Score: ' + score, 10, 30);
g.fillText('Rem: ' + bricks.length, 200, 30);
// 游戏结束画面
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.font = '32px bold monospace';
g.fillStyle = '#ffcc00';
g.fillText(gameoverReason === 'cleared_all' ? 'YOU WIN!' : 'GAME OVER', 195, 400);
g.font = '16px monospace';
g.fillStyle = '#aaaaaa';
g.fillText('Tap to restart', 195, 450);
g.textAlign = 'left';
}
}
function destroy() {
for (let sub of inputSubs) {
for (const sub of subs) {
try { sub.cancel(); } catch (_) {}
}
inputSubs = [];
phase = 'booting';
subs = [];
balls = [];
bricks = [];
items = [];
paddleWidenTimer = 0;
paddle.w = PADDLE_ORIG_W;
pointerTargetX = WIDTH / 2;
powerups = [];
touchX = null;
}
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 }));
let mainBall = { x: 0, y: 0, vx: 0, vy: 0 };
if (balls.length > 0) {
const b = balls[0];
mainBall = { x: b.x, y: b.y, vx: b.vx, vy: b.vy };
}
return {
seed: boot?.seed,
seed,
state: () => ({
phase,
result: gameoverReason === 'cleared_all' ? 'win' : (gameoverReason === 'ball_lost' ? 'lose' : null),
result: phase === 'gameover' ? (gameoverReason === 'cleared_all' ? 'win' : 'lose') : null,
score,
remaining,
progress: 0,
gameoverReason,
remaining: bricks.length,
ball: mainBall,
paddle: { x: paddle.x, y: paddle.y, w: paddle.w },
balls: ballPositions,
balls: balls.map(b => ({ x: b.x, y: b.y }))
})
};
}
let boot;
return { init: (b) => { boot = b; return init(b); }, update, render, destroy, _forensicsView };
return { init, update, render, destroy, _forensicsView };
}

View File

@ -14,8 +14,10 @@
"type": "paddle-intercept",
"ballPath": "ball.x",
"paddleY": 800,
"steps": 70,
"stepMs": 130
"steps": 80,
"stepMs": 130,
"drainAfter": 48,
"drainX": 10
},
"expectedEngineCallPrefixes": [
"particles.",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -1,6 +1,6 @@
{
"gameId": "t2048",
"url": "http://localhost:4320/games/_wg1-gen/t2048/index.html",
"url": "http://localhost:4323/games/_wg1-gen/t2048/index.html",
"ts": null,
"guards": {
"A_boot": {
@ -8,8 +8,8 @@
},
"C_frame": {
"pass": true,
"f0": 47,
"f1": 77,
"f0": 49,
"f1": 79,
"delta": 30
},
"I_control": {
@ -18,7 +18,7 @@
},
"D_render": {
"pass": true,
"bright": 81964,
"bright": 81997,
"maxCh": 249
},
"E_live": {
@ -45,9 +45,9 @@
},
"G_input": {
"pass": true,
"inputHash": "400f8c09",
"controlHash": "9c7f4289",
"atFrame": 2967,
"inputHash": "19cc6df1",
"controlHash": "45497b06",
"atFrame": 2766,
"ctrlFrame": -1
},
"H_progress": {

View File

@ -2,13 +2,11 @@
"driver": {
"type": "key-cycle",
"keys": [
"ArrowLeft",
"ArrowUp",
"ArrowRight",
"ArrowDown"
"ArrowRight"
],
"steps": 120,
"stepMs": 220
"stepMs": 200
},
"expectedEngineCallPrefixes": [
"particles.",

View File

@ -1,11 +1,11 @@
{
"title": "小行星 Asteroids",
"brief": "做一款经典小行星 Asteroids。竖屏逻辑视口 390×844。\n- 玩家飞船在屏幕中央,有惯性(推进后保持速度、缓慢摩擦)。按键 ArrowLeft/ArrowRight 旋转朝向ArrowUp 朝当前朝向推进Space 朝当前朝向发射子弹。\n- 屏幕四周漂浮大陨石(用受控 random 初始化方向);子弹击中大陨石→裂成两块小陨石、得分 +1击中小陨石→击碎、得分 +1。所有元素屏幕环绕飞出一边从另一边回来。\n- 飞船被陨石撞到 = 失败;清光所有陨石 = 胜利。\n- 【CCD 注意】子弹与小陨石都可能高速移动,请用【连续碰撞/沿速度向量扫掠检测】判命中,避免高速穿透(漏判)。\n- 顶部文本 HUD分数 / 剩余陨石数 / 状态。\n- 【终态契约·务必遵守】失败/胜利即把 phase 置 'gameover' 并【驻留】(显示结果 + 「点击重玩」);点击重玩前 phase 恒 'gameover',绝不自动重开 / 不下一帧复位(宿主每 500ms 轮询)。\n- 【可测性·务必导出状态】_forensicsView().state() 纯只读实时真值phaseresult(null|'win'|'lose')score(整数,开局0)asteroidsRemaining(剩余陨石数)ship:{x,y,angle}。harness 据 score/phase 验进展与终局。\n- 反馈(juice):开火 / 击碎 / 撞毁触发 getEngine().particles.spawnEmitter + getEngine().audio.synth.synthSfx。\n- 画面:深色背景铺满;飞船 / 陨石 / 子弹 / 文字亮色实心。",
"brief": "做一款经典小行星 Asteroids。竖屏逻辑视口 390×844。\n- 玩家飞船在屏幕中央,有惯性(推进后保持速度、缓慢摩擦)。按键 ArrowLeft/ArrowRight 旋转朝向ArrowUp 朝当前朝向推进Space 朝当前朝向发射子弹。\n- 屏幕四周漂浮大陨石(用受控 random 初始化方向);子弹击中大陨石→裂成两块小陨石、得分 +1击中小陨石→击碎、得分 +1。所有元素屏幕环绕飞出一边从另一边回来。\n- 飞船被陨石撞到 = 失败;清光所有陨石 = 胜利。\n- 【CCD 注意】子弹与小陨石都可能高速移动,请用【连续碰撞/沿速度向量扫掠检测】判命中,避免高速穿透(漏判)。\n- 顶部文本 HUD分数 / 剩余陨石数 / 状态。\n- 【终态契约·务必遵守】失败/胜利即把 phase 置 'gameover' 并【驻留】(显示结果 + 「点击重玩」);点击重玩前 phase 恒 'gameover',绝不自动重开 / 不下一帧复位(宿主每 500ms 轮询)。\n- 【可测性·务必导出状态】_forensicsView().state() 纯只读实时真值phaseresult(null|'win'|'lose')score(整数,开局0)asteroidsRemaining(剩余陨石数)ship:{x,y,angle}angle=朝向弧度,与 atan2 同向:指上≈-π/2asteroids:[{x,y}...](所有活跃陨石中心逻辑像素,供 harness 旋向瞄准开火)。harness 据 ship.angle/asteroids 旋转对齐开火、据 score/phase 验进展与终局。\n- 反馈(juice):开火 / 击碎 / 撞毁触发 getEngine().particles.spawnEmitter + getEngine().audio.synth.synthSfx。\n- 画面:深色背景铺满;飞船 / 陨石 / 子弹 / 文字亮色实心。",
"play_spec": {
"driver": { "type": "key-cycle", "keys": ["ArrowLeft", "ArrowUp", "Space", "ArrowRight", "Space", "Space"], "steps": 50, "stepMs": 200 },
"driver": { "type": "aim-fire", "shipPath": "ship", "targetsPath": "asteroids", "fireKey": "Space", "steps": 90, "stepMs": 120, "downMs": 80 },
"expectedEngineCallPrefixes": ["particles.", "audio.synth"],
"assertAfterPlay": [
{ "path": "score", "op": "increased", "why": "循环旋转+推进+开火→击碎陨石得分上升;若高速子弹穿透(CCD缺)或打不中则不升=暴露 CCD 缺口" }
{ "path": "score", "op": "increased", "why": "aim-fire 读陨石坐标旋向对齐开火→击碎得分上升;若游戏不暴露陨石坐标致盲射则不升=可测性边界" }
],
"expectLatch": true
}

View File

@ -1,8 +1,8 @@
{
"title": "Flappy Bird",
"brief": "做一款经典 Flappy Bird。竖屏逻辑视口 390×844坐标 y 向下为正y 越大越靠近屏幕底部)。\n- 小鸟固定在屏幕左侧 x≈110受【适中】重力持续下落点击任意位置 = 小鸟获得一个向上的瞬时速度(拍翅)。**拍翅力度与重力要平衡**:玩家能轻松让小鸟在屏内上下浮动,不会一拍就冲顶、也不会一松就坠底;开局小鸟稳定悬浮在屏幕中部附近等待操作。\n- 成对的上下管道从右向左匀速滚动;缝隙【宽松】——垂直缺口高度 ≥ 屏高的 24%;管道水平间距给足反应时间:开局到第一对管道 ≥ 1.5 秒,之后每对间距稳定。小鸟从缝隙穿过 = 得分 +1。\n- 撞到管道 / 触顶 / 触底 = 失败。\n- 顶部文本 HUD得分 / 状态。\n- 【终态契约·务必遵守】失败即把 _forensicsView().state().phase 置 'gameover' 并【驻留】(显示结果 + 「点击重玩」提示);玩家点击重玩前 phase 必须恒为 'gameover'**绝不自动重开 / 不下一帧复位**(宿主每 500ms 轮询 phase 判结束。gameover 下点击一律忽略;仅「重玩」按钮重开。\n- 【可测性·务必导出状态】_forensicsView().state() 返回纯只读实时真值:\n · phaseresultnull | 'lose'\n · score整数已穿过管道数开局必为 0\n · bird{ x, y, vy }小鸟中心逻辑像素坐标与竖直速度y 向下为正,实时)\n · nextGap{ x, centerY }(小鸟前方最近一对管道【缝隙竖直中点】的 x 与 y 逻辑像素;缝隙中心 y 务必准确harness 据此控制拍翅;无管道时为 null。\n harness 读 bird.y 与 nextGap.centerY 自动拍翅穿缝:鸟低于缝中心就拍、高于则任其下落。\n- 反馈(juice):拍翅 / 得分 / 撞击触发 getEngine().particles.spawnEmitter + getEngine().audio.synth.synthSfx。\n- 画面:深色背景铺满 390×844小鸟 / 管道 / 文字亮色实心。",
"brief": "做一款经典 Flappy Bird。竖屏逻辑视口 390×844坐标 y 向下为正y 越大越靠近屏幕底部)。\n- 小鸟固定在屏幕左侧 x≈110受【适中】重力持续下落点击任意位置 = 小鸟获得一个向上的瞬时速度(拍翅)。**拍翅力度与重力要平衡**:玩家能轻松让小鸟在屏内上下浮动,不会一拍就冲顶、也不会一松就坠底;开局小鸟稳定悬浮在屏幕中部附近等待操作。\n- 成对的上下管道从右向左匀速滚动;缝隙【宽松】——垂直缺口高度 ≥ 屏高的 24%;管道水平间距给足反应时间:开局到第一对管道 ≥ 1.5 秒,之后每对间距稳定。小鸟从缝隙穿过 = 得分 +1。缝隙中心 y 随机落在中部可达区间(约 [300, 560]),勿贴顶/贴底致正常拍翅无法穿越。\n- 撞到管道 / 触顶 / 触底 = 失败。\n- 顶部文本 HUD得分 / 状态。\n- 【终态契约·务必遵守】失败即把 _forensicsView().state().phase 置 'gameover' 并【驻留】(显示结果 + 「点击重玩」提示);玩家点击重玩前 phase 必须恒为 'gameover'**绝不自动重开 / 不下一帧复位**(宿主每 500ms 轮询 phase 判结束。gameover 下点击一律忽略;仅「重玩」按钮重开。\n- 【可测性·务必导出状态】_forensicsView().state() 返回纯只读实时真值:\n · phaseresultnull | 'lose'\n · score整数已穿过管道数开局必为 0\n · bird{ x, y, vy }小鸟中心逻辑像素坐标与竖直速度y 向下为正,实时)\n · nextGap{ x, centerY }(小鸟前方最近一对管道【缝隙竖直中点】的 x 与 y 逻辑像素;缝隙中心 y 务必准确harness 据此控制拍翅;无管道时为 null。\n harness 读 bird.y 与 nextGap.centerY 自动拍翅穿缝:鸟低于缝中心就拍、高于则任其下落。\n- 反馈(juice):拍翅 / 得分 / 撞击触发 getEngine().particles.spawnEmitter + getEngine().audio.synth.synthSfx。\n- 画面:深色背景铺满 390×844小鸟 / 管道 / 文字亮色实心。",
"play_spec": {
"driver": { "type": "flap-to-gap", "steps": 70, "stepMs": 110, "margin": 14 },
"driver": { "type": "flap-to-gap", "steps": 100, "stepMs": 90, "margin": 14, "lookahead": 0.18, "aimBias": 24, "vyPath": "bird.vy" },
"expectedEngineCallPrefixes": ["particles.", "audio.synth"],
"assertAfterPlay": [
{ "path": "score", "op": "increased", "why": "适配性真玩(读 bird.y/nextGap.centerY 拍翅穿缝)→得分上升;物理太硬秒死或穿不过则 0" }

View File

@ -1,9 +1,9 @@
{
"title": "多球弹球 Multiball",
"brief": "做一款多球打砖块变体。竖屏逻辑视口 390×844。\n- 底部一块玩家挡板水平移动厚度≥14。开局有 2~3 个球同时斜向匀速运动,碰墙/挡板反弹(碰挡板按击中位置调水平速度)。\n- 顶部一片砖块网格;任一球击中砖块→该砖消除、得分 +1。部分砖被击碎后掉落道具下落挡板接到=增球或扩板)。\n- 所有球都掉到挡板下方 = 失败;清光所有砖 = 通关。\n- 命中砖块触发屏震 + 闪白getEngine().particles.shakeScreen / flashScreen 或 spawnEmitter。\n- 【CCD 注意】球速较快,碰撞请沿速度向量做连续检测,避免高速球穿砖/穿墙。\n- 球速:合速度约每秒 320~420 逻辑像素。\n- 【终态契约·务必遵守】失败/通关即把 phase 置 'gameover' 并【驻留】(结果 + 「点击重玩」);点击重玩前 phase 恒 'gameover',绝不自动重开 / 不下一帧复位。gameover 下点击忽略。\n- 【控制·务必跟手】挡板朝触摸点 x 平滑逼近(每帧插值 0.2~0.35),绝非一格一跳。\n- 【可测性·务必导出状态】_forensicsView().state() 纯只读实时真值phaseresult(null|'win'|'lose')scoreremaining(剩余砖数)ball:{x,y,vx,vy}(任选一个【活跃主球】的位置,供 harness 挡板接球用,实时paddle:{x,y,w}balls:[{x,y}...](所有活跃球,可选。harness 读 ball.x 自动移挡板接球、据 remaining/phase 验进展与终局。\n- 反馈(juice):击砖/接道具/掉球触发 getEngine().particles.spawnEmitter + getEngine().audio.synth.synthSfx。\n- 画面:深色背景铺满;挡板/砖/球/道具/文字亮色实心。",
"brief": "做一款多球打砖块变体。竖屏逻辑视口 390×844。\n- 底部一块玩家挡板水平移动厚度≥14。开局有 2~3 个球同时斜向匀速运动,碰墙/挡板反弹(碰挡板按击中位置调水平速度)。\n- 顶部一片砖块网格;任一球击中砖块→该砖消除、得分 +1。部分砖被击碎后掉落道具下落挡板接到=增球或扩板)。\n- 任一球 y 越过屏幕底沿即出局丢失(绝不在底部反弹回弹);所有球都掉到挡板下方出局 = 失败;清光所有砖 = 通关。\n- 命中砖块触发屏震 + 闪白getEngine().particles.shakeScreen / flashScreen 或 spawnEmitter。\n- 【CCD 注意】球速较快,碰撞请沿速度向量做连续检测,避免高速球穿砖/穿墙。\n- 球速:合速度约每秒 320~420 逻辑像素。\n- 【终态契约·务必遵守】失败/通关即把 phase 置 'gameover' 并【驻留】(结果 + 「点击重玩」);点击重玩前 phase 恒 'gameover',绝不自动重开 / 不下一帧复位。gameover 下点击忽略。\n- 【控制·务必跟手】挡板朝触摸点 x 平滑逼近(每帧插值 0.2~0.35),绝非一格一跳。\n- 【可测性·务必导出状态】_forensicsView().state() 纯只读实时真值phaseresult(null|'win'|'lose')scoreremaining(剩余砖数)ball:{x,y,vx,vy}(任选一个【活跃主球】的位置,供 harness 挡板接球用,实时paddle:{x,y,w}balls:[{x,y}...](所有活跃球,可选。harness 读 ball.x 自动移挡板接球、据 remaining/phase 验进展与终局。\n- 反馈(juice):击砖/接道具/掉球触发 getEngine().particles.spawnEmitter + getEngine().audio.synth.synthSfx。\n- 画面:深色背景铺满;挡板/砖/球/道具/文字亮色实心。",
"play_spec": {
"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 },
"driver": { "type": "paddle-intercept", "ballPath": "ball.x", "paddleY": 800, "steps": 80, "stepMs": 130, "drainAfter": 48, "drainX": 10 },
"expectedEngineCallPrefixes": ["particles.", "audio.synth"],
"assertAfterPlay": [
{ "path": "remaining", "op": "decreased", "why": "多球+挡板接球击碎砖→剩余下降;碰撞失效/高速穿透(CCD缺)则不降" }

View File

@ -1,47 +1,22 @@
{
"game_id": "asteroids",
"model": "deepseek-v4-flash",
"pass": false,
"retries": 2,
"pass": true,
"retries": 0,
"attempts": [
{
"attempt": 0,
"stage": "play",
"errors": [],
"usage": {
"prompt_tokens": 3905,
"completion_tokens": 8225,
"total_tokens": 12130,
"wall_s": 98.632,
"id": "e0b13e0d-3a3c-49e3-9b8f-3fda547153dd",
"prompt_tokens": 3966,
"completion_tokens": 10009,
"total_tokens": 13975,
"wall_s": 118.648,
"id": "98a2a1a3-f11d-4bfc-8c4a-1bd0ba3d8370",
"finish_reason": "stop"
},
"play_pass": false,
"verdict_guards": {
"A_boot": true,
"C_frame": true,
"I_control": true,
"D_render": true,
"E_live": true,
"B_uncaught": true,
"F_wiring": false,
"G_input": true,
"H_progress": false
}
},
{
"attempt": 1,
"stage": "play",
"errors": [],
"usage": {
"prompt_tokens": 4078,
"completion_tokens": 8972,
"total_tokens": 13050,
"wall_s": 101.377,
"id": "c2b6f24b-41f7-45de-9d3a-0b3723e79aa3",
"finish_reason": "stop"
},
"play_pass": false,
"play_pass": true,
"verdict_guards": {
"A_boot": true,
"C_frame": true,
@ -51,32 +26,7 @@
"B_uncaught": true,
"F_wiring": true,
"G_input": true,
"H_progress": false
}
},
{
"attempt": 2,
"stage": "play",
"errors": [],
"usage": {
"prompt_tokens": 4041,
"completion_tokens": 11024,
"total_tokens": 15065,
"wall_s": 91.997,
"id": "ea98e889-f96b-42d8-8c0e-691432017bc1",
"finish_reason": "stop"
},
"play_pass": false,
"verdict_guards": {
"A_boot": true,
"C_frame": true,
"I_control": true,
"D_render": true,
"E_live": true,
"B_uncaught": true,
"F_wiring": false,
"G_input": true,
"H_progress": false
"H_progress": true
}
}
],
@ -93,8 +43,8 @@
"C_frame": {
"pass": true,
"f0": 49,
"f1": 79,
"delta": 30
"f1": 80,
"delta": 31
},
"I_control": {
"pass": true,
@ -102,22 +52,35 @@
},
"D_render": {
"pass": true,
"bright": 66529,
"bright": 73961,
"maxCh": 255
},
"E_live": {
"pass": true,
"distinctStates": 52,
"samples": 52
"distinctStates": 81,
"samples": 81
},
"B_uncaught": {
"pass": true,
"uncaught": []
},
"F_wiring": {
"pass": false,
"callCount": 0,
"sample": [],
"pass": true,
"callCount": 41,
"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"
@ -125,62 +88,113 @@
},
"G_input": {
"pass": true,
"inputHash": "241aa975",
"controlHash": "7f223357",
"atFrame": 1241,
"ctrlFrame": 1244
"inputHash": "abb370ce",
"controlHash": "38e0cc7c",
"atFrame": 1581,
"ctrlFrame": -1
},
"H_progress": {
"pass": false,
"pass": true,
"checks": [
{
"path": "score",
"op": "increased",
"before": 0,
"after": 0,
"pass": false,
"why": "循环旋转+推进+开火→击碎陨石得分上升;若高速子弹穿透(CCD缺)或打不中则不升=暴露 CCD 缺口"
"after": 6,
"pass": true,
"why": "aim-fire 读陨石坐标旋向对齐开火→击碎得分上升;若游戏不暴露陨石坐标致盲射则不升=可测性边界"
}
],
"latch": {
"pass": false,
"reason": "真玩未到达 gameover 终态(终态不可达/无失败态/play-spec 未驱动到结束)",
"phaseNow": "playing"
"pass": true,
"dwelledMs": 600,
"after": "gameover",
"reason": ""
},
"state0": {
"phase": "playing",
"result": null,
"score": 0,
"asteroidsRemaining": 5,
"asteroidsRemaining": 6,
"ship": {
"x": 195,
"y": 422,
"angle": -1.5707963267948966
}
},
"asteroids": [
{
"x": 96.99717124909638,
"y": 519.2675040247642
},
{
"x": 319.4950147635825,
"y": 629.6736080402688
},
{
"x": 1.5485410220474345,
"y": 718.9562386134752
},
{
"x": 73.61813029544744,
"y": 761.5286812129356
},
{
"x": 337.5254555930884,
"y": 529.2215852456798
},
{
"x": 324.4826751629315,
"y": 697.42908304362
}
]
},
"state1": {
"phase": "playing",
"result": null,
"score": 0,
"asteroidsRemaining": 5,
"phase": "gameover",
"result": "lose",
"score": 6,
"asteroidsRemaining": 6,
"ship": {
"x": 195,
"y": 422,
"angle": -1.5707963267948966
}
"angle": -3.120796326794893
},
"asteroids": [
{
"x": -27.856391456713904,
"y": 735.1075081715107
},
{
"x": 116.74067642420584,
"y": 770.5545437859361
},
{
"x": 166.3536431747016,
"y": 455.6770210725548
},
{
"x": 272.7968611275529,
"y": 641.2863973557977
},
{
"x": 171.07105373838698,
"y": 848.8609648800336
},
{
"x": 213.6553002398836,
"y": 87.47766219533963
}
]
}
}
},
"pass": false,
"pass": true,
"notes": []
},
"tokens": {
"prompt": 12024,
"completion": 28221
"prompt": 3966,
"completion": 10009
},
"request_ids": [
"e0b13e0d-3a3c-49e3-9b8f-3fda547153dd",
"c2b6f24b-41f7-45de-9d3a-0b3723e79aa3",
"ea98e889-f96b-42d8-8c0e-691432017bc1"
"98a2a1a3-f11d-4bfc-8c4a-1bd0ba3d8370"
]
}

View File

@ -1,7 +1,7 @@
{
"game_id": "flappy",
"model": "deepseek-v4-pro",
"pass": false,
"model": "deepseek-v4-flash",
"pass": true,
"retries": 2,
"attempts": [
{
@ -9,11 +9,11 @@
"stage": "play",
"errors": [],
"usage": {
"prompt_tokens": 4029,
"completion_tokens": 8475,
"total_tokens": 12504,
"wall_s": 143.451,
"id": "6a71d430-00e3-4772-9b81-1228bc19bf59",
"prompt_tokens": 4060,
"completion_tokens": 8280,
"total_tokens": 12340,
"wall_s": 291.239,
"id": "ace28b05-5cab-405f-9a94-891cb9ebdb6f",
"finish_reason": "stop"
},
"play_pass": false,
@ -25,33 +25,23 @@
"E_live": true,
"B_uncaught": true,
"F_wiring": true,
"G_input": true,
"G_input": false,
"H_progress": false
}
},
{
"attempt": 1,
"stage": "play",
"errors": [],
"stage": "validate",
"errors": [
"违禁:禁 Date.now用 ctx.time.nowMs"
],
"usage": {
"prompt_tokens": 4163,
"completion_tokens": 7746,
"total_tokens": 11909,
"wall_s": 316.381,
"id": "5c3db979-5f22-4b7e-bde8-62d2c07e9071",
"prompt_tokens": 4242,
"completion_tokens": 5776,
"total_tokens": 10018,
"wall_s": 74.574,
"id": "e94fa6f0-1e0a-4163-a3f9-476480d82641",
"finish_reason": "stop"
},
"play_pass": false,
"verdict_guards": {
"A_boot": true,
"C_frame": true,
"I_control": true,
"D_render": true,
"E_live": true,
"B_uncaught": true,
"F_wiring": true,
"G_input": true,
"H_progress": false
}
},
{
@ -59,14 +49,14 @@
"stage": "play",
"errors": [],
"usage": {
"prompt_tokens": 4163,
"completion_tokens": 7230,
"total_tokens": 11393,
"wall_s": 125.343,
"id": "97b8fe12-616d-41bc-b60a-6c32efe20a6d",
"prompt_tokens": 4106,
"completion_tokens": 8529,
"total_tokens": 12635,
"wall_s": 95.039,
"id": "030f4ab8-f4e1-44ad-88ad-516ffe78d673",
"finish_reason": "stop"
},
"play_pass": false,
"play_pass": true,
"verdict_guards": {
"A_boot": true,
"C_frame": true,
@ -76,7 +66,7 @@
"B_uncaught": true,
"F_wiring": true,
"G_input": true,
"H_progress": false
"H_progress": true
}
}
],
@ -92,8 +82,8 @@
},
"C_frame": {
"pass": true,
"f0": 48,
"f1": 78,
"f0": 51,
"f1": 81,
"delta": 30
},
"I_control": {
@ -102,13 +92,13 @@
},
"D_render": {
"pass": true,
"bright": 178920,
"bright": 189671,
"maxCh": 255
},
"E_live": {
"pass": true,
"distinctStates": 6,
"samples": 6
"distinctStates": 40,
"samples": 41
},
"B_uncaught": {
"pass": true,
@ -116,7 +106,7 @@
},
"F_wiring": {
"pass": true,
"callCount": 8,
"callCount": 44,
"sample": [
"particles.spawnEmitter",
"audio.synth.synthSfx",
@ -125,6 +115,10 @@
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx"
],
"expected": [
@ -134,20 +128,20 @@
},
"G_input": {
"pass": true,
"inputHash": "0e133ce8",
"controlHash": "4fb72d48",
"atFrame": 214,
"ctrlFrame": 218
"inputHash": "4ae5c2e6",
"controlHash": "aad4cfb2",
"atFrame": 749,
"ctrlFrame": 750
},
"H_progress": {
"pass": false,
"pass": true,
"checks": [
{
"path": "score",
"op": "increased",
"before": 0,
"after": 0,
"pass": false,
"after": 5,
"pass": true,
"why": "适配性真玩(读 bird.y/nextGap.centerY 拍翅穿缝)→得分上升;物理太硬秒死或穿不过则 0"
}
],
@ -163,42 +157,43 @@
"score": 0,
"bird": {
"x": 110,
"y": 559.1999999999999,
"vy": 336
},
"nextGap": {
"x": 600,
"centerY": 174.12938179075718
"y": 422,
"vy": 0
},
"nextGap": null,
"remaining": 0,
"progress": null,
"gameoverReason": ""
},
"state1": {
"phase": "gameover",
"result": "lose",
"score": 0,
"score": 5,
"bird": {
"x": 110,
"y": 523.0999999999999,
"vy": -169
"y": 405.6666666666665,
"vy": -116.66666666666673
},
"nextGap": {
"x": 600,
"centerY": 174.12938179075718
"x": 120,
"centerY": 302.81019813381135
},
"gameoverReason": "pipe_collision"
"remaining": 1,
"progress": null,
"gameoverReason": "pipe"
}
}
},
"pass": false,
"pass": true,
"notes": []
},
"tokens": {
"prompt": 12355,
"completion": 23451
"prompt": 12408,
"completion": 22585
},
"request_ids": [
"6a71d430-00e3-4772-9b81-1228bc19bf59",
"5c3db979-5f22-4b7e-bde8-62d2c07e9071",
"97b8fe12-616d-41bc-b60a-6c32efe20a6d"
"ace28b05-5cab-405f-9a94-891cb9ebdb6f",
"e94fa6f0-1e0a-4163-a3f9-476480d82641",
"030f4ab8-f4e1-44ad-88ad-516ffe78d673"
]
}

View File

@ -1,72 +1,22 @@
{
"game_id": "multiball",
"model": "deepseek-v4-flash",
"pass": false,
"retries": 2,
"pass": true,
"retries": 0,
"attempts": [
{
"attempt": 0,
"stage": "play",
"errors": [],
"usage": {
"prompt_tokens": 3991,
"completion_tokens": 13015,
"total_tokens": 17006,
"wall_s": 114.807,
"id": "49098ea1-7ede-4e09-9e08-13f6dbdc486d",
"prompt_tokens": 4013,
"completion_tokens": 12724,
"total_tokens": 16737,
"wall_s": 129.522,
"id": "2e8c56b4-8545-4607-8206-a1d26d6fcfd4",
"finish_reason": "stop"
},
"play_pass": false,
"verdict_guards": {
"A_boot": true,
"C_frame": true,
"I_control": false,
"D_render": true,
"E_live": true,
"B_uncaught": true,
"F_wiring": true,
"G_input": true,
"H_progress": true
}
},
{
"attempt": 1,
"stage": "play",
"errors": [],
"usage": {
"prompt_tokens": 4118,
"completion_tokens": 7988,
"total_tokens": 12106,
"wall_s": 86.921,
"id": "afc6dad5-f0d3-4ded-b783-70ceffd60aca",
"finish_reason": "stop"
},
"play_pass": false,
"verdict_guards": {
"A_boot": true,
"C_frame": true,
"I_control": false,
"D_render": true,
"E_live": true,
"B_uncaught": true,
"F_wiring": true,
"G_input": true,
"H_progress": true
}
},
{
"attempt": 2,
"stage": "play",
"errors": [],
"usage": {
"prompt_tokens": 4116,
"completion_tokens": 14371,
"total_tokens": 18487,
"wall_s": 155.242,
"id": "abc8fb1c-3c77-478e-bdaa-ec0774104d96",
"finish_reason": "stop"
},
"play_pass": false,
"play_pass": true,
"verdict_guards": {
"A_boot": true,
"C_frame": true,
@ -76,7 +26,7 @@
"B_uncaught": true,
"F_wiring": true,
"G_input": true,
"H_progress": false
"H_progress": true
}
}
],
@ -92,9 +42,9 @@
},
"C_frame": {
"pass": true,
"f0": 46,
"f1": 75,
"delta": 29
"f0": 49,
"f1": 79,
"delta": 30
},
"I_control": {
"pass": true,
@ -102,31 +52,31 @@
"results": [
{
"tapX": 50,
"before": 194.99999999990354,
"after": 50,
"moved": -144.99999999990354,
"distToTarget": 0,
"before": 195,
"after": 54.09589111049999,
"moved": -140.9041088895,
"distToTarget": 4.095891110499991,
"pass": true
},
{
"tapX": 340,
"before": 50,
"after": 310,
"moved": 260,
"distToTarget": 30,
"before": 54.09589111049999,
"after": 306.3636674932592,
"moved": 252.2677763827592,
"distToTarget": 33.636332506740814,
"pass": true
}
]
},
"D_render": {
"pass": true,
"bright": 146201,
"bright": 154512,
"maxCh": 255
},
"E_live": {
"pass": true,
"distinctStates": 64,
"samples": 72
"distinctStates": 15,
"samples": 15
},
"B_uncaught": {
"pass": true,
@ -134,10 +84,10 @@
},
"F_wiring": {
"pass": true,
"callCount": 15,
"callCount": 27,
"sample": [
"audio.synth.synthSfx",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
@ -145,8 +95,8 @@
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"audio.synth.synthSfx",
"particles.spawnEmitter",
"audio.synth.synthSfx",
"particles.spawnEmitter"
],
"expected": [
@ -156,106 +106,88 @@
},
"G_input": {
"pass": true,
"inputHash": "9cd794ce",
"controlHash": "6afc48cc",
"atFrame": 1984,
"ctrlFrame": -1
"inputHash": "7a86cc2e",
"controlHash": "b60e0fa8",
"atFrame": 615,
"ctrlFrame": 620
},
"H_progress": {
"pass": false,
"pass": true,
"checks": [
{
"path": "remaining",
"op": "decreased",
"before": 40,
"after": 36,
"before": 78,
"after": 69,
"pass": true,
"why": "多球+挡板接球击碎砖→剩余下降;碰撞失效/高速穿透(CCD缺)则不降"
}
],
"latch": {
"pass": false,
"reason": "真玩未到达 gameover 终态(终态不可达/无失败态/play-spec 未驱动到结束)",
"phaseNow": "playing"
"pass": true,
"dwelledMs": 600,
"after": "gameover",
"reason": ""
},
"state0": {
"phase": "playing",
"result": null,
"score": 0,
"remaining": 40,
"progress": 0,
"gameoverReason": "",
"remaining": 78,
"ball": {
"x": 171,
"y": 793,
"vx": -103.2459330675854,
"vy": 317.75830848240577
"x": 178.39632164686918,
"y": 772.8069677809253,
"vx": -149.90107628982514,
"vy": -312.5555661244529
},
"paddle": {
"x": 194.99999700380667,
"y": 804,
"x": 195,
"y": 810,
"w": 80
},
"balls": [
{
"x": 171,
"y": 793
"x": 55.977109343511984,
"y": 517.5532554459528
},
{
"x": 187,
"y": 793
"x": 212.95210833312012,
"y": 465.970216069029
},
{
"x": 203,
"y": 793
"x": 199.6740988851525,
"y": 475.6221190751453
}
]
},
"state1": {
"phase": "playing",
"result": null,
"score": 4,
"remaining": 36,
"progress": 0,
"gameoverReason": "",
"phase": "gameover",
"result": "lose",
"score": 9,
"remaining": 69,
"ball": {
"x": 171,
"y": 793,
"vx": -103.2459330675854,
"vy": 317.75830848240577
"x": 178.39632164686918,
"y": 772.8069677809253,
"vx": -149.90107628982514,
"vy": -312.5555661244529
},
"paddle": {
"x": 171.00000000000003,
"y": 804,
"x": 179.63617952826013,
"y": 810,
"w": 80
},
"balls": [
{
"x": 171,
"y": 793
},
{
"x": 187,
"y": 793
},
{
"x": 203,
"y": 793
}
]
"balls": []
}
}
},
"pass": false,
"pass": true,
"notes": []
},
"tokens": {
"prompt": 12225,
"completion": 35374
"prompt": 4013,
"completion": 12724
},
"request_ids": [
"49098ea1-7ede-4e09-9e08-13f6dbdc486d",
"afc6dad5-f0d3-4ded-b783-70ceffd60aca",
"abc8fb1c-3c77-478e-bdaa-ec0774104d96"
"2e8c56b4-8545-4607-8206-a1d26d6fcfd4"
]
}

View File

@ -1,7 +1,7 @@
"""prompt.py —— W-G1 生成上下文包:把装载契约/能力面/禁用项/硬规则蒸馏成 system prompt + few-shot。
来源理解阶段 7 份地图的综合engine-caps/load-contract/ref-shape/plugin-surface
原则把便宜模型的活压到最小只产出一个纯工厂模块能力恒经 boot.ctx质量由 harness 门兜
原则把便宜模型的活压到最小只产出一个纯工厂模块能力恒经 boot.ctx质量由 harness 门兜
"""
from pathlib import Path

View File

@ -3,7 +3,7 @@
job in brief题面 + play-spec+ model + max_retries
result{ pass, retries, attempts[{usage,stage,errors}], bundle 路径, verdict 四件套, factory 路径 }
闭环generate validate(静态+node --check) scaffold+build(esbuild) play(CDP )
闭环generate validate(静态+node --check) scaffold+build(esbuild) play(CDP )
任一阶段失败即把错因回喂模型重试 max_retries "能生成 ≠ 能玩"必须过 play 才算 pass
"""