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>
@ -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 + 增强 driver〔vy 前瞻 / 弃守排空〕后 flash 翻绿,multiball retries=0);t2048/asteroids 宿主键盘桥(L0 已修)重打包即活;angrybirds/asteroids 盲玩=可测性边界(asteroids 已 enrich `asteroids:[{x,y}]` + aim-fire driver 转确定性瞄准)。**生成侧铁律:单件失败禁直接归『模型造不出』,必重生成区分『实例 bug / 可测性缺口 / 模型短板』**——『模型造不出』是最大伪报源(原 flappy『升 pro 仍挂=模型天花板』被本轮证伪=driver+实例)。
|
||||
|
||||
@ -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 兜。
|
||||
|
||||
|
||||
@ -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 款补全**:留后续波次。
|
||||
|
||||
@ -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)。
|
||||
@ -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 px(y 越小越高),留下落余量→落入缝心而非贴下沿
|
||||
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}] 选最近目标,按 origin→target 方向【反解拖拽矢量】发射;
|
||||
* 发射物理常数(重力/力度比例)游戏未暴露 → 用「力度扫描 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':每步读球 x、tap 到球正下方把挡板移过去接(真玩到清砖);到 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 门会判红线)
|
||||
|
||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 36 KiB |
@ -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": {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}}}
|
||||
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 42 KiB |
@ -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": []
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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}}}
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 18 KiB |
@ -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": []
|
||||
}
|
||||
@ -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 };
|
||||
}
|
||||
@ -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.",
|
||||
|
||||
@ -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}}}
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 17 KiB |
@ -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": []
|
||||
}
|
||||
@ -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 };
|
||||
}
|
||||
@ -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.",
|
||||
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
@ -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": {
|
||||
|
||||
@ -2,13 +2,11 @@
|
||||
"driver": {
|
||||
"type": "key-cycle",
|
||||
"keys": [
|
||||
"ArrowLeft",
|
||||
"ArrowUp",
|
||||
"ArrowRight",
|
||||
"ArrowDown"
|
||||
"ArrowRight"
|
||||
],
|
||||
"steps": 120,
|
||||
"stepMs": 220
|
||||
"stepMs": 200
|
||||
},
|
||||
"expectedEngineCallPrefixes": [
|
||||
"particles.",
|
||||
|
||||
@ -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() 纯只读实时真值:phase;result(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() 纯只读实时真值:phase;result(null|'win'|'lose');score(整数,开局0);asteroidsRemaining(剩余陨石数);ship:{x,y,angle}(angle=朝向弧度,与 atan2 同向:指上≈-π/2);asteroids:[{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
|
||||
}
|
||||
|
||||
@ -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 · phase;result:null | '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 · phase;result:null | '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" }
|
||||
|
||||
@ -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() 纯只读实时真值:phase;result(null|'win'|'lose');score;remaining(剩余砖数);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() 纯只读实时真值:phase;result(null|'win'|'lose');score;remaining(剩余砖数);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缺)则不降" }
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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。
|
||||
"""
|
||||
|
||||
|
||||