feat(cheap-gen): W-AXIS-V2 波2——取证契约退役+提示全加载链清洗,四门∧测试员为唯一验收链
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled

opus 执行位交付、主会话四步验收过(三套测试亲跑/红线 diff 亲读/冒烟截图亲眼):

- 验收消费:check 新增源码级插件调用存在断言(_pluginCallPresenceErrors 词法层,
  接替 F_wiring 真接线护栏);_forensicsView「必须实现」拦截摘除,降级为可选调试
  接口(handleTap 输入契约与 ctx.log 日志纪律保留);gate_judge 放行措辞对齐四门
- play-spec/驱动器退役(逐件裁定 10 件):gen.mjs done 门自动产 play-spec 摘除、
  cheap_gates/cheap_studio/cheap_modify/service_driver 调用摘除、_read_driver_type
  改读 evidence/playtest/playtest.json;golden-spec 三工具(auto_vs_golden/bake_off/
  compare_node)显式封存;ensure_play_spec 两实现保留一个版本窗口(只摘调用不删码)
- prompt 全加载链清洗:cheap-system.md v1.6.3→1.7.0(删 _forensicsView 自动验收
  契约红线与全部 targets/occupied 语义,五法句/品类指路同步),cheap_roles 内置
  回退逐字节同步(test_roles 12/12 证 parity),modify 切片经运行时切块自动同步;
  5 品类 skills+littlejs 手册+recipes×2+模板 8 件+README 6 件教学清洗,真设计
  价值(单击 casual/可解性质/双层反馈/latch)保留
- 批账:hard_genre_batch/xtheme 行加 playtest 段,_row_total_cost 修 shadow 下
  测试员成本漏算(v2 取 max 防双计)

验收(主会话亲验):pytest cheap-worker 512 绿 + tier2 141 绿 + node 49/49;
25 局基线新旧口径重放对照——真坏 5 局全拦(floor 拦 2/测试员拦 3)、无一例旧拦
新放,翻案 2 局均为破案坐实的仪器误杀;n=3 冒烟(mode=v2)全链绿:w2v2-sim
harness verdict.pass=True 而测试员 reject(机械放行测试员逮住,盘上双证)、
w2v2-sim-fix 现象反馈两轮回喂→fix2 收敛(配方显示修成,局内截图亲眼);
rg 零残留(模板残留均为「验收不读它」的降级调试接口合规形态);真跑 ¥8.4。

已知余项:repairFeedback 有产地、Service 续修环消费者未接(测试员 reject 不入
RepairMiddleware,续修判据仍四门)——是否接入随波3 校准数据一并裁;v1 配置级
回退窗口关闭为创始人知悉项,genconfig 默认 mode 仍 shadow、切 v2 归创始人。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
lili 2026-07-10 09:21:04 -07:00
parent 79313bf2f4
commit 8d851ea716
49 changed files with 318 additions and 201 deletions

View File

@ -34,7 +34,7 @@ description: "当为非遗/传统技艺题材 brief 产设计阶段玩法方案
| 范式 | 一句话 | 我们运行时怎么落(插件) |
|---|---|---|
| **工序节拍**(品类骨架默认) | 一件作品 = 一条 ≥3 道的工序链,工序依次「就绪」,在火候窗口内点掉 → 出成品;错过窗口回炉 | `_template-feiyi` 已接好整条循环:session-score 计工钱 + timer-scheduler 限时 + hud-ui 工位/窗口条 + particles-juice 出彩 + audio-music 音效 |
| **图样拼合** | 按纹样图谱点亮格子/部件,拼出一幅完整图样(剪纸/织纹/年画式的构图感) | core 网格状态 + hud-ui 绘格 + 点亮判定走 targets occupied |
| **图样拼合** | 按纹样图谱点亮格子/部件,拼出一幅完整图样(剪纸/织纹/年画式的构图感) | core 网格状态 + hud-ui 绘格 + 单击命中点亮判定 |
| **节律配合** | 跟着节拍点(舞龙鼓点/号子),命中节拍窗口得分、连中叠倍 | timer-scheduler 节拍源 + gamefeel ComboWindow + audio-music |
| **工坊经营** | 接订单→按工序制作→交付收钱(非遗 × 经营的复合;经营范式见 sim-business §1) | 工序节拍循环 + 订单表;别超 2 主机制 |
| **收集图鉴** | 每件成品入册,凑齐一套解锁下一套 | save-progress 存收集 + hud-ui 图鉴格 |
@ -57,7 +57,7 @@ description: "当为非遗/传统技艺题材 brief 产设计阶段玩法方案
## 3. 数值与节奏(节律张力是本品类的手感命脉)
- **双层奖励(与自动验收解耦,工程现实)**:**基础分**——每完成一道工序都加分(盲点也能推进,保九门驱动器可过门);**技巧分**——火候窗口内的「正窗」时刻(窗口前 40%)出手给暴击倍率、连续正窗叠 combo。真人靠技巧分玩出高分,驱动器拿基础分过门,两不耽误。
- **双层奖励(基础反馈保上手 + 技巧分给深度)**:**基础分**——每完成一道工序都加分(随手点也有正反馈,新手不至于无所适从);**技巧分**——火候窗口内的「正窗」时刻(窗口前 40%)出手给暴击倍率、连续正窗叠 combo。新手随便玩也有反馈、熟手靠技巧分玩出高分,两不耽误。
- **工序节奏目标值(design 必给具体区间,别让 code agent 拍脑袋)**:工序就绪间隔 **~0.61.0s**(太慢 = 干等,太快 = 手忙);火候窗口 **~58s**(过窗回炉,不判死);回炉冷却 **~0.81.2s**;一局 **60s**,期望成品 **24 件**(工序链 46 道);后 20s 可把窗口收紧到 **~3.55s** 做赶工段。这些目标值交 core.js 参数区。
- **工钱曲线**:后道工序分值高于前道(越接近成品越值钱,弃单代价自然变大);成品奖励 ≈ 单道工序分的 35 倍;解锁阈值取「23 件成品的工钱」,制造"差一件就解锁"的钩子。
- **代价设计**:错过窗口 = 回炉重做该道(损时间不清进度),**不做「一错全毁」**——挫败 = 流失;紧张感来自时间损失与 combo 断裂,不来自惩罚清零。
@ -132,7 +132,7 @@ description: "当为非遗/传统技艺题材 brief 产设计阶段玩法方案
- ❌ 3D 拉坯 / 自由手势雕刻(连续轨迹判定)→ 2D 点击 + 程序化动效;手感靠窗口时机与 juice,不靠手势精度。
- ❌ 知识问答墙 / 长文化说明打断玩法 → 文化经工序名/器物名/氛围体感传达,玩法优先。
- ❌ 超过 2 个主机制 → 聚焦 1 条主循环 + 1 成长轴。
- **核心动作做成「先选工具再选工位」两步**:零-LLM 自动验收 driver 只会单击 `occupied:true` 目标、跟不动两步 → 九门 H 验不到进展 = 好游戏被误判 → ✅ **核心动作单击即完成当前工序**(就绪工位 occupied:true,点它 = 该道完成);确需多步,则 `_forensicsView().state().targets` 必须随状态把"下一步该点的"标 occupied:true,让 driver 跟着推进
- **核心动作优先单击直给**(点就绪工位 = 该道工序完成):「先选工具再选工位」两步流程抬高上手门槛、拖慢节拍手感——本品类的张力在火候窗口时机,不在选择流程;确需多步,给清晰视觉指引(选中态高亮 + 下一步该点的工位有 affordance),让真人一眼看懂下一步点哪
- ❌ **文化表述红线**:工序、器物、民俗意象用**通识**表述(制陶有塑形上釉、织造有经纬提花这一层常识粒度);**不编造具体非遗项目的失实细节**——不虚构某项目的历史年代/传承人/名录地位/地域归属,不使用"官方认证""名录在册"这类可能失实的措辞;brief 点名了具体项目时,只用其公知常识、拿不准的细节一律泛化成通识写法。
- ✅ 留下的:工序节拍、图样拼合、节律配合、收集图鉴、技艺解锁、轻工坊经营 —— 全在 11 注入插件 + 单屏 + 便宜模型可生成范围内。

View File

@ -19,7 +19,7 @@ description: "当生成 agent 要写一款可上线 LittleJS 小游戏的 game-l
## 0. 三句话心法
1. **你只写「玩法身份(WHAT)」,通用机制(HOW)一律调插件——能调就别自己实现。** 写碰撞数学/粒子/补间/计分/状态机 = 错;这些是插件。
2. **你的唯一产出靶 = `game-logic.js` 的命名导出 `createGame({plugins,bundle,viewport})`**,返回 GameInstance:`init/update/render/destroy` + 输入方法 `handleTap(x,y)`[/`handleKey(key)`] + `_forensicsView()`。**零引擎 import**。
2. **你的唯一产出靶 = `game-logic.js` 的命名导出 `createGame({plugins,bundle,viewport})`**,返回 GameInstance:`init/update/render/destroy` + 输入方法 `handleTap(x,y)`[/`handleKey(key)`],另可挂可选调试接口 `_forensicsView()`。**零引擎 import**。
3. **受控面铁律**:游戏里 **绝不出现** `Date.now` / `performance.now` / `Math.random` / `setTimeout` / `requestAnimationFrame` / `new AudioContext` / `import 'littlejsengine'`。时间/随机/音频/引擎能力一律经 `boot.ctx` 或注入的插件拿;**输入不调 `ctx.getInput`**——写实例方法 `handleTap(x,y)`/`handleKey(key)`,L1 据此在 pointerdown/keydown 时派发(确定性、可取证、引擎可换)。
---
@ -114,7 +114,7 @@ export function createGame({ plugins, bundle, viewport }) {
- **输入收归 L1(别自己订阅)**:不写 `ctx.getInput()`、不自建点击队列、不在 `update` 里挑时机消费点击——只写实例方法 **`handleTap(x,y)`**(点击)/**`handleKey(key)`**(键盘),L1 的 game.js wrapper 在 pointerdown/keydown 时**直达**调用。HJ-AGI 实测:M3 把点击塞 `pendingClicks`、在 `update` 的 play 分支(menu early-return 之后)才消费 → 菜单点击永不处理 → 启动不了;收归 L1 结构性根除。check 会拦 `getInput` 与"零输入方法"。〕
- **⚠️ 驱动插件 onFrame**:`update(dt)` 内**必须 `bundle.tick(dt)`**(`bundle` 是 createGame 入参)——否则 timer-scheduler/gamefeel 等的 onFrame 不推进、timer 永不到期、一局不结束。
- **viewport**:画面口径读入参 `viewport.w/h`,别硬编码 390×844。
- **取证面(`_forensicsView()` 必须实现,可测性红线)**:实例必须挂 `_forensicsView()``{ state(), measures() }`;`state()` 导出可观测态——至少 `{ phase, score }`,有可点目标则加 `targets:[{x,y,occupied}]`(九门据此验「真活+有进展」+ 驱动)。**targets 要反映「当前该点什么才推进」**:多步动作(如选客→选货)在选中后,把第二步目标(货架商品)也标 `occupied:true` —— 否则零-LLM driver 单击跟不动两步、H 门验不到进展(实测 wanglanmei-v4:两步成单 → driver 只选客不成单 → score 0 误判)。**最稳:核心成单做成单击即完成**。**不写则 check 拦 + 九门 E_live/H_progress 读不到状态**。另可选挂 `_handleAction(action)`(host.do() 走逻辑驱动,便于确定性 e2e)。范式见 `_template/src/game-logic.js`
- **调试视图(`_forensicsView()`,可选)**:挂 `_forensicsView()``{ state(), measures() }`,`state()` 导出可观测态(如 `{ phase, score }`)供人工调试观测——W-AXIS-V2 起验收不再读它(验收 = 四门机械投影 ∧ 测试 agent 视觉引导真玩),写不写都不影响过门;保留它是给调试与日志一个统一出口。另可选挂 `_handleAction(action)`(host.do() 走逻辑驱动,便于确定性 e2e)。范式见 `_template/src/game-logic.js`
- **关键事件记日志(`ctx.log`)**:在 init/场景切换/得分/出错处 调 `ctx.log('tag', 信息)`(`ctx`=init(boot).boot.ctx)→ 写 `window.__gameLog`,便于读日志诊断运行时问题。**插件调用已由 host 自动记**(你只补游戏语义事件)。
---
@ -158,7 +158,7 @@ export function createGame({ plugins, bundle, viewport }) {
- ✅ **起局/交互按钮的命中矩形在场景 `onEnter`(状态进入点)确定性建立、`render` 只画不写**——绝不让它只在 `render` 副作用里生成(`menuStartBtn = renderMenu(...)` 那种回写):并发渲染帧饥饿时 `render` 可能一次都没跑过 → 命中矩形恒 `null` → 菜单点不动、一局起不来(卡 menu 头号成因)。把按钮几何抽成纯函数,`onEnter` 调它设命中区、`render` 调它画,命中与绘制同源不漂移(`_template*` 六款范例的 `menuStartRect/overRestartRect` 即此写法)。
- ❌ scene 的 update 回调臆造首参写成 `update(running, dt)` → ✅ **scene 的 `update` 回调只收一个参数 `(dt)`**(`sceneFsm.define(name, { update(dt){…} })`;`sceneFsm.update(dt)` 只透传 dt,没有 running 之类首参)。写错首参 → 真正的 dt 落到第二参=undefined → `state.elapsedMs += undefined = NaN` → 不再 spawn/计时、计时显示「NaNs」、游戏空转看着像没 bug 实则永不推进(实测 wanglanmei-v2:菜单能进 play 但无顾客、营业额恒 0)。**任何回调签名都以 `src/plugins/<name>/api.d.ts` 为准,别臆造参数。**
- ❌ `update(dt)` 把 dt 当毫秒直接累加 `elapsedMs += dt` → ✅ **dt 单位是「秒」(≈1/60)**;游戏内毫秒计时(倒计时/spawn 间隔/耐心,常量如 60000/1300/4800)累加要 **`elapsedMs += dt * 1000`**。漏 ×1000 → 计时慢 1000 倍 → 永不到 spawn 阈值=零顾客、一局永不结束(实测 wanglanmei-v2 翻车点;`_template` 范例里 `remainMs - dt*1000` 就是这个换算)。
- ❌ `_forensicsView()` 在**外层**先算好 phase/targets 再闭包返回(`const phase=...; return { state(){ return { phase } } }`)→ ✅ **phase/targets/score 必须在 `state()` 函数体内部实时计算**(host 只在 boot 调一次 `_forensicsView()`,外层算的值定格成 boot 快照=永远 menu、targets 永远空 → 自动验收/驱动看不到目标、永判死菜单。实测 wanglanmei-v2:游戏真在 play 但门读到的 phase 恒 menu)。照 `_template/src/game-logic.js``state: () => ({ phase: sceneFsm.current(), ... })` 写。
- ❌ `_forensicsView()`(若写)在**外层**先算好 phase 再闭包返回(`const phase=...; return { state(){ return { phase } } }`)→ ✅ **导出值在 `state()` 函数体内部实时计算**(host 只在 boot 调一次 `_forensicsView()`,外层算的值定格成 boot 快照——调试时读到的恒是旧值,实测 wanglanmei-v2:游戏真在 play 但读到的 phase 恒 menu)。照 `_template/src/game-logic.js``state: () => ({ phase: sceneFsm.current(), ... })` 写。
- ❌ 在 game-logic.js 取受控面用 `opts.ctx` / `opts.boot` → ✅ 受控面在**实例 `init(boot)``boot.ctx`**(`boot.ctx.time` / `boot.ctx.random` / `boot.ctx.getEngine()` / `boot.ctx.log()`);**输入不经 `boot.ctx.getInput`——写实例方法 `handleTap(x,y)`/`handleKey(key)`,L1 派发**;`createGame` 入参是扁平 `{plugins,bundle,viewport}`,**没有 ctx**。
- ❌ 修 bug 时图省事写 `Math.random()` / `Date.now()` / `setTimeout()` → ✅ `boot.ctx.random()``boot.ctx.random.next()` / `boot.ctx.random.range(a,b)``boot.ctx.time()``boot.ctx.time.nowMs()``timer-scheduler.after(ms,cb)`(#1:time/random 既可直接调用又保留方法,两形态都对;红线 check 必拦裸 Math.random/Date.now;**越改越要守,thrash 时尤其别引入**)。
- ✅ `sessionScore.add(n)` = `addScore(n)` 别名(#6 阶段一B:直觉名直接可用,两者等价);**其它方法名拿不准一律先 `read_file` 对应 api.d.ts**。

View File

@ -87,7 +87,7 @@ description: "当为剧情互动/文字冒险/恋爱剧情类 brief 产设计阶
剧情游戏 UI 套路(都用 hud-ui 落):
- **文本卡**:羊皮纸/卡片底 + 描边,正文逐字换行(中文无空格,按字宽断行);
- **选项按钮**:纵排 2~3 个、宽 ~80% 视口、高 ≥48(手指友好);**命中区=绘制区=取证 targets 三者同源**(经 core.layoutChoices 一处算,见 §9);
- **选项按钮**:纵排 2~3 个、宽 ~80% 视口、高 ≥48(手指友好);**命中区=绘制区同源**(选项几何经 core.layoutChoices 一处算、onEnter 设命中区、render 只画——两处各算一份迟早漂移,玩家点中画出来的按钮却没反应);
- **属性 HUD**:顶部一行"勇气 2 机智 1",涨的瞬间配粒子(particles-juice);
- **幕数进度**:右上角「第 N 幕」——章节推进可见(替代成长轴的可见性);
- **结局结算屏**:结局**称号**大字(「果敢信使」)+ 结局正文卡 + 属性回顾 + **图鉴行(已解锁亮称号、未解锁 ???)** + 再读一次;
@ -139,9 +139,8 @@ description: "当为剧情互动/文字冒险/恋爱剧情类 brief 产设计阶
- ❌ 存档树 / 回溯任意节点 UI → 一局一路走到结局,重玩靠「再读一次」+ 图鉴钩子。
- ❌ 自由输入对话(打字回复)→ 全部预设选项(2~3 个)。
- ❌ 计时逼选 / QTE 限时选项 → 剧情类零时间压力(它的爽点是"想清楚再选");要紧张感用文案不用倒计时。
- ❌ **选项做成「先选中再确认」两步**:零-LLM 自动验收 driver 只会单击 `occupied:true` 目标、跟不动两步 → 九门 H 验不到进展 = **好游戏被误判** → ✅ **选项单击即推进**;**深度靠分支/检定/图鉴,不靠交互步数**。
- ❌ 结局后自动跳回菜单/自动重开 → **结局必须 latch**(phase 驻留 over,取证 targets 清空),「再读一次」只在玩家点按钮时触发。
- ✅ **取证契约(九门可达,钉死)**:`_forensicsView().state()``targets` 在 play 态列**当前每个选项**(中心坐标 `occupied:true`)、menu/over 态空数组;`score` = 已做选择数,随推进单调 +1;**选项命中区=绘制区=取证 targets 必须同源一处算**(_template-story 的 `core.layoutChoices` 即此;三者一旦分叉,driver 点的坐标就不再是真命中区)。场景名钉死 `menu`/`play`/`over`
- ❌ **选项做成「先选中再确认」两步** → ✅ **选项单击即推进**(点了就走,读者的手不该被流程绊住);**深度靠分支/检定/图鉴,不靠交互步数**。
- ❌ 结局后自动跳回菜单/自动重开 → **结局必须 latch**(phase 驻留 over,结局卡停在屏上给玩家看够),「再读一次」只在玩家点按钮时触发。
- ✅ 留下的:分支选择、属性累积/检定、线索、多结局图鉴、章节推进、好感路线 —— 全在 11 插件 + 单屏 + 便宜模型可生成范围内。
---

View File

@ -9,7 +9,7 @@ description: "当为解谜/找茬/消除/记忆类 brief 产设计阶段玩法
>
> **铁律(创始人 2026-06-21)**:"好玩 = 上游设计层职责",不在 runtime/插件。插件只让设计**能被 juice 出手感**;**这份 skill 负责让设计本身好玩**。
>
> **可达性边界**:产出必须落得到 A-model 轻量运行时——**LittleJS 2D 手机竖屏(390×844)、单/少场景、便宜模型可生成的多文件代码、11 注入插件能力域**。解谜品类还有一条**品类特有的可达红线:谜题的"解"必须能被零-LLM 自动验收 driver 点出来**(§9,设计再妙、driver 解不动就会被九门误判)。
> **可达性边界**:产出必须落得到 A-model 轻量运行时——**LittleJS 2D 手机竖屏(390×844)、单/少场景、便宜模型可生成的多文件代码、11 注入插件能力域**。解谜品类还有一条**品类特有的可达红线:谜题的"解"必须在屏上肉眼可辨**(§9,设计再妙、玩家看不出哪里可点就是卡死)。
>
> **上位标准**:分层质量与验收口径 = [`docs/architecture/架构/生成引擎/游戏质量与爆火能力.md`](../../docs/architecture/架构/生成引擎/游戏质量与爆火能力.md)(L1 机制可玩/L2 内容丰富/L3 留存结构/L4 传播钩子;品类 rubric 计分制见其 §4)。本品类 rubric 机器版(喂 LLM 验证 agent)= `cheap-worker/fixtures/genre-rubrics/puzzle.json`,与本文 §10 品类条目**同源**——改任一处必须同改另一处并做金标复验。
>
@ -39,7 +39,7 @@ description: "当为解谜/找茬/消除/记忆类 brief 产设计阶段玩法
| **规则匹配谜盘**(主流首选) | 每关给一条线索(如"找出所有【红宝石】"),盘面若干格,点中所有符合的即过关;规则随关卡换维度(颜色→形状→数量→组合) | core 纯逻辑产盘面+判定 + hud-ui 线索横幅/格子 + session-score 计分 + particles-juice 点中爆点 |
| **找不同 / 找目标** | 一堆干扰项里藏 N 个目标(不同色/缺一角/多一笔),全找到过关 | core 产干扰+目标 + render 细微差异绘制 + juice 找到闪光 |
| **顺序推理** | 按推理出的次序点(从小到大/按亮灯顺序/按字序),点错断连击 | core 产序列+当前该点项 + hud-ui 高亮 + timer-scheduler 节奏 |
| **记忆翻牌配对** | 短暂展示后盖住,凭记忆点出成对目标 | core 记牌面 + timer-scheduler 展示倒计时;**两步操作须守 §9 的 occupied 跟随纪律** |
| **记忆翻牌配对** | 短暂展示后盖住,凭记忆点出成对目标 | core 记牌面 + timer-scheduler 展示倒计时;**两步操作须守 §9 的下一步指引纪律** |
| **归类整理** | 把混在一起的东西按规则归拢(点出所有"属于厨房的") | 同规则匹配谜盘,规则换成类别归属 |
| **消除变体(点选式)** | 点走相邻同色/凑三消除,清空过关 | core 盘面+消除判定;**只用点选、不做拖拽滑动**(§9) |
| **星级 / 收集图鉴** | 每关按用时/步数评 13 星,攒星解锁新章节/新主题 | session-score(best 接 save)+ save-progress 存星 + hud-ui 图鉴格 |
@ -65,9 +65,9 @@ description: "当为解谜/找茬/消除/记忆类 brief 产设计阶段玩法
- **盘面与解数**:起步 **3×3 格、每关 35 个正确目标**;上限 4×4(竖屏手指友好,格子 ≥72px);目标太少无清屏感、太多变体力活。
- **观察窗口**:出题后留 **0.81.5s** 让玩家扫一眼盘面再判定(别一出题就算超时);记忆类展示 **1.52.5s** 再盖住。
- **一关时长**:**820s/关**为舒适区;整局限时 6090s 或 812 关到终局,两种收口都可(限时更适合自动验收,见 §9)
- **一关时长**:**820s/关**为舒适区;整局限时 6090s 或 812 关到终局,两种收口都可。
- **计分骨架**:每个正确目标基分(如 +10)+ **连击倍率**(2s 内连续正确 ×1.5/×2,断连归 1)+ 过关奖励(+30 起、随关数上浮 ×1.15/关)+ 剩时奖励。让"聪明又快"肉眼变成大分差。
- **错点惩罚(轻)**:点错 = 抖屏 + 断连击 + 短锁定 **0.30.5s**;**绝不倒扣到看不见涨分**(分数总体must上升,挫败=流失;真玩验收也断言 score 递增)。
- **错点惩罚(轻)**:点错 = 抖屏 + 断连击 + 短锁定 **0.30.5s**;**绝不倒扣到看不见涨分**(分数总体must上升,挫败=流失)。
- **双货币(变现预留)**:星星(过关挣,解锁章节)+ 提示券(稀缺,卡壳消耗,**经看广告或攒**)。看广告点位:卡壳要提示 · 失败续 10 秒 · 结算翻倍。包装成"福利",不打断顿悟爽点。
---
@ -128,7 +128,7 @@ description: "当为解谜/找茬/消除/记忆类 brief 产设计阶段玩法
7. **UI 布局**(线索横幅 / HUD / 连击飘字 / 结算面板,§5);
8. **插件选择清单**(常用:sceneFsm + sessionScore + hudUi + timerScheduler + audioMusic + juice,记忆类加 save 存最佳);
9. **首 3 分钟脚本**(逐关:示范关→舒适关→首个顿悟关→内容预告);
10. **取证契约声明**(§9:targets 怎么列、occupied 怎么标、score 怎么涨——设计阶段就写清,别等实现时才想)。
10. **可点性可见声明**(§9:当前该点的目标靠什么视觉特征与干扰项区分、多步范式下一步怎么指引——设计阶段就写清,别等实现时才想)。
> **MVP-first 铁律(关乎 code agent 能否收敛)**:首版**只做可玩核心**——1 个谜题主范式 + 35 关阶梯 + 基分/连击 + 基础音效,谜面元素 ≤3 种属性维度。**每日谜题 / 图鉴 / 称号系统 / 无尽模式 / 提示券商店 一律标「后续·MVP 不做」**,别一稿堆满 —— 设计太满 → code agent 实现负担过重 → read-thrash 不收敛。**先出能玩的核心,再谈丰富。**
@ -138,16 +138,16 @@ description: "当为解谜/找茬/消除/记忆类 brief 产设计阶段玩法
## 9. 可达性红线(落不到运行时的设计 = 废稿,必剔除/简化)
- ❌ 拖拽 / 滑动 / 连线 / 画线为核心操作(华容道滑块、连连看划线、一笔画)→ 零-LLM 自动验收 driver **只会单击**,拖不动 → 改成**点选等价形态**(点两格交换、点格子步进、点序列下一项)。
- ❌ 拖拽 / 滑动 / 连线 / 画线为核心操作(华容道滑块、连连看划线、一笔画)→ 竖屏小格上拖拽误操作率高、单击是手机 casual 最稳的输入形态 → 改成**点选等价形态**(点两格交换、点格子步进、点序列下一项)。
- ❌ 大段文字谜面 / 灯谜 / 阅读理解 → 竖屏小字读不动、机器也验不了;线索用**视觉属性**(颜色/形状/数量/位置)表达,横幅一行字封顶。
- ❌ 多屏关卡地图 / 大世界章节 → 单/少场景(menu→play→over),章节感用主题轮换与图鉴表达。
- ❌ 复杂谜题编辑器 / 用户出题 → 超出便宜档;盘面一律 core 纯逻辑程序化生成(受控随机,可复现)。
- ❌ 超过 2 个主机制(谜盘+消除+记忆混一款)→ 聚焦 1 主范式 + 1 成长点缀。
- ❌ **「解」不在取证面上**——这是解谜品类的头号验收翻车点:**`_forensicsView().state().targets` 必须兼职"求解器"——当前**该点的正确目标**标 `occupied:true`、其余(干扰项/已清/锁定中)标 `false`**;自动验收只点 `occupied:true` 的格子,解谜逻辑再对、occupied 标错 = 九门 H 验不到进展 = 好游戏被误判。多步范式(翻牌配对:先翻 A 再翻 B)必须**随状态把"下一步该点的"跟标 occupied**(翻开 A 后,把 A 的配对牌标 occupied);做不到就改单步范式。
- ❌ **重写盘面生成器而丢「可解性质」**——解谜品类第二号翻车点(批产实测:自写符文生成器产出 0 解盘面 → 第一关就无解 → 永无 occupied 目标 → 九门 H 挂):脚手架的 `generateBoard` 保证**每关恰好 N 个解、至少 1 个解、干扰项绝不撞规则**;换主题/换规则维度**优先只改 THEME/LEVELS 表**、别动生成器;确要自写规则系统,必须保住同一性质(生成后自检一遍「符合规则的格数 == 预期解数、≥1」,0 解=不可解关=废稿)。
- ❌ **「解」肉眼不可辨**——这是解谜品类的头号翻车点:**当前该点的正确目标必须有可见的视觉区分**(线索横幅指明规则 + 目标属性肉眼可判——颜色/形状/数量差异要够大),解谜逻辑再对、屏上看不出哪里该点 = 玩家与测试员一样卡死。多步范式(翻牌配对:先翻 A 再翻 B)必须**随状态给"下一步该点的"视觉指引**(翻开 A 后,提示区亮出 A 的花色);做不到就改单步范式。
- ❌ **重写盘面生成器而丢「可解性质」**——解谜品类第二号翻车点(批产实测:自写符文生成器产出 0 解盘面 → 第一关就无解 → 谁玩都卡死):脚手架的 `generateBoard` 保证**每关恰好 N 个解、至少 1 个解、干扰项绝不撞规则**;换主题/换规则维度**优先只改 THEME/LEVELS 表**、别动生成器;确要自写规则系统,必须保住同一性质(生成后自检一遍「符合规则的格数 == 预期解数、≥1」,0 解=不可解关=废稿)。
- ❌ **在场景 render 回调里引用未定义变量**——render 回调抛错会被 scene-fsm **隔离成告警不崩**,画面异常但局不死(批产实测:renderMenu 里一个未定义的 `cy` 曾让整局 E/G/H 三门连锁挂——当时按钮命中区还靠 render 返回值回写 `menuStartBtn = renderMenu(...)`,render 一抛错命中区永远拿不到、卡死菜单)。2026-07-03 起模板已改约定:**命中矩形在场景 onEnter 用纯几何函数确定性建立,render 只画不赋值**(cheap-system v1.6.1 硬约束),旧连锁已断;但 render 抛错仍会黑屏丢反馈,改完 render 先顺一遍所有变量来源。
- ❌ 点错即死 / 三错清零重开 → 错点轻惩罚(§3),乱点不该赢、但点错不该死。
- ✅ 留下的:规则匹配谜盘、找目标、顺序推理、点选消除、记忆配对(守 occupied 纪律)、星级收集、关卡阶梯、连击计分 —— 全在 11 注入插件 + 单屏 + 便宜模型可生成范围内。
- ✅ 留下的:规则匹配谜盘、找目标、顺序推理、点选消除、记忆配对(守下一步指引纪律)、星级收集、关卡阶梯、连击计分 —— 全在 11 注入插件 + 单屏 + 便宜模型可生成范围内。
---
@ -194,8 +194,8 @@ description: "当为解谜/找茬/消除/记忆类 brief 产设计阶段玩法
## 相关
- 代码层实现 → [`littlejs-game-dev.md`](littlejs-game-dev.md)(11 注入插件 API / 结构 / 资产-mmx / 工厂契约)
- 品类轻脚手架(克隆起点)→ `game-runtime/games/_template-puzzle/`(规则匹配谜盘骨架:盘面生成/线索/判定/关卡流转已接好,取证契约齐)
- 品类轻脚手架(克隆起点)→ `game-runtime/games/_template-puzzle/`(规则匹配谜盘骨架:盘面生成/线索/判定/关卡流转已接好)
- 过门正例(few-shot,读它看"丰富成什么样")→ `game-runtime/games/_fewshot-puzzle/src/`(在骨架上做出规则递进+连击+星级+结算炫耀的完整款)
- 品类 rubric 机器版(喂 LLM 验证 agent)→ `cheap-worker/fixtures/genre-rubrics/puzzle.json`;金标 play-spec → `cheap-worker/fixtures/golden-specs/puzzle.play-spec.json`
- 品类 rubric 机器版(喂 LLM 验证 agent)→ `cheap-worker/fixtures/genre-rubrics/puzzle.json`
- 便宜模型生成 worker / 九门 → [`cheap-model-game-generation.md`](cheap-model-game-generation.md)
- 质量分层与档位观测线(上位标准)→ [`docs/architecture/架构/生成引擎/游戏质量与爆火能力.md`](../../docs/architecture/架构/生成引擎/游戏质量与爆火能力.md)

View File

@ -32,5 +32,5 @@ render(g) { sceneFsm.render(g); }
**别踩**
- 回调表里 `update(dt)` 只收 dt、`render(g)` 只收 g——与五法同形别臆造 `update(scene, dt)`
- `_forensicsView().state().phase` 必须**实时读** `sceneFsm.current()`(在 state() 函数体内别在外层先算好闭包返回host 只在 boot 调一次 _forensicsView外层算的值会定格成 boot 快照=永远 menu → 自动验收永判死菜单)。
- 若写了可选调试接口 `_forensicsView()`:`state().phase`**实时读** `sceneFsm.current()`(在 state() 函数体内别在外层先算好闭包返回host 只在 boot 调一次 _forensicsView外层算的值会定格成 boot 快照=调试读到的恒是 menu 旧值)。
- 状态名你自己定menu/play/over 只是惯例);`transition(to)` 的 to 必须是你 define 过的名。

View File

@ -44,4 +44,4 @@ render(g) {
**别踩**
- 音效名用语义名(`coin/win/hit/jump/fail/click`)或预设(`blip/thud/chime`),未知名自动兜底 blip`audioMusic.play(intensity)` 入参是**情绪强度 0..1**、不是曲目名。
- `juice.spawnEmitter('burst', x, y)` 也可写 `juice.burst(x, y)`(母语别名);`hitStop`/`shakeScreen` 收毫秒/像素。
- 结算`_forensicsView().state().phase` 要能读到 `gameover`(自动验收据 latch 驻留判终态可达)
- 结算屏必须驻留(latch):到终态后停在结算不自动重开——玩家要看清战绩,测试员与调试观测也才能确认终态可达

View File

@ -148,7 +148,7 @@ description: "当为经营模拟/放置养成类 brief 产设计阶段玩法方
- ❌ 3D / 大量逐帧手绘动画 → 2D + 程序化动效(particles-juice/缓动)+ 少量 mmx 静态图。
- ❌ 复杂经济(多资源链/市场波动)→ 双货币 + 简单成长曲线。
- ❌ 超过 2 个主机制 → 便宜模型 + 单屏扛不住,聚焦 1 主 + 1 成长轴。
- **核心成单/得分动作做成「先选 A 再选 B」两步**(如"点顾客锁定→再点货架商品"):零-LLM 自动验收 driver 只会单击 `occupied:true` 目标、跟不动两步 → 九门 H 验不到进展 = **好游戏被误判** → ✅ **核心动作单击即成单**(点等待中的顾客 = 自动按库存售卖收钱,casual 友好 + driver 可达);**深度靠进货/库存/解锁/节奏,不靠两步匹配**。若确需多步,则 `_forensicsView().state().targets` 必须**随状态把"下一步该点的"标 occupied:true**(选中顾客后,把该卖的货架商品标 occupied),让 driver 跟着两步推进
- **核心成单/得分动作优先单击直给**(如"点等待中的顾客 = 自动按库存售卖收钱"):手机竖屏 casual 玩家一根手指就该玩得转,「先选 A 再选 B」两步匹配抬高上手门槛;**深度靠进货/库存/解锁/节奏,不靠交互步数**。确需多步(先锁顾客再点货架),须给清晰的视觉指引(选中态高亮 + 下一步该点的目标有 affordance),让真人一眼看懂下一步点哪
- ✅ 留下的:放置产出、合成升级、点击经营、阶段解锁、收集、轻决策、数值滚雪球 —— 全在 11 注入插件 + 单屏 + 便宜模型可生成范围内。
---
@ -181,7 +181,7 @@ description: "当为经营模拟/放置养成类 brief 产设计阶段玩法方
每品类需 ≥1 金标正例 + ≥1 薄反例锚住评分尺(rubric 文本或评分 prompt 变更须金标复验,单款分组小计漂移 >±1 即回退)。经营品类的锚 2026-07-04 落定(生产口径 genre=sim-business,同输入复采样 n=3 取中位,入口 `cheap_verify.verify_richness`):
- **金标正例 = [`cheap-worker/fixtures/golden-samples/sim-business/bake-shop-serve-1`](../../cheap-worker/fixtures/golden-samples/sim-business/bake-shop-serve-1/)**(S5b lab 复验小批产的烘焙店 60 秒上菜 rush:三桌耐心条、700ms 连击窗叠技巧分、营收三档阈值 22/55/100 渐次解锁菜单;attempts=1 一次过九门、真玩 driver=tap-targets、¥2.00)。**通用 12 条 n=3 中位 = 10/12**(单发 11/9/10,方差 ±2 合纪律;稳定缺项只有「放置回归」——60 秒单局制无离线收益,形态使然)。第 12 条「核心操作非无脑」命中:多桌耐心排序 + 连击维护是真实决策,走客有真代价。
- **金标正例 = [`cheap-worker/fixtures/golden-samples/sim-business/bake-shop-serve-1`](../../cheap-worker/fixtures/golden-samples/sim-business/bake-shop-serve-1/)**(S5b lab 复验小批产的烘焙店 60 秒上菜 rush:三桌耐心条、700ms 连击窗叠技巧分、营收三档阈值 22/55/100 渐次解锁菜单;attempts=1 一次过当时九门、¥2.00;锚定于 W-AXIS-V2 前,当时驱动器口径已随取证契约退役)。**通用 12 条 n=3 中位 = 10/12**(单发 11/9/10,方差 ±2 合纪律;稳定缺项只有「放置回归」——60 秒单局制无离线收益,形态使然)。第 12 条「核心操作非无脑」命中:多桌耐心排序 + 连击维护是真实决策,走客有真代价。
- **薄反例/对照 = `_template-shop` 骨架原样**(C 波 2026-07-03 实测:通用前 11 条 4/11、第 12 条未命中——点顾客即自动上菜结算 = 无脑点击;品类段 1/4)。**正反例通用段分离 56 分,通用尺的判别力锚在这条差距上**;与 TRPG/puzzle 模板(富正例)相反,经营骨架天然薄,薄对照角色恰好由它担、不必再造合成材料。
- **品类扩展段(4 条)诚实口径**:正例本款只中 2/4——serve 形态天然没有进货补货与双货币,这两条的正锚要等进货型经营款出现;品类段正例对骨架对照(2/4 vs 1/4)分离仅 1 分,**回退判定勿跨形态用品类段,以通用段 + L4 为准**。
- **方差纪律**:M3 单发方差 ±2,取中位;锚定凭据(逐条命中理由原始记录)= 正例 `evidence/richness-n3.json`

View File

@ -132,14 +132,14 @@ description: "当为掷骰冒险(TRPG)类 brief 产设计阶段玩法方案或
## 9. 可达性红线(落不到运行时的设计 = 废稿,必剔除/简化)
- ❌ **长流程重游戏**:跨局剧情存档推进 / 章节制大冒险 / 大地图自由探索 → 改单局可完(4590s)+ 跨局只留纪录/图鉴。
- ❌ **回合制菜单战斗**("攻击/防御/道具/逃跑"四键菜单):多步操作 + 状态菜单,零-LLM 自动验收 driver 只会单击 `occupied:true` 目标、跟不动 → ✅ **核心动作单击即结算**(点遭遇卡 = 自动掷骰+判定+结算一步完成)。
- ❌ **回合制菜单战斗**("攻击/防御/道具/逃跑"四键菜单):多步菜单流程拖慢单局节奏、casual 玩家上手门槛高 → ✅ **核心动作单击即结算**(点遭遇卡 = 自动掷骰+判定+结算一步完成,爽点在「挑哪张」不在按菜单)。
- ❌ **纯随机无抉择**(自动掷骰/只有一个按钮):没有"挑哪张"的掌控感 = 抽卡机不是 TRPG → 同屏 ≥2 张差异化亮牌。
- ❌ **掷骰黑箱**(点卡直接出结果不见骰):悬念归零 → 判定式飘字必须有。
- ❌ 联网组队 / PVP → 单机 + 本地最高纪录。
- ❌ 3D 骰子物理 / 大量逐帧动画 → 2D 程序化(飘字+粒子+闪光足够演出)。
- ❌ 超过 2 个主机制(战斗+锻造+钓鱼…)→ 聚焦点卡掷骰 + 1 成长轴。
- ✅ 留下的:掷骰对抗、亮牌取舍、等级/伙伴成长、难度爬坡、连胜、限时、纪录/图鉴 —— 全在 11 注入插件 + 单屏 + 便宜模型可生成范围内。
- **_forensicsView 契约(自动验收可达)**:`targets` 列本层可点遭遇卡(ready 才 `occupied:true`);`score` 用金币战利品且**每次结算至少 +1**(失败安慰金)——score 单调升是 H 门进展契约,断了 = 好游戏被误判
- **每次结算至少 +1 金币(失败安慰金)**:胜负都给正反馈,失败也不颗粒无收——保底反馈是 casual 上手缓冲,别让连败玩家零收获直接弃游
---
@ -156,7 +156,7 @@ description: "当为掷骰冒险(TRPG)类 brief 产设计阶段玩法方案或
5. **[L4] 冒险战报可炫耀**:结算屏讲这局冒险的故事(层数/击败/大成功/胜负因由)、可截图?(正:「爬到第 5 层·击败 7·大成功 2·勇者倒下」;反:只有一个总分数字)
> 命中越多越好玩;通用 12 条命中 ≤3 ≈ 能玩但无趣。**设计阶段就过这两张表,别等做出来才发现无趣。**
> 机检硬门 vs LLM 评分维度的分界照质量模型 SoT §4 规范五:score 递增等真玩断言归 L1 硬门(play-spec assertAfterPlay);上述品类条目全是 LLM 评分维度,**不落项目代码、不进九门、不进脚手架**。
> 机检硬门 vs LLM 评分维度的分界照质量模型 SoT §4 规范五(v2 修订:play-spec 断言载体已随 W-AXIS-V2 退役,机械可判的归四门地板与测试员转写、程度评价归 LLM 评分,分界原则不变);上述品类条目全是 LLM 评分维度,**不落项目代码、不进机械门、不进脚手架**。
---

View File

@ -1,6 +1,11 @@
"""
auto_vs_golden.py M2 auto-vs-golden delta (Node 退役授权第三条 · 按品类 · LLM)
封存(W-AXIS-V2 波2,2026-07-10)对照范式建立在 ensure_play_spec 自动 spec tap-targets 驱动器上;
两者已随取证契约退出便宜档验收链(验收权威 = 四门投影 测试 agent 真玩)其使命Node 退役授权的
自动 spec 不比金标驱得差已随 Node 路退役完结,不改挂测试员(范式不同,无对照意义)保留代码
供历史复跑;生产链与批跑不再调用,play-spec 生成器窗口期结束后随其一并清理
对同一款生成产物,在同一份 staged src/ 上先用生产自动 spec(ensure_play_spec)后用金标 spec
(inject_golden)各驱动一次九门,逐门比过门率方向单边( plan① M2关键门不得低于金标基线):
查的是自动 spec 是否比金标**驱得差**(delta<0 退化),不是查自动比金标松后者对断言同质的

View File

@ -1,6 +1,10 @@
"""
bake_off.py 便宜档 80% 达标门(按品类 · LLM)
封存(W-AXIS-V2 波2,2026-07-10)本工具以 verdict.pass(driven 九门)为达标判据,依赖 ensure_play_spec
自动 spec 驱动;驱动器已随取证契约退出便宜档验收链v2 的达标口径 = accepted(四门投影 测试 agent 真玩),
批量达标测量由 hard_genre_batch / hard_genre_xtheme(已切新口径)承担保留代码供历史复跑;不再作达标门
代表样本(tap-targets occupied 三品类:点击得分/打地鼠/经营点客) n ,每款:
run_studio(run_gates=True) 生成 自动 spec(U2 prefixes)驱动九门 verdict.pass
按品类聚合 pass_count/total,逐品类 0.8 判达标;任一品类缺样本报覆盖不足任一 <0.8 整体未达标

View File

@ -1,7 +1,7 @@
"""cheap_gates.py — 便宜档九门收口的同步包装(供 Service 续修 check 闭包经 to_thread 调用)。
归并后便宜档收敛判据从check+build 绿升为九门绿:RepairMiddleware check 闭包在 finish
把工程收口跑一遍九门(stagesmokeensure_play_specplay), worker.gate_judge.judge_cheap_verdict 归一
把工程收口跑一遍九门(stagesmokeplay(undriven)), worker.gate_judge.judge_cheap_verdict 归一
本模块只做同步 pipeline + 端口透传,不做判据归一(那在 gate_judge, tier2 并列)
端口由调用方按 session 派生传入(决策③ 并发避撞);shell-out/subprocess 阻塞,故封在同步函数里给 to_thread
"""
@ -10,7 +10,7 @@ import cheap_run
def run_cheap_gates(game_id: str, port: int, cdp_port: int) -> dict:
"""同步跑便宜档九门收口 stage→smoke→ensure_play_spec→play,返回九门 verdict({pass, guards})或 {}(未跑出)。
"""同步跑便宜档九门收口 stage→smoke→play(undriven),返回九门 verdict({pass, guards})或 {}(未跑出)。
任一段失败(stage 打包不出 / play 没产 verdict) {}(judge_cheap_verdict 判未过feedback harness 说明,
不伪造门绿)端口按 session 派生传入,避免并发多局撞固定 4320/9222(决策③)
@ -19,10 +19,10 @@ def run_cheap_gates(game_id: str, port: int, cdp_port: int) -> dict:
if not st["ok"]:
print(f"[cheap-gates] game={game_id} stage 失败(判未过):{st['output'][:300]}", flush=True)
return {}
sm = cheap_run.smoke(game_id, port=port, cdp_port=cdp_port)
# ensure_play_spec 据 smoke 抓的 state 产 driver(已存在不覆盖),让九门 driven=true、E_live/H_progress 由 advisory 升致命,
# 根治「裸 harness 无 driver → 假绿」。smoke 失败(state=None)时按保守 key-cycle 薄 spec,play 仍照跑
cheap_run.ensure_play_spec(game_id, sm.get("state"))
# smoke 作 boot 预检(早捕装载失败,诊断更清);其 _forensicsView state 曾供 ensure_play_spec 推断驱动器,
# W-AXIS-V2 波2 拆 tap-targets 驱动器后不再消费——便宜档验收权威改四门投影(A/B/C/D)∧ 测试 agent 真玩,
# 九门以 undriven 硬集(A/B/C/D + F/G/I)跑,floor 投影只取 A/B/C/D。ensure_play_spec 不再调
cheap_run.smoke(game_id, port=port, cdp_port=cdp_port)
# 红线③(封陈旧 verdict 假绿,镜像 tier2 run.py:866):起 play 前清残留 verdict,保证 judge 拿到的恒是本次真门产出。
# serve-and-play.sh 早退路径(chrome/serve/cdp 未就绪)+ 超时都不写新 verdict 也不删旧的,不清则 play 读回上一局绿 verdict → 假绿放行。
(cheap_run.wg1_game_dir(game_id) / "evidence" / "verdict.json").unlink(missing_ok=True)

View File

@ -15,7 +15,7 @@
· materialize_source_project(系统级落盘): base files 直接写到 game_dir**系统恢复已知良源**,
cheap_run.write_file L1_FIXED 守卫(那守卫是防 *agent* 写坏 wiring;这里是系统落盘,只仍校验不越界)
· execute_deterministic_modify(集成):apply scaffold 已知良 plumbing materialize 覆盖改后源
build stage smoke ensure_play_spec 九门 play,**复用 cheap_run 现有九门,不另造**
build stage smoke 九门 play(undriven),**复用 cheap_run 现有九门,不另造**
apply no-op/unsupported 直接 failed( manifest + error),不重建( esbuild/Chrome)
best-effort 铁律:apply 失败 / 重建失败 failed + 诊断,绝不抛崩 worker只改 L3(core/assets/game-logic),
@ -290,7 +290,7 @@ def execute_deterministic_modify(game_id: str, source_project, modify_patch,
"""确定性执行集成:apply 改一处 → 落盘已知良源 → 重建 → 九门。复用 cheap_run 九门,不另造。
流程:apply_deterministic_modify ok:( plumbing scaffold 已知良 entry-bundle/index.html)
materialize 覆盖改后 L3 build(esbuild) stage smoke ensure_play_spec 九门 play
materialize 覆盖改后 L3 build(esbuild) stage smoke 九门 play(undriven)
apply no-op/unsupported 直接 failed( manifest + error),**不重建**( esbuild/Chrome)
任一步失败 failed + 诊断(stage 标到哪步断的),绝不抛崩
@ -328,15 +328,14 @@ def execute_deterministic_modify(game_id: str, source_project, modify_patch,
"stage": "build", "error": "build 失败:" + bd.get("output", ""),
"smokeOk": None, "playOk": None}
# 5. stage → smoke → ensure_play_spec → 循环外九门 play(复用 cheap_studio 收口同一套)。
# 5. stage → smoke → 循环外九门 play(undriven;复用 cheap_studio 收口同一套;ensure_play_spec 波2 已摘)。
st = cheap_run.stage(game_id)
if not st["ok"]:
log(f"stage 失败 game_id={game_id}: {st.get('output', '')[:300]}")
return {"status": "failed", "manifest": manifest, "verdict": None,
"stage": "stage", "error": "stage 失败:" + st.get("output", ""),
"smokeOk": None, "playOk": None}
sm = cheap_run.smoke(game_id, port=port, cdp_port=cdp_port)
cheap_run.ensure_play_spec(game_id, sm.get("state")) # 已存在不覆盖;缺则据 state 形态补 driver(防裸跑假绿)
sm = cheap_run.smoke(game_id, port=port, cdp_port=cdp_port) # boot 预检(smokeOk 供返回);其 state 不再喂 ensure_play_spec
pr = cheap_run.play(game_id, port=port, cdp_port=cdp_port)
verdict = pr.get("verdict")
# W-AXIS-V2 波1:create 与 modify 不允许两套验收标准——确定性改也过统一编排器(四门投影 ∧ 测试员真玩)。

View File

@ -10,9 +10,11 @@ skill / 插件 api.d.ts / 起点范例。L3 context 由 agent 经 read_file 按
2. 完成工具名是 **finish**不是 Node done 便宜档的 done = cheap_toolkit finish 工具
工具内复核 check+build 绿才接受
system prompt 正文与 prompt.mjs 保持语义一致入口契约 createGame({plugins,bundle,viewport}) 五法 + _forensicsView
红线零裸时间随机 DOM / bundle.tick / ctx.time.nowMs() / 扁平注入插件 / 方法名以 api.d.ts 为准
本周收口的 footgunsceneFsm.define 逐场景调真返 void 的绘制/音效 API 别接返回值;drawButton 已改返命中矩形可直接接住判命中输入契约handleTap/handleKey
system prompt 正文要点入口契约 createGame({plugins,bundle,viewport}) 五法红线零裸时间随机 DOM /
bundle.tick / ctx.time.nowMs() / 扁平注入插件 / 方法名以 api.d.ts 为准本周收口的 footgunsceneFsm.define
逐场景调真返 void 的绘制/音效 API 别接返回值;drawButton 已改返命中矩形可直接接住判命中输入契约
handleTap/handleKeyW-AXIS-V2 波2 起不再教 _forensicsView 取证契约自动验收随 tap-targets 驱动器退役,
验收 = 四门投影 测试 agent 视觉引导真玩;模型不再为死契约写代码
生成丰富度设计步U-A1除指向 littlejs-game-dev.md怎么写代码再加一个先设计后写码
经营/养成/放置/点客类先 read sim-business-game-design.md 设计什么才好玩的范式核心循环 + 进货库存资源环
@ -41,7 +43,7 @@ _SYSTEM_PROMPT = """你是 A-model 游戏生成 agent。目标产物 = 一款**
- 我已把克隆起点拷到 G/SCAFFOLD_DESC你的活是据 brief **借插件能力把它改造成一款高质游戏**别把它当"这档就该这么简单"的天花板
- **L1 固定 plumbing 别动write 会被拒**G/index.htmlG/entry-bundle.jsG/src/game.js wrapper**G/src/host-config.js**host 装配已替你注入** 11 件能力插件** + viewport 固定 390×844 + 工厂 wiring你无需装配任何插件
- **你只写 L3 游戏本体****G/src/game-logic.js必写游戏核心** + 按需 core.js纯逻辑/ render.js画面/ balance.js数值/ assets.js资产结构简单可把逻辑/画面集中在 game-logic.js 多系统/多场景再拆 core/render把脚手架起点改造成 brief 要的**高质游戏**多借插件能力做卖相与深度**画面读 viewport.w/h390×844 竖屏**
- **入口契约钉死**game-logic.js 必须 `export function createGame({ plugins, bundle, viewport })` 返回 GameInstance 五法 init(boot)/update(dt)/render(g)/destroy()+必须 _forensicsView**插件已扁平注入 plugins**用到才解构sceneFsm/sessionScore/hudUi/timerScheduler/save/gamefeel/juice/palettePost/audioMusic/collision/physics
- **入口契约钉死**game-logic.js 必须 `export function createGame({ plugins, bundle, viewport })` 返回 GameInstance 五法 init(boot)/update(dt)/render(g)/destroy()**插件已扁平注入 plugins**用到才解构sceneFsm/sessionScore/hudUi/timerScheduler/save/gamefeel/juice/palettePost/audioMusic/collision/physics
先读手册必须别凭记忆猜 API
1. read_file('.agents/skills/littlejs-game-dev.md') code 层作业手册结构/边界/红线/11 注入插件 API 速查
@ -49,10 +51,10 @@ _SYSTEM_PROMPT = """你是 A-model 游戏生成 agent。目标产物 = 一款**
3. 用到某插件就 read_file 它的 api.d.ts game-runtime/src/plugins/scene-fsm/api.d.ts.../session-score/api.d.ts看精确签名
4. 你的起点 G/src/game-logic.js 本身就是**跑通的脚手架范例**五法齐全 + 4 编排插件read_file 照其结构**借插件能力**改造成 brief 的高质游戏最省事在它基础上改多调插件加内容别从零写
5. ** brief 属经营 / 养成 / 放置 / 点客这类攒资源成长解锁品类**read_file('.agents/skills/sim-business-game-design.md') 设计什么才好玩的范式6 心理引擎 / §1 玩法范式含进货补货环+决策模式清单 / §8 设计配方 / §10 反无趣自检littlejs-game-dev 教你代码怎么写sim-business 教你设计什么才好玩配对用先想清好玩再写正确
6. ** brief 属剧情 / 互动叙事 / 文字冒险这类读文本做选择推进剧情品类**read_file('.agents/skills/narrative-game-design.md') 设计什么才好玩的范式6 心理引擎 / §1 范式含分支选择+多结局图鉴 / §3 文本红线 / §8 设计配方 / §9 可达性红线含取证契约 read_file('game-runtime/games/_template-story/src/game-logic.js') 看剧情品类过门范例分支推进 + 结局 latch + 结局图鉴的骨架写法其故事数据在同目录 core.js 顶部参数空间
6. ** brief 属剧情 / 互动叙事 / 文字冒险这类读文本做选择推进剧情品类**read_file('.agents/skills/narrative-game-design.md') 设计什么才好玩的范式6 心理引擎 / §1 范式含分支选择+多结局图鉴 / §3 文本红线 / §8 设计配方 / §9 可达性红线 read_file('game-runtime/games/_template-story/src/game-logic.js') 看剧情品类过门范例分支推进 + 结局 latch + 结局图鉴的骨架写法其故事数据在同目录 core.js 顶部参数空间
7. ** brief TRPG / 掷骰 / 冒险 / 地牢爬塔这类掷骰对抗抉择成长品类**read_file('.agents/skills/trpg-game-design.md') 设计什么才好玩的范式6 心理引擎 / §1 玩法范式含掷骰爬塔 / §3 掷骰概率数值 / §9 红线核心动作单击即结算·掷骰过程可见 / §10 反无趣自检
8. ** brief 属非遗 / 传统技艺 / 民俗这类传统文化玩法化品类**read_file('.agents/skills/heritage-game-design.md') 设计什么才好玩的范式§1 玩法化范式含工序节拍 / §3 双层奖励与工序节奏目标值 / §9 文化表述红线 / §10 反贴皮自检需要完整过门范例再 read_file('game-runtime/games/_fewshot-feiyi/src/game-logic.js')
9. ** brief 属解谜找规律 / 找不同 / 翻牌配对 / 顺序推理 / 点选消除这类观察推理点解品类**read_file('.agents/skills/puzzle-game-design.md') 取解谜设计范式顿悟距离 / 规则递进关卡阶梯 / 卡壳兜底 / 成绩炫耀 / §9 品类可达红线解谜的必须映射到 targets occupied想看丰富成什么样的完整过门正例 read_file('game-runtime/games/_fewshot-puzzle/src/game-logic.js') 参照
9. ** brief 属解谜找规律 / 找不同 / 翻牌配对 / 顺序推理 / 点选消除这类观察推理点解品类**read_file('.agents/skills/puzzle-game-design.md') 取解谜设计范式顿悟距离 / 规则递进关卡阶梯 / 卡壳兜底 / 成绩炫耀 / §9 品类可达红线想看丰富成什么样的完整过门正例 read_file('game-runtime/games/_fewshot-puzzle/src/game-logic.js') 参照
先设计后写码决定这游戏好不好玩关键别跳过
能跑 好玩动手写码前先据 brief **game-logic.js 顶部写一段设计注释块**8 判定句三个空填好 + 核心数值锚 + 的三段难度定下**轻量玩法设计**再实现设计只在心里 = 收尾自查与验收都无从对照
@ -75,7 +77,7 @@ A 读手册1 + 按品类 5/6/7/8/9+ 读你的起点 game-logic.js4
- game-logic.js 必须 **export function createGame({ plugins, bundle, viewport })**命名导出别改名别默认导出**零引擎 import**引擎经 boot.ctx.getEngine()
- 游戏源game-logic / core / render**零裸** Math.random / Date.now / setTimeout / requestAnimationFrame / new AudioContext / addEventListener 时间随机经 boot.ctx定时经 timer-scheduler音效经 plugins.audioMusic**输入不调 ctx.getInput**已收归 L1见下输入契约
- update(dt) **必须调 bundle.tick(dt)**bundle createGame 入参否则插件 onFrame 不推进timer 永不到期一局不结束**dt 单位是1/60**游戏内若用毫秒计时倒计时 / spawn 间隔 / 顾客耐心常量都是 ms 60000/1300累加时务必elapsedMs += dt * 1000 ×1000 计时慢 1000 永不到 spawn 阈值=零顾客一局不结束**timerScheduler.after(ms,cb)/every(ms,cb) 同样收毫秒#73 秒写 after(3000)、别写 after(3);与 dt=秒相反,别混。**
- **入参是扁平的 { plugins, bundle, viewport }没有 opts没有 opts.runtime没有 ctx**L1 已替你摊平受控面 ctx 只在五法 **init(boot) boot.ctx****调用形态**时间 **ctx.time()** ctx.time.nowMs()随机 **ctx.random()** ctx.random.next()/ctx.random.range(a,b)画布 ctx.getContext2d()引擎 ctx.getEngine()日志 ctx.log(tag,msg)**#1ctx.time/ctx.random 既可直接调用ctx.time()=毫秒、ctx.random()=[0,1))、又保留方法(.nowMs()/.elapsedMs()、.next()/.range(a,b)/.reseed())——两形态都对**。**但 nowMs/next 是 time/random 上的方法、不是 ctx 上的——别写 ctx.nowMs()(应 ctx.time() 或 ctx.time.nowMs())。**五法 = init(boot)/update(dt)/render(g)/destroy()+必须 _forensicsView+ **输入方法 handleTap(x,y)**(见下「输入契约」)。
- **入参是扁平的 { plugins, bundle, viewport }没有 opts没有 opts.runtime没有 ctx**L1 已替你摊平受控面 ctx 只在五法 **init(boot) boot.ctx****调用形态**时间 **ctx.time()** ctx.time.nowMs()随机 **ctx.random()** ctx.random.next()/ctx.random.range(a,b)画布 ctx.getContext2d()引擎 ctx.getEngine()日志 ctx.log(tag,msg)**#1ctx.time/ctx.random 既可直接调用ctx.time()=毫秒、ctx.random()=[0,1))、又保留方法(.nowMs()/.elapsedMs()、.next()/.range(a,b)/.reseed())——两形态都对**。**但 nowMs/next 是 time/random 上的方法、不是 ctx 上的——别写 ctx.nowMs()(应 ctx.time() 或 ctx.time.nowMs())。**五法 = init(boot)/update(dt)/render(g)/destroy() + **输入方法 handleTap(x,y)**(见下「输入契约」)。
- **插件扁平注入直接 const { sceneFsm, sessionScore, hudUi, timerScheduler } = plugins用到才解构****绝不写 opts.runtime.plugins**那是 L1 内部的你见不到 写了就 undefined boot reading 'define'游戏只用插件 new register
- **可用插件键 11 已注入用到才取**sceneFsm / sessionScore / hudUi / timerScheduler / save / gamefeel / juice / palettePost / audioMusic / collision / physics
- **插件方法名一律以 api.d.ts 为准别臆测**实测幻觉scene-fsm defineScenes define用到某插件先 read_file 它的 api.d.tsskill 实测易犯的幻觉 API逐条列
@ -83,7 +85,6 @@ A 读手册1 + 按品类 5/6/7/8/9+ 读你的起点 game-logic.js4
- **sceneFsm.define(name, handlers) 逐场景调一次绝不单对象批量注册** sceneFsm.define({ menu: {...}, play: {...}, over: {...} }) 第一参当成了场景名[object Object]menu/play/over 全没真注册 后续 transition('play') 找不到目标态被忽略 卡菜单实测合成/2048 类翻车点 **每个场景各调一次**sceneFsm.define('menu', { onEnter(){...} }); sceneFsm.define('play', { onEnter(){...}, update(dt){...} }); sceneFsm.define('over', { onEnter(){...} });define 第一参恒为场景名字符串第二参才是回调表 { onEnter, onExit, update, render }**回调表里 update 只收 (dt)render 只收 (g)与五法同形别臆造场景名/state 首参绝不写 update(scene, dt)**可链式 .define(...).define(...)
- **画面读 viewport.w/h**viewport 是入参别硬编码 390×844
- **美术资产 assets/manifest.json 非空** read_file('assets/manifest.json') 看有哪些资产init(boot) const assets = boot.assets || {}render const img = assets['ref名']?.image; if (img) g.drawImage(img, x, y, w, h); else 程序化回退**ref = manifest 条目 file 去扩展名**shopkeeper-idle.jpg 'shopkeeper-idle'缺图/加载失败时 assets[ref].image nullhost 已容错 务必走程序化回退别崩BGM/音效经 plugins.audioMusic不在 boot.assets manifest/无美术 全程序化绘制即可
- **必须实现 _forensicsView()**可测性红线 + 自动驱动契约返回 { state(){}, measures(){} }state() 至少 { phase, score }**若游戏靠点击推进经营点客/打地鼠/点离散目标等state().targets:[{x,y,occupied}] 列当前屏上目标可点的等待的客/冒头的鼠 occupied:true不可点置 false** 自动验收据此点 occupied:true 的目标真驱动你的游戏**键名固定为 `targets`不得改成 items/objects **判卷驱动器靠 state 有无 `targets` 键选驱动器族点目标族 vs 循环按键族改名=选错族=盲驱动器驱动不了你的游戏=H 进展门挂**score 用累计进展营收/得分随推进上升** 自动验收据 score 上升判"真有进展"不写则验证读不到状态check 会拦**钉死phase/targets/score 必须在 state() 函数体内部实时读取/计算绝不在 _forensicsView() 外层先算好再闭包返回**host 只在 boot 调一次 _forensicsView外层算的值会被定格成 boot 快照=永远 menutargets 永远空 自动验收看不到顾客永判死菜单
- **关键事件用 ctx.log 记日志** init/场景切换/得分/出错处 ctx.log('tag', 信息)**ctx 来自 init 的入参 bootconst ctx = boot.ctx绝不是 boot.boot.ctx**host 传入的 boot 已是 {ctx, mainContext, canvas, seed, assets}多套一层 .boot=undefined ctx=null 无输入游戏点不动check 会拦 .boot.ctx插件调用已自动记你只补游戏语义事件
- **输入契约钉死·根治"启动不了"**输入订阅已由 L1game.js wrapper接管**你绝不自己订阅输入**不写 ctx.getInput不自建 pendingClicks 队列不在 update(dt) 里挑时机消费点击你只写实例方法 **handleTap(x, y)**点击主输入经营点客 / 打地鼠 / 点按钮等**menu/玩中/结算各 phase 的判定全写在它内部**L1 会在每次 pointerdown **直达**调用它键盘玩法方向键 / 空格 **handleKey(key)**L1 keydown 时调用check 会拦调了 getInput一个输入方法都没暴露

View File

@ -637,6 +637,11 @@ def _validate_c5_best_effort(spec: dict) -> None:
def ensure_play_spec(game_id: str, state: Optional[dict]) -> dict:
"""据 _forensicsView().state() 形态自动产 _wg1-gen/<id>/play-spec.json(对 tools.mjs ensurePlaySpec)。
退役·保留一个版本窗口(W-AXIS-V2 波2,2026-07-10)生产验收链(cheap_gates/cheap_studio/cheap_modify/
gen.mjs done )已全部摘除对本函数的调用便宜档验收权威 = 四门投影(A/B/C/D) 测试 agent 真玩,
不再靠 tap-targets/key-cycle 驱动器把九门驱成 driven实现保留仅供封存对照工具(auto_vs_golden/
compare_node)历史复跑与单测;窗口期满随 play-spec 生成层一并清理
driver 推断( play.cdp.cjs driver 家族): targets 数组(点击类) tap-targets occupied;
否则(按键类) key-cycle让九门 driven=trueE_live/H_progress advisory 升为致命,
根治"纯 harness 裸跑无 driver → 假绿"

View File

@ -154,18 +154,25 @@ def _read_service_run_summary(game_id: str, *, wait_s: float = 5.0) -> dict:
def _read_driver_type(game_id: str):
"""读 ensure_play_spec 产的 driver 类型(_wg1-gen/<id>/play-spec.json.driver.type);缺失 → None(trace.gatespec 省略)。"""
"""W-AXIS-V2 波2:tap-targets/key-cycle 驱动器随取证契约退役,验收改由测试 agent 视觉引导真玩驱动。
停读已退役的 play-spec.json.driver.type,改读测试员真相层(evidence/playtest/playtest.json):存在则回其
模型标识(playtest:<model>),缺失(_build_summary run_acceptance 跑测试员之前先组 summary,此刻尚无)则回
常量 'playtest-agent'两路都如实反映v2 的驱动主体是测试 agenttrace.driverType 现为纯观测维,D11 首局
信号已迁 trace.playtest( plan §5)
"""
import json # noqa: PLC0415
import cheap_run # noqa: PLC0415
p = cheap_run.wg1_game_dir(game_id) / "play-spec.json"
p = cheap_run.wg1_game_dir(game_id) / "evidence" / "playtest" / "playtest.json"
if not p.exists():
return None
return "playtest-agent"
try:
return ((json.loads(p.read_text(encoding="utf-8")) or {}).get("driver") or {}).get("type")
except Exception: # noqa: BLE001
return None
model = (json.loads(p.read_text(encoding="utf-8")) or {}).get("model")
return f"playtest:{model}" if model else "playtest-agent"
except Exception: # noqa: BLE001 读不出只回通用标记,绝不抛
return "playtest-agent"
def _build_summary(game_id: str, brief: str, verdict, svc: dict, turn: dict, t0: float) -> dict:

View File

@ -2,7 +2,7 @@
cheap_studio.py 便宜档 ReAct 主编排spike 核心对照源Node gen.mjs 主循环 + tier2 studio.py:run_studio
组装内置 Agent + ReActConfig + 外层有界 resume 循环 studio.py:404-458done cheap_toolkit finish
工具触发工具内复核 check+build 绿finish 收敛后收口 stage smoke ensure_play_spec 循环外九门 play
工具触发工具内复核 check+build 绿finish 收敛后收口 stage smoke 循环外九门 play
熔断CircuitBreakerMiddleware/ traceTier2TraceMiddleware/ 历史压缩ContextConfig import 复用 tier2零重写
为什么要外层 resumeAgentScope 2.0.3 内置 ReAct "模型产出无 tool_call 的纯文本回合"即退出看一次门没绿就停
@ -115,7 +115,7 @@ def _verdict_brief(v) -> dict:
def _closeout_gates(game_id, *, port, cdp_port) -> dict:
"""CLI 收口门流水线 stage → smoke → ensure_play_spec → 九门 play(一次跑齐;回喂循环与收口段共用)。
"""CLI 收口门流水线 stage → smoke →九门 play(一次跑齐;回喂循环与收口段共用)。
Service cheap_gates.run_cheap_gates 同序,多回传 staged/smoke_ok/driver_type run-summary 组装
(Service 路这些由 collector/driver 另采) play 前清残留 verdict(红线③,镜像 cheap_gates.py:28):
@ -134,18 +134,9 @@ def _closeout_gates(game_id, *, port, cdp_port) -> dict:
# smoke 失败真因必须可见(2026-07-04 bake_off 实证:并发争用致 100% FAIL→全场 key-cycle 回退考卷,
# 而失败原因被吞在 raw 里,烧掉一轮诊断)——打印 raw 尾供归因。
_rec(f"smoke raw 尾:{(sm.get('raw') or '')[-300:]}")
ps = cheap_run.ensure_play_spec(game_id, sm.get("state"))
# driver 类型:本轮新产的直接带;复用旧 spec(hash 一致)时读盘补(spec 是判卷单一来源)。
dt = ps.get("driverType")
if not dt:
try:
spec = json.loads((cheap_run.wg1_game_dir(game_id) / "play-spec.json").read_text(encoding="utf-8"))
dt = (spec.get("driver") or {}).get("type")
except Exception: # noqa: BLE001 —— 读不出只降级 None
dt = None
out["driver_type"] = dt
_rec(f"play-spec {'已产 driver=' + str(dt) if ps.get('wrote') else ps.get('reason', '?')}")
(cheap_run.wg1_game_dir(game_id) / "evidence" / "verdict.json").unlink(missing_ok=True) # 红线③:清陈旧 verdict
# W-AXIS-V2 波2:ensure_play_spec 已摘——play 以 undriven 硬集(A/B/C/D + F/G/I)跑,floor 投影只取 A/B/C/D;
# driver_type 随 tap-targets 驱动器退役恒 None(out 初始化即 None;trace.driverType 观测维不再承 D11 首局信号,已迁 trace.playtest)。
(cheap_run.wg1_game_dir(game_id) / "evidence" / "verdict.json").unlink(missing_ok=True) # 红线③:清陈旧 verdict,防读回上轮绿 verdict 假绿
pr = cheap_run.play(game_id, port=port, cdp_port=cdp_port)
out["verdict"] = pr["verdict"]
return out
@ -210,7 +201,7 @@ async def run_studio(game_id, brief, *, max_iters=40, max_resumes=6, max_tokens=
judge 调用 additive 追加品类扩展条目分母独立小计落 richness.genreNone scaffold_template
GENRE_BY_TEMPLATE 推断两路都没有/条目缺 自动退化为纯通用底座评分
run_gates=True默认spike 单局收口跑全套 stagesmokeensure_play_spec九门 play
run_gates=True默认spike 单局收口跑全套 stagesmoke九门 playundrivenensure_play_spec 波2 已摘
run_gates=Falsegeneration-only compare_node 对照用收口只到 stage+smoke 为止****自动产 play-spec
****跑内部九门 play 由调用方在生成与 play 之间注入金标 spec 再单独 play把驱动器从对照变量里摘掉
Codex C1run_studio 内部已 play对照需把生成与 play 拆开
@ -378,7 +369,7 @@ async def run_studio(game_id, brief, *, max_iters=40, max_resumes=6, max_tokens=
breaker_tripped = {"kind": None, "reason": f"{type(e).__name__}: {e}"}
_rec(f"未捕获异常:{type(e).__name__}: {e}")
# ── 收口:finish 后 stage → smoke → ensure_play_spec → 循环外九门 play ──
# ── 收口:finish 后 stage → smoke →循环外九门 play ──
# CLI 回喂对齐后,run_gates 路的九门收口已在循环内跑过(closeout),此处直接复用最后一轮结果,
# 不重复跑门;对照路(run_gates=False)保持原「只 stage+smoke」行为零改动。
finished = session.finished is not None

View File

@ -1,6 +1,10 @@
"""
compare_node.py 便宜档 Python vs Node 旧路对照验证
封存(W-AXIS-V2 波2,2026-07-10)对照范式以金标 play-spec 驱动九门(H_progress 为可玩门);play-spec/
驱动器已随取证契约退出便宜档验收链,Node 旧路亦已退役两路对照使命完结保留代码供历史复跑;
生产链不再调用,play-spec 生成器窗口期结束后随其一并清理
两层用法:
· 单品类对照(spike U6 起点):compare_logic / build_report / main_async brief 各跑一次,纯逻辑见下
· 三品类批量对照(WU-A 后续 U2):run_multi 三个 tap-targets 代表品类(点击得分/打地鼠/经营点客) n=35,

View File

@ -51,12 +51,22 @@ COVERED_BRIEFS = [
def _row_total_cost(row: dict) -> float:
"""一局的总成本 = 生成 costRmb + 判定 judge.costRmb(判定用独立档,与生成台账隔离,累计兜底要两者都算)。"""
"""一局的总成本 = 生成 costRmb + 判定 judge.costRmb + 测试员 playtest.costRmb(W-AXIS-V2 波2)。
v2 模式下 judge 段镜像 playtest 裁决两者 costRmb 同值 max 只计一次(防同一笔钱双计);
shadow 模式下 judge 是判定器成本playtest 是测试员成本,两笔真实分立相加 acceptanceVersion 分辨
"""
gen = row.get("costRmb") or 0.0
j = row.get("judge") or {}
pt = row.get("playtest") or {}
jc = j.get("costRmb") or 0.0
pc = pt.get("costRmb") or 0.0
try:
return float(gen) + float(jc)
if row.get("acceptanceVersion") == "v2":
extra = max(float(jc or 0.0), float(pc or 0.0)) # v2:judge 段镜像 playtest,同一笔钱别双计
else:
extra = float(jc or 0.0) + float(pc or 0.0) # shadow/v1:判定器与测试员各自真花,分立累计
return float(gen) + extra
except (TypeError, ValueError):
return 0.0
@ -102,6 +112,7 @@ async def _run_one(genre, brief, port, cdp, round_no):
rich = summary.get("richness") or {}
layer = summary.get("failureLayer") or {} # classify_cheap_failure_layer 返回 {layer,reason,failedGates}
j = summary.get("judge") or {} # run_acceptance 写入(仅 run_gates∧finished 时有)
pt = summary.get("playtest") or {} # 测试员段(W-AXIS-V2 波2:v2/shadow 时有,批账观测口径)
return {
"genre": genre, "gid": gid, "round": round_no,
"template": scaffold_template or "_template(通用)",
@ -120,6 +131,14 @@ async def _run_one(genre, brief, port, cdp, round_no):
"costRmb": j.get("costRmb"), "reason": j.get("reason"),
"model": j.get("model"), "frames": j.get("frames"),
} if j else None,
# ── 测试员段(W-AXIS-V2 波2 §4:裁决/degraded/成本/rolls/首局三字段——批账观测口径):
"playtest": {
"accepted": pt.get("accepted"), "verdict": pt.get("verdict"),
"degraded": pt.get("degraded"), "imageBlind": pt.get("imageBlind"),
"rollCount": pt.get("rollCount"), "costRmb": pt.get("costRmb"),
"reason": pt.get("reason"), "summary": pt.get("summary"),
"firstPlay": pt.get("firstPlay"), "model": pt.get("model"),
} if pt else None,
# ── 失败归因三层(driver_contract/mechanical/gameplay/none)+ 归档指针:
"failedGates": v.get("failedGates"),
"failureLayer": layer.get("layer"),
@ -170,12 +189,14 @@ async def main():
f.write(json.dumps(r, ensure_ascii=False) + "\n")
cumulative += _row_total_cost(r)
j = r.get("judge") or {}
pt = r.get("playtest") or {}
print(f"{_mark(r):>13s} r{round_no} {genre:14s} tpl={r.get('template','?')} "
f"accepted={r.get('accepted')} floorPass={r.get('floorPass')} ver={r.get('acceptanceVersion')} "
f"verdictPass(对照)={r.get('verdictPass')} finished={r.get('finished')} "
f"playtest={pt.get('verdict')}/rolls={pt.get('rollCount')} ptDegraded={pt.get('degraded')} "
f"judge={j.get('verdict')}/{j.get('rejectClasses')} degraded={j.get('degraded')} "
f"layer={r.get('failureLayer')} attempts={r.get('attempts')} "
f"genCost=¥{r.get('costRmb')} judgeCost=¥{j.get('costRmb')} wall={r.get('wallSec')}s "
f"genCost=¥{r.get('costRmb')} judgeCost=¥{j.get('costRmb')} ptCost=¥{pt.get('costRmb')} wall={r.get('wallSec')}s "
f"累计=¥{cumulative:.2f} {('FAIL='+r['fail']) if r.get('fail') else ''}")
# ¥200 跨局硬停:立即停机,已完成局全在 JSONL,报 PARTIAL。
if cumulative > _HARD_STOP_RMB:

View File

@ -53,6 +53,7 @@ async def _run_one(sem, tag, brief, port, cdp):
v = summary.get("verdict") or {}
floor = summary.get("floor") or {}
rich = summary.get("richness") or {}
pt = summary.get("playtest") or {} # 测试员段(W-AXIS-V2 波2 批账观测口径)
return {
"tag": tag, "gid": gid, "template": scaffold_template or "_template(通用)",
"locked": write_whitelist is not None,
@ -61,6 +62,10 @@ async def _run_one(sem, tag, brief, port, cdp):
"floorPass": floor.get("pass"), # 预筛权威 = 四门投影(取代 verdictPass)
"acceptanceVersion": summary.get("acceptanceVersion"),
"verdictPass": v.get("pass"), # harness 原生九门/七门,仅对照、不作判定输入
"playtest": { # 测试员裁决摘要(裁决/degraded/rolls/成本)
"verdict": pt.get("verdict"), "degraded": pt.get("degraded"),
"rollCount": pt.get("rollCount"), "costRmb": pt.get("costRmb"),
} if pt else None,
"failedGates": v.get("failedGates"), "attempts": summary.get("attempts"),
"costRmb": summary.get("costRmb"), "richness": rich.get("score"),
"wallSec": round(time.time() - t0, 1),
@ -79,11 +84,13 @@ async def main():
fp = r.get("floorPass") is True
passed += 1 if acc else 0
mark = "" if acc else ("⚠️测试员拒" if fp else ("FALSE-PASS" if r.get("ok") else ""))
pt = r.get("playtest") or {}
print(f"{mark} {r['tag']:16s} tpl={r.get('template')} lock={r.get('locked')} "
f"accepted={r.get('accepted')} floorPass={r.get('floorPass')} ver={r.get('acceptanceVersion')} "
f"verdictPass(对照)={r.get('verdictPass')} failedGates={r.get('failedGates')} "
f"verdictPass(对照)={r.get('verdictPass')} playtest={pt.get('verdict')}/rolls={pt.get('rollCount')} "
f"ptDegraded={pt.get('degraded')} failedGates={r.get('failedGates')} "
f"attempts={r.get('attempts')} rich={r.get('richness')}/12 "
f"cost={r.get('costRmb')} wall={r.get('wallSec')}s {('FAIL='+r['fail']) if r.get('fail') else ''}")
f"cost={r.get('costRmb')} ptCost=¥{pt.get('costRmb')} wall={r.get('wallSec')}s {('FAIL='+r['fail']) if r.get('fail') else ''}")
print(f">>> 跨主题换皮达标(accepted 最终验收 = 四门 ∧ 测试员):{passed}/{len(results)} 达标率={passed/len(results):.0%}")
out = Path(__file__).resolve().parent / "results" / "hard-genre-xtheme.json"
out.parent.mkdir(exist_ok=True)

View File

@ -59,11 +59,15 @@ def test_puzzle_exemplars_pass_contract_check():
def test_puzzle_exemplars_forensics_contract_present():
"""静态兜底:两件范例的 game-logic.js 必须带解谜取证契约关键面(createGame/_forensicsView/targets/occupied)。"""
"""静态兜底:两件范例的 game-logic.js 必须带现行输入契约面(createGame/handleTap/bundle.tick)。
W-AXIS-V2 波2:取证契约(targets/occupied) tap-targets 驱动器退役,不再是范例必备面;
_forensicsView 降级为可选调试接口(范例保留实现但不作断言)
"""
for tmpl in _PUZZLE_EXEMPLARS:
src = (_GAME_RUNTIME / "games" / tmpl / "src" / "game-logic.js").read_text(encoding="utf-8")
for needle in ("export function createGame({ plugins, bundle, viewport })",
"_forensicsView", "targets", "occupied", "bundle.tick"):
"handleTap", "bundle.tick"):
assert needle in src, f"{tmpl}: game-logic.js 缺契约面「{needle}"

View File

@ -13,10 +13,12 @@ from cheap_roles import build_system_prompt, SKILL_PATH
def test_entry_contract_present():
"""入口契约五法 + _forensicsView 在场。"""
"""入口契约五法在场;_forensicsView 取证契约教学已清洗(W-AXIS-V2 波2:自动验收随驱动器退役,
验收 = 四门投影 测试 agent 真玩,模型不再为死契约写代码)正文必须零残留"""
p = build_system_prompt("t1")
assert "createGame({ plugins, bundle, viewport })" in p
assert "_forensicsView" in p
assert "_forensicsView" not in p
assert "occupied" not in p # targets/occupied 语义同批清洗
assert "init(boot)/update(dt)/render(g)/destroy()" in p

View File

@ -1,6 +1,6 @@
---
id: config.cheap-system
version: 1.6.3
version: 1.7.0
---
你是 A-model 游戏生成 agent。目标产物 = 一款**能跑能玩、有内容、不易同质化的高质小游戏**(多文件 LittleJS落在 ⟦G⟧/。
@ -11,7 +11,7 @@ version: 1.6.3
- 我已把克隆起点拷到 ⟦G⟧/⟦SCAFFOLD_DESC⟧。你的活是据 brief **借插件能力把它改造成一款高质游戏**——别把它当"这档就该这么简单"的天花板。
- **L1 固定 plumbing 别动write 会被拒)**⟦G⟧/index.html、⟦G⟧/entry-bundle.js、⟦G⟧/src/game.js薄 wrapper、**⟦G⟧/src/host-config.js**host 装配:已替你注入**全 11 件能力插件** + viewport 固定 390×844 + 工厂 wiring你无需装配任何插件
- **你只写 L3 游戏本体****⟦G⟧/src/game-logic.js必写游戏核心** + 按需 core.js纯逻辑/ render.js画面/ balance.js数值/ assets.js资产。结构简单可把逻辑/画面集中在 game-logic.js 内;多系统/多场景再拆 core/render。把脚手架起点改造成 brief 要的**高质游戏**(多借插件能力做卖相与深度);**画面读 viewport.w/h390×844 竖屏)**。
- **入口契约(钉死)**game-logic.js 必须 `export function createGame({ plugins, bundle, viewport })` 返回 GameInstance 五法 init(boot)/update(dt)/render(g)/destroy()+必须 _forensicsView。**插件已扁平注入 plugins**用到才解构sceneFsm/sessionScore/hudUi/timerScheduler/save/gamefeel/juice/palettePost/audioMusic/collision/physics。
- **入口契约(钉死)**game-logic.js 必须 `export function createGame({ plugins, bundle, viewport })` 返回 GameInstance 五法 init(boot)/update(dt)/render(g)/destroy()。**插件已扁平注入 plugins**用到才解构sceneFsm/sessionScore/hudUi/timerScheduler/save/gamefeel/juice/palettePost/audioMusic/collision/physics。
【先读手册(必须;别凭记忆猜 API
1. read_file('.agents/skills/littlejs-game-dev.md') —— code 层作业手册(结构/边界/红线/11 注入插件 API 速查)。
@ -19,10 +19,10 @@ version: 1.6.3
3. 用到某插件就 read_file 它的 api.d.ts如 game-runtime/src/plugins/scene-fsm/api.d.ts、.../session-score/api.d.ts看精确签名。
4. 你的起点 ⟦G⟧/src/game-logic.js 本身就是**跑通的脚手架范例**(五法齐全 + 调 4 编排插件read_file 它,照其结构**借插件能力**改造成 brief 的高质游戏(最省事:在它基础上改、多调插件加内容,别从零写)。
5. **若 brief 属经营 / 养成 / 放置 / 点客这类「攒资源→成长→解锁」品类**read_file('.agents/skills/sim-business-game-design.md') 取「设计什么才好玩」的范式6 心理引擎 / §1 玩法范式含进货补货环+决策模式清单 / §8 设计配方 / §10 反无趣自检。littlejs-game-dev 教你「代码怎么写」、sim-business 教你「设计什么才好玩」——配对用:先想清好玩、再写正确。
6. **若 brief 属剧情 / 互动叙事 / 文字冒险这类「读文本→做选择→推进剧情」品类**read_file('.agents/skills/narrative-game-design.md') 取「设计什么才好玩」的范式6 心理引擎 / §1 范式含分支选择+多结局图鉴 / §3 文本红线 / §8 设计配方 / §9 可达性红线含取证契约);再 read_file('game-runtime/games/_template-story/src/game-logic.js') 看剧情品类过门范例(分支推进 + 结局 latch + 结局图鉴的骨架写法,其故事数据在同目录 core.js 顶部参数空间)。
6. **若 brief 属剧情 / 互动叙事 / 文字冒险这类「读文本→做选择→推进剧情」品类**read_file('.agents/skills/narrative-game-design.md') 取「设计什么才好玩」的范式6 心理引擎 / §1 范式含分支选择+多结局图鉴 / §3 文本红线 / §8 设计配方 / §9 可达性红线);再 read_file('game-runtime/games/_template-story/src/game-logic.js') 看剧情品类过门范例(分支推进 + 结局 latch + 结局图鉴的骨架写法,其故事数据在同目录 core.js 顶部参数空间)。
7. **若 brief 属 TRPG / 掷骰 / 冒险 / 地牢爬塔这类「掷骰对抗→抉择→成长」品类**read_file('.agents/skills/trpg-game-design.md') 取「设计什么才好玩」的范式6 心理引擎 / §1 玩法范式含掷骰爬塔 / §3 掷骰概率数值 / §9 红线〔核心动作单击即结算·掷骰过程可见〕 / §10 反无趣自检)。
8. **若 brief 属非遗 / 传统技艺 / 民俗这类「传统文化玩法化」品类**read_file('.agents/skills/heritage-game-design.md') 取「设计什么才好玩」的范式§1 玩法化范式含工序节拍 / §3 双层奖励与工序节奏目标值 / §9 文化表述红线 / §10 反贴皮自检);需要完整过门范例再 read_file('game-runtime/games/_fewshot-feiyi/src/game-logic.js')。
9. **若 brief 属解谜(找规律 / 找不同 / 翻牌配对 / 顺序推理 / 点选消除这类「观察→推理→点解」品类)**read_file('.agents/skills/puzzle-game-design.md') 取解谜设计范式(顿悟距离 / 规则递进关卡阶梯 / 卡壳兜底 / 成绩炫耀 / §9 品类可达红线——解谜的「解」必须映射到 targets occupied);想看「丰富成什么样」的完整过门正例,可 read_file('game-runtime/games/_fewshot-puzzle/src/game-logic.js') 参照。
9. **若 brief 属解谜(找规律 / 找不同 / 翻牌配对 / 顺序推理 / 点选消除这类「观察→推理→点解」品类)**read_file('.agents/skills/puzzle-game-design.md') 取解谜设计范式(顿悟距离 / 规则递进关卡阶梯 / 卡壳兜底 / 成绩炫耀 / §9 品类可达红线);想看「丰富成什么样」的完整过门正例,可 read_file('game-runtime/games/_fewshot-puzzle/src/game-logic.js') 参照。
【先设计后写码:决定这游戏好不好玩(关键,别跳过)】
能跑 ≠ 好玩。动手写码前,先据 brief 在 **game-logic.js 顶部写一段设计注释块**≤8 行:⑨ 判定句三个空填好 + 核心数值锚 + ⑩ 的三段难度)定下**轻量玩法设计**,再实现——设计只在心里 = 收尾自查与验收都无从对照:
@ -45,7 +45,7 @@ A 读手册1 + 按品类 5/6/7/8/9+ 读你的起点 game-logic.js4
- game-logic.js 必须 **export function createGame({ plugins, bundle, viewport })**(命名导出、别改名、别默认导出);**零引擎 import**(引擎经 boot.ctx.getEngine())。
- 游戏源game-logic / core / render**零裸** Math.random / Date.now / setTimeout / requestAnimationFrame / new AudioContext / addEventListener —— 时间随机经 boot.ctx定时经 timer-scheduler音效经 plugins.audioMusic**输入不调 ctx.getInput**(已收归 L1见下「输入契约」
- update(dt) 内**必须调 bundle.tick(dt)**bundle 是 createGame 入参;否则插件 onFrame 不推进、timer 永不到期、一局不结束)。**dt 单位是「秒」≈1/60**——游戏内若用毫秒计时(倒计时 / spawn 间隔 / 顾客耐心,常量都是 ms 如 60000/1300累加时务必「elapsedMs += dt * 1000」×1000 → 计时慢 1000 倍 → 永不到 spawn 阈值=零顾客、一局不结束。**timerScheduler.after(ms,cb)/every(ms,cb) 同样收毫秒(#73 秒写 after(3000)、别写 after(3);与 dt=秒相反,别混。**
- **入参是扁平的 { plugins, bundle, viewport }——没有 opts、没有 opts.runtime、没有 ctx**L1 已替你摊平)。受控面 ctx 只在五法 **init(boot) 的 boot.ctx****调用形态**:时间 **ctx.time()** 或 ctx.time.nowMs()、随机 **ctx.random()** 或 ctx.random.next()/ctx.random.range(a,b)、画布 ctx.getContext2d()、引擎 ctx.getEngine()、日志 ctx.log(tag,msg)。**#1ctx.time/ctx.random 既可直接调用ctx.time()=毫秒、ctx.random()=[0,1))、又保留方法(.nowMs()/.elapsedMs()、.next()/.range(a,b)/.reseed())——两形态都对**。**但 nowMs/next 是 time/random 上的方法、不是 ctx 上的——别写 ctx.nowMs()(应 ctx.time() 或 ctx.time.nowMs())。**五法 = init(boot)/update(dt)/render(g)/destroy()+必须 _forensicsView+ **输入方法 handleTap(x,y)**(见下「输入契约」)。
- **入参是扁平的 { plugins, bundle, viewport }——没有 opts、没有 opts.runtime、没有 ctx**L1 已替你摊平)。受控面 ctx 只在五法 **init(boot) 的 boot.ctx****调用形态**:时间 **ctx.time()** 或 ctx.time.nowMs()、随机 **ctx.random()** 或 ctx.random.next()/ctx.random.range(a,b)、画布 ctx.getContext2d()、引擎 ctx.getEngine()、日志 ctx.log(tag,msg)。**#1ctx.time/ctx.random 既可直接调用ctx.time()=毫秒、ctx.random()=[0,1))、又保留方法(.nowMs()/.elapsedMs()、.next()/.range(a,b)/.reseed())——两形态都对**。**但 nowMs/next 是 time/random 上的方法、不是 ctx 上的——别写 ctx.nowMs()(应 ctx.time() 或 ctx.time.nowMs())。**五法 = init(boot)/update(dt)/render(g)/destroy() + **输入方法 handleTap(x,y)**(见下「输入契约」)。
- **插件扁平注入:直接 const { sceneFsm, sessionScore, hudUi, timerScheduler } = plugins用到才解构****绝不写 opts.runtime.plugins**(那是 L1 内部的,你见不到 → 写了就 undefined → boot 崩 reading 'define')。游戏只用插件、不 new、不 register。
- **可用插件键(全 11 已注入,用到才取)**sceneFsm / sessionScore / hudUi / timerScheduler / save / gamefeel / juice / palettePost / audioMusic / collision / physics。
- **插件方法名一律以 api.d.ts 为准、别臆测**实测幻觉scene-fsm 写 defineScenes应 define。用到某插件先 read_file 它的 api.d.tsskill「⚠ 实测易犯的幻觉 API」逐条列 ❌→✅。
@ -53,7 +53,6 @@ A 读手册1 + 按品类 5/6/7/8/9+ 读你的起点 game-logic.js4
- **sceneFsm.define(name, handlers) 逐场景调一次——绝不单对象批量注册**:❌ sceneFsm.define({ menu: {...}, play: {...}, over: {...} }) → 第一参当成了场景名「[object Object]」、menu/play/over 全没真注册 → 后续 transition('play') 找不到目标态被忽略 → 卡菜单(实测合成/2048 类翻车点)。✅ **每个场景各调一次**sceneFsm.define('menu', { onEnter(){...} }); sceneFsm.define('play', { onEnter(){...}, update(dt){...} }); sceneFsm.define('over', { onEnter(){...} });define 第一参恒为场景名字符串、第二参才是回调表 { onEnter, onExit, update, render }**回调表里 update 只收 (dt)、render 只收 (g)、与五法同形——别臆造场景名/state 首参,绝不写 update(scene, dt)**;可链式 .define(...).define(...))。
- **画面读 viewport.w/h**viewport 是入参;别硬编码 390×844
- **美术资产(若 assets/manifest.json 非空)**:先 read_file('assets/manifest.json') 看有哪些资产init(boot) 里 const assets = boot.assets || {}render 里 const img = assets['ref名']?.image; if (img) g.drawImage(img, x, y, w, h); else 程序化回退。**ref = manifest 条目 file 去扩展名**shopkeeper-idle.jpg → 'shopkeeper-idle')。缺图/加载失败时 assets[ref].image 为 nullhost 已容错)→ 务必走程序化回退、别崩。BGM/音效经 plugins.audioMusic不在 boot.assets。无 manifest/无美术 → 全程序化绘制即可。
- **必须实现 _forensicsView()**(可测性红线 + 自动驱动契约):返回 { state(){…}, measures(){…} }state() 至少 { phase, score }。**若游戏靠点击推进(经营点客/打地鼠/点离散目标等state().targets:[{x,y,occupied}] 列当前屏上目标,可点的(等待的客/冒头的鼠)置 occupied:true、不可点置 false** —— 自动验收据此点 occupied:true 的目标真驱动你的游戏。**键名固定为 `targets`,不得改成 items/objects 等**:判卷驱动器靠 state 有无 `targets` 键选驱动器族(点目标族 vs 循环按键族),改名=选错族=盲驱动器驱动不了你的游戏=H 进展门挂。**score 用累计进展(营收/得分),随推进上升** —— 自动验收据 score 上升判"真有进展"。不写则验证读不到状态check 会拦)。**钉死phase/targets/score 必须在 state() 函数体内部实时读取/计算——绝不在 _forensicsView() 外层先算好再闭包返回**host 只在 boot 调一次 _forensicsView外层算的值会被定格成 boot 快照=永远 menu、targets 永远空 → 自动验收看不到顾客、永判死菜单)。
- **关键事件用 ctx.log 记日志**:在 init/场景切换/得分/出错处 调 ctx.log('tag', 信息)。**ctx 来自 init 的入参 boot即「const ctx = boot.ctx」——绝不是 boot.boot.ctx**host 传入的 boot 已是 {ctx, mainContext, canvas, seed, assets},多套一层 .boot=undefined → ctx=null → 无输入、游戏点不动check 会拦 .boot.ctx。插件调用已自动记你只补游戏语义事件。
- **输入契约(钉死·根治"启动不了"**:输入订阅已由 L1game.js wrapper接管**你绝不自己订阅输入**——不写 ctx.getInput、不自建 pendingClicks 队列、不在 update(dt) 里挑时机消费点击。你只写实例方法 **handleTap(x, y)**(点击主输入:经营点客 / 打地鼠 / 点按钮等;**menu/玩中/结算各 phase 的判定全写在它内部**L1 会在每次 pointerdown 时**直达**调用它;键盘玩法(方向键 / 空格)写 **handleKey(key)**L1 在 keydown 时调用。check 会拦「调了 getInput」与「一个输入方法都没暴露」。

View File

@ -176,7 +176,7 @@ prompts:
# 首版正文 = _SYSTEM_PROMPT 内置原文逐字节对齐(含 ⟦G⟧/⟦SCAFFOLD_DESC⟧ 占位符),默认行为字节不变;
# 调一条 prompt = 改 04-config/cheap-system.md 正文 + 升 version下次生成自动生效。
- id: config.cheap-system
version: 1.6.3 # v1.6.3(W-AXIS 波2 拆残笼):①「两层奖励」改纯游戏设计语(基础反馈保上手+技巧分给深度)、删「围盲驱动器/绕正则」措辞②「check 正则命中」改「词法真实命中、报错带行号」(红线判定换词法);③删整文件重写熔断恐吓、edit_file 补锚点容错说明(空白归一/近似片段)。v1.6.2:正文补 edit_file 工具契约(七工具/小改优先 edit_file、别整文件重写)——修热取源停在「六工具/write_file 整体覆盖」的 stale 欠账,治大文件整写截断(便宜档死圈病根之一)。v1.6.1:起局鲁棒性 + targets 键契约进红线——①起局/交互按钮命中矩形必须在状态进入点 onEnter 确定性建立、render 只画(治并发 rAF 饥饿下卡 menu②点目标玩法 state 键名固定 `targets`(驱动器契约 C5 依赖,改名=盲驱动器选错族=H 门挂。v1.6.0:打磨不降档(创始人 07-04 定调「档位切 scope 不切 polish」——删 spike「能跑能玩即可」残留、hitstop 出禁项改结算标配、设计块强制落盘+finish 前收尾自查、新增 ⑩ 难度曲线、⑨ 经营决策模式指针。v1.5.0:策划知识包 v2——⑨ 否决项加判定句式+内嵌正反例8/9 条计数口径统一prompt.mjs 冻结为 A/B 史料不再双源同改。v1.4.0W-GENRE 品类路由并集(剧情+6/TRPG+7/非遗+8/解谜+9
version: 1.7.0 # v1.7.0(W-AXIS-V2 波2 契约退役):删 _forensicsView「自动验收契约」整条红线与全部 targets/occupied 语义(五法句/品类指路同步)——tap-targets 驱动器与 play-spec 退出便宜档验收链,验收 = 四门投影 ∧ 测试 agent 视觉引导真玩,模型不再为死契约写代码;handleTap 输入契约与 ctx.log 日志纪律保留(服务真人与测试员)。v1.6.3(W-AXIS 波2 拆残笼):①「两层奖励」改纯游戏设计语(基础反馈保上手+技巧分给深度)、删「围盲驱动器/绕正则」措辞②「check 正则命中」改「词法真实命中、报错带行号」(红线判定换词法);③删整文件重写熔断恐吓、edit_file 补锚点容错说明(空白归一/近似片段)。v1.6.2:正文补 edit_file 工具契约(七工具/小改优先 edit_file、别整文件重写)——修热取源停在「六工具/write_file 整体覆盖」的 stale 欠账,治大文件整写截断(便宜档死圈病根之一)。v1.6.1:起局鲁棒性 + targets 键契约进红线——①起局/交互按钮命中矩形必须在状态进入点 onEnter 确定性建立、render 只画(治并发 rAF 饥饿下卡 menu②点目标玩法 state 键名固定 `targets`(驱动器契约 C5 依赖,改名=盲驱动器选错族=H 门挂。v1.6.0:打磨不降档(创始人 07-04 定调「档位切 scope 不切 polish」——删 spike「能跑能玩即可」残留、hitstop 出禁项改结算标配、设计块强制落盘+finish 前收尾自查、新增 ⑩ 难度曲线、⑨ 经营决策模式指针。v1.5.0:策划知识包 v2——⑨ 否决项加判定句式+内嵌正反例8/9 条计数口径统一prompt.mjs 冻结为 A/B 史料不再双源同改。v1.4.0W-GENRE 品类路由并集(剧情+6/TRPG+7/非遗+8/解谜+9
stage: "04-config"
owner: WS2
file: 04-config/cheap-system.md

View File

@ -14,7 +14,7 @@
- **换手艺不重写循环**:工序链(刻版→上墨→铺纸→拓印)与作品表(福字/门神/财神/麒麟/童子抱鱼)全落在 core.js 参数空间;工序节拍结构(就绪→窗口内点击→出成品→回炉)原样保留。
- **在循环上加机制**:正窗暴击 + 连击、攒工钱解锁新图样(本品类成长轴 = 工艺步骤/图样解锁)、结算屏摆出本局印成的年画(炫耀时刻)。
- **取证契约不破**:`_forensicsView().state()``targets`(occupied=当前就绪工位)与 `score`(单调上升)语义未动——这是过九门的命脉
- **调试视图不破**:`_forensicsView` 保留为可选调试接口(W-AXIS-V2 起验收 = 四门投影 ∧ 测试 agent 真玩,不再读状态导出)
## 复验配方(怀疑漂移时跑)
@ -24,8 +24,7 @@ node tools/amodel-gen/tools.mjs scaffold-saa p11d-fewshot-recheck _template-feiy
cp games/_fewshot-feiyi/src/*.js games/amgen-p11d-fewshot-recheck/src/
node -e "import('./tools/amodel-gen/tools.mjs').then(m=>process.exit(m.check('p11d-fewshot-recheck').ok?0:1))"
node tools/amodel-gen/tools.mjs build-saa p11d-fewshot-recheck && node tools/amodel-gen/tools.mjs stage p11d-fewshot-recheck
cp ../cheap-worker/fixtures/golden-specs/heritage-craft.play-spec.json games/_wg1-gen/p11d-fewshot-recheck/play-spec.json
bash games/_wg1-gen/_shared/serve-and-play.sh p11d-fewshot-recheck
bash games/_wg1-gen/_shared/serve-and-play.sh p11d-fewshot-recheck # undriven 九门(W-AXIS-V2:验收权威=四门投影∧测试员,此处只作机器门快检)
```
## 漂移纪律(设计档 §3⑥/§6:per-genre 正例是「必须同改」清单成员)

View File

@ -10,12 +10,12 @@
* · **吉祥图样解锁**:累计工钱达标(80/160) 麒麟/童子抱鱼入图鉴可印
* · 60s 到点切 over 结算,再开一印
*
* 已在 _forensicsView.state 实时暴露工资 + combo + 暴击 + 解锁图样列表,
* 满足九门 H 进展门(score 单调上升) E_live(targets 含就绪工位)
* _forensicsView.state 实时暴露工资 + combo + 暴击 + 解锁图样列表可选调试接口,
* 供人工调试观测(W-AXIS-V2 起验收 = 四门投影 测试 agent 真玩,不再读状态导出)
*
* 入参(L1 已摊平):createGame({ plugins, bundle, viewport })
* 受控面 ctx init(boot) boot.ctx
* 返回:init/update/render/destroy + handleTap + _forensicsView
* 返回:init/update/render/destroy + handleTap(+可选 _forensicsView)
*/
'use strict';
@ -115,7 +115,7 @@ export function createGame({ plugins, bundle, viewport }) {
return;
}
if (cur === SCENE_PLAY) {
// 工序核心:点中就绪工位 = 完成该道(1 步成序,自动验收据此涨分)。
// 工序核心:点中就绪工位 = 完成该道(1 步成序,张力在火候窗口时机、不在选择流程)。
if (!work || !isStepTappable(work, elapsedMs)) return;
const st = stations[work.stepIndex];
if (st && hitStation(x, y, st)) completeStep(st);
@ -286,10 +286,8 @@ export function createGame({ plugins, bundle, viewport }) {
},
/**
* 取证契约:实时算 phase/score/targets/combo/解锁等
* 必须在函数体内实时算,不能闭包冻结(否则 host boot 快照= menu)
* targets = 每工位一项;当前工序在窗口内可点 occupied:true,
* 其余 false(自动验收 driver 据此驱动游戏)
* 调试视图(可选接口,验收不读):实时算 phase/score/targets/combo/解锁等,供人工调试观测
* 函数体内实时算是好习惯,别闭包冻结(否则读到的恒是 boot 快照= menu)
*/
_forensicsView() {
return {

View File

@ -12,7 +12,7 @@
| 同款钩子 | `GENRE_META`(品类原型 rule-match-board + 主题标签) | 同款钩子(L4) |
| 卡壳兜底 | 8s 无进展剩余解脉动(卡壳不出局) | P4 卡壳兜底 |
**取证契约与骨架完全一致**(丰富不碰契约):`targets` 只把当前正确解标 `occupied:true`;`score` 随点对/过关单调升;menu/over 期 `targets:[]` 键在场
**调试视图与骨架完全一致**(丰富不碰结构层):`_forensicsView` 为可选调试接口(W-AXIS-V2 起验收 = 四门投影 ∧ 测试 agent 真玩,不再读状态导出),保留它是给调试观测一个统一出口
范例签名 == 现契约:本目录随 `cheap-worker/tests/test_genre_puzzle_assets.py` 走机器校验门(scaffold + tools.mjs check),API 漂移会在 CI 挂——**改插件签名时必须同步改本范例**(防它成下一代漂移源)。

View File

@ -11,7 +11,7 @@
* · 称号 + 跨局收集:总星数换称号;累计星星经 save 插件持久(回归可见积攒,第二动机)
* · 结算炫耀:分数/星级/称号/通关数/最高连击/累计收集/最佳纪录(不是打完黑屏)
* · 同款钩子:GENRE_META 品类原型 + 主题标签(remix 预填元数据)
* 取证契约与骨架一致(targets 只把当前正确解标 occupiedscore 单调升)丰富不碰契约
* 调试视图与骨架一致(可选 _forensicsView,验收不读它)丰富不碰结构层
*
*/
@ -290,7 +290,7 @@ export function createGame({ plugins, bundle, viewport }) {
inited = false;
},
/** 取证契约同骨架:targets 只把当前正确解标 occupied、score 单调升;丰富层键 additive。 */
/** 调试视图同骨架(可选接口,验收不读):targets 标当前正确解便于调试观测;丰富层键 additive。 */
_forensicsView() {
return {
state: () => ({

View File

@ -1,11 +1,11 @@
# _template-feiyi — A-model「非遗工序节拍」品类骨架(3 层架构)
一个**可玩的完整工序游戏**:一件作品 = 一条有先后的工序链,工序依次「就绪」→ 在火候窗口内点击就绪工位完成该道 → 整条链走完出一件成品 → 错过窗口回炉重做 → 60 秒限时开工结算(menu→play→over)。这份骨架**已经把整套工序循环接好了**(含让自动验收能跑通的取证契约)——你的活是按 brief **换手艺**,不是重写循环。
一个**可玩的完整工序游戏**:一件作品 = 一条有先后的工序链,工序依次「就绪」→ 在火候窗口内点击就绪工位完成该道 → 整条链走完出一件成品 → 错过窗口回炉重做 → 60 秒限时开工结算(menu→play→over)。这份骨架**已经把整套工序循环接好了**(含可选的调试视图)——你的活是按 brief **换手艺**,不是重写循环。
**怎么改 = 给这间工坊换一门手艺,循环结构别动:**
- 换手艺/工序/作品/难度 → 改 `src/core.js` 顶部的「多样性参数空间」(`CRAFT_CHAIN` 工序链、`WORKS` 作品表、就绪间隔/窗口/回炉常量)。制陶写选料/塑形/上彩,织造写纺线/上机/织纹,灯彩写扎骨/糊面/点灯——**同骨架做出不雷同的手艺**就靠这里。工序名用该门手艺的**通识**工序词,别编造具体非遗项目的失实细节(设计手册 §9 红线)。
- 换观感(案台布景/工位长相/配色)→ 改 `src/render.js`
- 想加机制(正窗暴击 combo/解锁新作品/图鉴)→ 在 `src/game-logic.js` 循环里加,但**别破坏 `_forensicsView``targets`/`score` 契约**(改坏=九门 H 进展门直接挂)
- 想加机制(正窗暴击 combo/解锁新作品/图鉴)→ 在 `src/game-logic.js` 循环里加。
- **别推倒重写整个循环**:工序就绪/火候窗口/回炉/出成品/限时结算都接好了,从它起步只补身份,远比从零写省。
- 玩法设计先读 [.agents/skills/heritage-game-design.md](../../../.agents/skills/heritage-game-design.md)(这个品类什么好玩:玩法化范式/节奏目标值/双层奖励/文化表述红线)。
@ -30,7 +30,7 @@ export function createGame({ plugins, bundle, viewport }) {
render(g) { /* 画在 g;读 viewport.w/h */ },
handleTap(x, y) { /* ★输入入口:各 phase 点击逻辑;L1 在 pointerdown 时直达调用(别自己订阅输入) */ },
destroy() {},
_forensicsView() { return { state(){ /* phase/score/targets 实时算 */ }, measures(){} }; }, // ★工序档自动验收契约
_forensicsView() { return { state(){ /* phase/score/targets 实时算 */ }, measures(){} }; }, // 可选调试接口(验收不读)
};
}
```
@ -43,7 +43,7 @@ export function createGame({ plugins, bundle, viewport }) {
- **零引擎 import**(引擎经 `boot.ctx.getEngine()`);游戏源**零裸** Math.random/Date.now/setTimeout/new AudioContext/addEventListener —— 时间随机经 boot.ctx,定时经 timer-scheduler,音效经 plugins.audioMusic;**输入不调 ctx.getInput**——写实例方法 `handleTap(x,y)`(点击)/`handleKey(key)`(键盘),L1 在 pointerdown/keydown 时直达调用。
- **插件方法名一律以 `src/plugins/<name>/api.d.ts` 为准**,别臆测(scene-fsm 是 `define` 不是 defineScenes)。本骨架已用对的母语写法:`sessionScore.add(n)` 计分、`juice.burst(x,y)` 爆点、`hudUi.drawButton(g,rect,opts)` 返回命中矩形可直接接住判命中。
- **画面读 `viewport.w/h`**,别硬编码 390×844。
- **`_forensicsView().state()` 的工序契约**:`targets` 列每工位一项、「当前就绪且窗口内可点」的工位 `occupied:true`(自动验收只点这些工位驱动工序);`score` 随完成工序单调上升(自动验收据它判真有进展)。**换手艺加机制都别动这两个键的语义。**
- **`_forensicsView()` 是可选调试接口**:state() 实时导出工序/工钱/窗口态等,供人工调试观测;W-AXIS-V2 起验收不再读它(验收 = 四门投影 ∧ 测试 agent 真玩)。
## 你写什么 vs 调什么

View File

@ -11,17 +11,17 @@
* · 火候窗口(ready windowMs)过窗回炉:该道重做损时间不清进度 + 轻屏震
* · 整条链走完 = 出一件成品:成品奖励 + 庆祝,自动开下一件(WORKS 轮换)
* · 60 秒限时开工 到点收工结算(sceneFsm over,latch 终态)
* · _forensicsView targets/score 契约(自动验收靠它驱动你的游戏,**改坏它=九门 H 直接挂**)
* · _forensicsView 调试视图(可选:host.state() 读它做调试观测,不影响验收)
*
* 你按 brief (身份层 · 多样性源,做出不雷同的手艺):
* · 换手艺/工序/作品/难度 core.js 顶部的多样性参数空间(CRAFT_CHAINWORKS节奏常量)
* · 画面观感(案台长啥样工位长啥样配色) render.js
* · 想加机制(正窗暴击 combo解锁新作品图鉴) 在循环里加,**别破坏 targets/score 契约**
* · 想加机制(正窗暴击 combo解锁新作品图鉴) 在循环里加
* · 改写本文件时 **import 列表必须与你用到的 core.js 导出一一对齐**: import 的裸标识符
* check/build 都拦不住(esbuild 当全局放行),运行时点一下就 ReferenceError 被隔离 永卡菜单九门全挂
*
* 入参(L1 已摊平):createGame({ plugins, bundle, viewport });受控面 ctx init(boot) boot.ctx
* 返回 GameInstance:init/update/render/destroy + handleTap + _forensicsView
* 返回 GameInstance:init/update/render/destroy + handleTap(+可选 _forensicsView)
*
*/
@ -113,7 +113,7 @@ export function createGame({ plugins, bundle, viewport }) {
return;
}
if (cur === SCENE_PLAY) {
// 工序核心:点中「就绪工位」= 完成当前道(1 步成序,自动验收据此涨分)。
// 工序核心:点中「就绪工位」= 完成当前道(1 步成序,张力在火候窗口时机、不在选择流程)。
if (!work || !isStepTappable(work, elapsedMs)) return;
const st = stations[work.stepIndex];
if (st && hitStation(x, y, st)) completeStep(st);
@ -256,11 +256,9 @@ export function createGame({ plugins, bundle, viewport }) {
},
/**
* 取证视图自动验收契约·改坏=九门挂:state() **实时**(绝不外层先算好闭包返回)
* · targets:每工位一项,occupied=true 表示当前就绪窗口内可点的工位自动验收只点 occupied:true 的工位
* **键名固定为 `targets`,不得改成 items/objects **:判卷驱动器(C5) state 有无 `targets` 键选驱动器族,改名=选错族=盲驱动点不到=H 门挂
* · score:= 工钱,随完成工序单调上升自动验收据它判真有进展
* 加机制时这两个键的语义务必保持(targets 列可点目标score 随进展升)
* 取证视图(可选调试接口):host.state() 据此读工序/工钱/窗口态等,供人工调试观测
* W-AXIS-V2 起验收不再读它(验收 = 四门投影 测试 agent 真玩);state() 内实时算是好习惯
* (外层闭包会定格成 boot 快照,调试时读到的恒是旧值)
*/
_forensicsView() {
return {

View File

@ -1,12 +1,12 @@
# _template-puzzle — A-model「规则谜盘」解谜品类轻脚手架(3 层架构)
一个**可玩的完整解谜游戏**:每关一条线索(找出所有【红色】/【星形】/【两颗】…)→ 观察盘面 → 点中所有符合的格子过关 → 关卡逐关引入新规则维度 → 通全关提前胜利 / 限时到点结算(menu→play→over)。这份骨架**已经把整套谜盘循环接好了**(含让自动验收能跑通的取证契约)——你的活是按 brief **换主题、加深度**,不是重写循环。
一个**可玩的完整解谜游戏**:每关一条线索(找出所有【红色】/【星形】/【两颗】…)→ 观察盘面 → 点中所有符合的格子过关 → 关卡逐关引入新规则维度 → 通全关提前胜利 / 限时到点结算(menu→play→over)。这份骨架**已经把整套谜盘循环接好了**(含可选的调试视图)——你的活是按 brief **换主题、加深度**,不是重写循环。
**怎么改 = 给这款解谜换身份,循环结构别动:**
- 换主题/元素/文案 → 改 `src/core.js``THEME`(shapes/colors/countNames/标题)。宝石写圆/菱/星,水果写苹果/香蕉/葡萄,海洋写鱼/贝/星——**同骨架做出不雷同的解谜**就靠这里。
- 调难度/关卡阶梯 → 改 `src/core.js``LEVELS`(盘面大小/解数/规则维度逐关递进)与节奏常量(观察窗/锁定/连击窗)。
- 换观感(底景/元素画法/配色)→ 改 `src/render.js`(`drawGlyph` 把圆/菱/星换成你的元素)。
- 想加机制(星级/图鉴/新规则维度/每日谜题)→ 在 `src/game-logic.js` 循环里加,但**别破坏 `_forensicsView``targets`/`score` 契约**(改坏=九门 H 进展门直接挂)
- 想加机制(星级/图鉴/新规则维度/每日谜题)→ 在 `src/game-logic.js` 循环里加。
- **别推倒重写整个循环**:出题/观察窗/点解判定/连击/过关递进/限时结算/卡壳提示都接好了,从它起步只补身份与深度,远比从零写省。
- 「设计什么才好玩」读 `.agents/skills/puzzle-game-design.md`(顿悟距离/规则递进/卡壳兜底/成绩炫耀);完整过门正例参照 `game-runtime/games/_fewshot-puzzle/src/`
@ -31,7 +31,7 @@ export function createGame({ plugins, bundle, viewport }) {
render(g) { /* 画在 g;读 viewport.w/h */ },
handleTap(x, y) { /* ★输入入口:各 phase 点击逻辑;L1 在 pointerdown 时直达调用(别自己订阅输入) */ },
destroy() {},
_forensicsView() { return { state(){ /* phase/score/targets 实时算 */ }, measures(){} }; }, // ★解谜档自动验收契约
_forensicsView() { return { state(){ /* phase/score/targets 实时算 */ }, measures(){} }; }, // 可选调试接口(验收不读)
};
}
```
@ -44,7 +44,7 @@ export function createGame({ plugins, bundle, viewport }) {
- **零引擎 import**(引擎经 `boot.ctx.getEngine()`);游戏源**零裸** Math.random/Date.now/setTimeout/new AudioContext/addEventListener —— 时间随机经 boot.ctx,定时经 timer-scheduler,音效经 plugins.audioMusic;**输入不调 ctx.getInput**——写实例方法 `handleTap(x,y)`(点击)/`handleKey(key)`(键盘),L1 在 pointerdown/keydown 时直达调用。
- **插件方法名一律以 `src/plugins/<name>/api.d.ts` 为准**,别臆测(scene-fsm 是 `define` 不是 defineScenes)。本骨架已用对的母语写法:`sessionScore.add(n)` 计分、`juice.burst(x,y)` 爆点、`hudUi.drawButton(g,rect,opts)` 返回命中矩形可直接接住判命中。
- **画面读 `viewport.w/h`**,别硬编码 390×844。
- **`_forensicsView().state()` 的解谜契约**:`targets` 每格一项、**只把「当前该点的正确解」标 `occupied:true`**(solve 阶段、未清、符合本关规则;干扰项/已清格/观察窗与锁定中一律 false)——解谜的取证面兼职"求解器",自动验收只点 occupied:true 的格驱动解谜;`score` 随点对/过关单调上升(自动验收据它判真有进展)。**换皮加机制都别动这两个键的语义。**
- **`_forensicsView()` 是可选调试接口**:state() 实时导出盘面/得分/关卡等(targets 标当前正确解,调试时一眼看出解算对没对);W-AXIS-V2 起验收不再读它(验收 = 四门投影 ∧ 测试 agent 真玩)。
## 你写什么 vs 调什么

View File

@ -161,7 +161,7 @@ export function computeTileGeometry(cols, rows, viewW, viewH) {
row: r,
col: c,
size,
x: BOARD_SIDE_MARGIN + c * (size + TILE_GAP) + size / 2, // 中心点(取证 targets 用它)
x: BOARD_SIDE_MARGIN + c * (size + TILE_GAP) + size / 2, // 中心点(命中判定与调试视图共用)
y: top + r * (size + TILE_GAP) + size / 2,
});
}
@ -194,7 +194,7 @@ function forceMismatch(attrs, rule, theme) {
/**
* 生成一关盘面(纯函数):恰好 matches 个格子符合规则其余全不符合保证全找到=过关可判
* 可解性质(改我之前先读):任何改动必须保住每关恰好 N 个解至少 1 个解干扰项绝不撞规则
* 0 解盘面 = 不可解关 = 永无 occupied 目标 = 九门 H (实测翻车点)换主题/维度优先只改 THEME/LEVELS
* 0 解盘面 = 不可解关 = 第一关就无解谁玩都卡死(实测翻车点)换主题/维度优先只改 THEME/LEVELS
* @param {number} levelIdx 关序(0 )
* @param {()=>number} rng 受控随机源(boot.ctx.random)
* @param {number} viewW / viewH 视口

View File

@ -10,16 +10,16 @@
* · 观察窗口 点解:点对 = 加分(连击倍率)+ 粒子 + 音效 + 飘字;点错 = 抖屏 + 断连击 + 短锁定
* · 全部解找齐 = 过关庆祝 下一关(规则维度逐关递进);通全关提前胜利,限时到点结算
* · 卡壳 9s 剩余解脉动软提示(卡壳不出局)
* · _forensicsView targets/score 契约(自动验收靠它驱动你的游戏,**改坏它=九门 H 直接挂**)
* · _forensicsView 调试视图(可选:host.state() 读它做调试观测,不影响验收)
*
* 你按 brief (身份层 · 多样性源,做出不雷同的解谜):
* · 换主题/元素/文案 core.js THEME(宝石水果/古文字/海洋)
* · 调难度/关卡阶梯 core.js LEVELS(盘面大小/解数/规则维度)与节奏常量
* · 换观感(底景/元素画法/配色) render.js
* · 想加机制(星级/图鉴/每日谜题/新规则维度) 在循环里加,**破坏 targets/score 契约**
* · 想加机制(星级/图鉴/每日谜题/新规则维度) 在循环里加,**丢盘面可解性质**( core.js generateBoard )
*
* 入参(L1 已摊平):createGame({ plugins, bundle, viewport });受控面 ctx init(boot) boot.ctx
* 返回 GameInstance:init/update/render/destroy + handleTap + _forensicsView
* 返回 GameInstance:init/update/render/destroy + handleTap(+可选 _forensicsView)
*
*/
@ -293,12 +293,9 @@ export function createGame({ plugins, bundle, viewport }) {
},
/**
* 取证视图自动验收契约·改坏=九门挂:state() **实时**(绝不外层先算好闭包返回)
* · targets:每格一项,**occupied=true 只标当前该点的正确解**(solve 阶段未清符合本关规则非锁定)
* 解谜品类的取证面兼职"求解器",自动验收只点 occupied:true 的格驱动解谜推进
* **键名固定为 `targets`,不得改成 items/objects **:判卷驱动器(C5) state 有无 `targets` 键选驱动器族,改名=选错族=盲驱动点不到=H 门挂
* · score:随点对/过关单调上升自动验收据它判真有进展
* 加机制换主题都别动这两个键的语义(干扰项/已清格/观察窗内一律 occupied:false)
* 取证视图(可选调试接口):host.state() 据此读盘面/得分/关卡等,供人工调试观测(targets
* occupied 标出当前正确解,调试时一眼能看出解算对没算对)W-AXIS-V2 起验收不再读它
* (验收 = 四门投影 测试 agent 真玩);state() 内实时算是好习惯(外层闭包会定格成 boot 快照)
*/
_forensicsView() {
return {

View File

@ -78,7 +78,7 @@ test('无浏览器全链路:boot→起局→观察窗挡点→点解加分→逐
assert.ok(F, '取证全局 __gameForensics 应已挂载');
assert.equal(host.mode, 'evidence');
// ① 初始态:menu;targets 键在场(自动验收据键存在性判 tap-targets 族)且为空数组。
// ① 初始态:menu;调试视图 targets 键在场且为空数组(菜单期无可点解,导出形态自洽)
assert.equal(F.state().phase, 'menu', 'boot 后应在 menu 场景');
assert.deepEqual(F.state().targets, [], 'menu 期 targets 应为空数组(键必须在)');

View File

@ -1,11 +1,11 @@
# _template-shop — A-model「经营点客」品类黄金骨架(3 层架构)
一个**可玩的完整经营游戏**:顾客陆续上门排队 → 点击在等待的顾客即上菜收钱 → 顾客有耐心条耗尽离开 → 60 秒限时营业结算(menu→play→over)。这份骨架**已经把整套经营循环接好了**(含让自动验收能跑通的取证契约)——你的活是按 brief **换皮**,不是重写循环。
一个**可玩的完整经营游戏**:顾客陆续上门排队 → 点击在等待的顾客即上菜收钱 → 顾客有耐心条耗尽离开 → 60 秒限时营业结算(menu→play→over)。这份骨架**已经把整套经营循环接好了**(含可选的调试视图)——你的活是按 brief **换皮**,不是重写循环。
**怎么改 = 给这家店换身份,循环结构别动:**
- 换主题/菜单/价格/难度 → 改 `src/core.js` 顶部的「多样性参数空间」(`DISH_MENU`、生成间隔、耐心、桌数)。咖啡店写拿铁/美式,书店写小说/绘本——**同骨架做出不雷同的店**就靠这里。
- 换观感(店内布景/顾客长相/配色)→ 改 `src/render.js`
- 想加机制(连击/VIP 客/上错菜扣分)→ 在 `src/game-logic.js` 循环里加,但**别破坏 `_forensicsView``targets`/`score` 契约**(改坏=九门 H 进展门直接挂)
- 想加机制(连击/VIP 客/上错菜扣分)→ 在 `src/game-logic.js` 循环里加。
- **别推倒重写整个循环**:顾客生成/桌位/耐心/上菜收钱/限时结算都接好了,从它起步只补身份,远比从零写省。
## 3 层 + 你只碰 L3
@ -29,7 +29,7 @@ export function createGame({ plugins, bundle, viewport }) {
render(g) { /* 画在 g;读 viewport.w/h */ },
handleTap(x, y) { /* ★输入入口:各 phase 点击逻辑;L1 在 pointerdown 时直达调用(别自己订阅输入) */ },
destroy() {},
_forensicsView() { return { state(){ /* phase/score/targets 实时算 */ }, measures(){} }; }, // ★经营档自动验收契约
_forensicsView() { return { state(){ /* phase/score/targets 实时算 */ }, measures(){} }; }, // 可选调试接口(验收不读)
};
}
```
@ -42,7 +42,7 @@ export function createGame({ plugins, bundle, viewport }) {
- **零引擎 import**(引擎经 `boot.ctx.getEngine()`);游戏源**零裸** Math.random/Date.now/setTimeout/new AudioContext/addEventListener —— 时间随机经 boot.ctx,定时经 timer-scheduler,音效经 plugins.audioMusic;**输入不调 ctx.getInput**——写实例方法 `handleTap(x,y)`(点击)/`handleKey(key)`(键盘),L1 在 pointerdown/keydown 时直达调用。
- **插件方法名一律以 `src/plugins/<name>/api.d.ts` 为准**,别臆测(scene-fsm 是 `define` 不是 defineScenes)。本骨架已用对的母语写法:`sessionScore.add(n)` 计分、`juice.burst(x,y)` 爆点、`hudUi.drawButton(g,rect,opts)` 返回命中矩形可直接接住判命中。
- **画面读 `viewport.w/h`**,别硬编码 390×844。
- **`_forensicsView().state()` 的经营契约**:`targets` 列每桌一项、有可点等待顾客的桌 `occupied:true`(自动验收只点这些桌驱动上菜);`score` 随上菜单调上升(自动验收据它判真有进展)。**换皮加机制都别动这两个键的语义。**
- **`_forensicsView()` 是可选调试接口**:state() 实时导出桌位/营收/耐心等,供人工调试观测;W-AXIS-V2 起验收不再读它(验收 = 四门投影 ∧ 测试 agent 真玩)。
## 你写什么 vs 调什么

View File

@ -9,15 +9,15 @@
* · 顾客陆续生成坐进桌位有耐心条 耐心耗尽自动离开(算流失)
* · 点击在等待的顾客 = 上菜:收钱(sessionScore.add)+ 粒子(juice.burst)+ 音效 + 飘字
* · 60 秒限时营业 到点结算(sceneFsm over,latch 终态)
* · _forensicsView targets/score 契约(自动验收靠它驱动你的游戏,**改坏它=九门 H 直接挂**)
* · _forensicsView 调试视图(可选:host.state() 读它做调试观测,不影响验收)
*
* 你按 brief (身份层 · 多样性源,做出不雷同的店):
* · 主题/菜单/价格/难度 core.js 顶部的多样性参数空间(DISH_MENU生成间隔耐心桌数)
* · 画面观感(店内长啥样顾客长啥样配色) render.js
* · 想加机制(连击 comboVIP 上错菜) 在循环里加,**别破坏 targets/score 契约**
* · 想加机制(连击 comboVIP 上错菜) 在循环里加
*
* 入参(L1 已摊平):createGame({ plugins, bundle, viewport });受控面 ctx init(boot) boot.ctx
* 返回 GameInstance:init/update/render/destroy + handleTap + _forensicsView
* 返回 GameInstance:init/update/render/destroy + handleTap(+可选 _forensicsView)
*
*/
@ -124,7 +124,7 @@ export function createGame({ plugins, bundle, viewport }) {
return;
}
if (cur === SCENE_PLAY) {
// 经营核心:点中任一还在等的顾客 = 给他上菜(1 步成交,自动验收据此涨分)。
// 经营核心:点中任一还在等的顾客 = 给他上菜(1 步成交,单击直给好上手)。
for (let i = 0; i < customers.length; i++) {
const c = customers[i];
if (c.state !== 'waiting' || !isTappable(c, elapsedMs)) continue;
@ -238,11 +238,9 @@ export function createGame({ plugins, bundle, viewport }) {
},
/**
* 取证视图自动验收契约·改坏=九门挂:state() **实时**(绝不外层先算好闭包返回)
* · targets:每桌一项,occupied=true 表示有可点的等待顾客自动验收只点 occupied:true 的桌驱动上菜
* **键名固定为 `targets`,不得改成 items/objects **:判卷驱动器(C5) state 有无 `targets` 键选驱动器族,改名=选错族=盲驱动点不到=H 门挂
* · score:= 营收,随上菜单调上升自动验收据它判真有进展
* 加机制时这两个键的语义务必保持(targets 列可点目标score 随进展升)
* 取证视图(可选调试接口):host.state() 据此读桌位/营收/耐心等,供人工调试观测
* W-AXIS-V2 起验收不再读它(验收 = 四门投影 测试 agent 真玩);state() 内实时算是好习惯
* (外层闭包会定格成 boot 快照,调试时读到的恒是旧值)
*/
_forensicsView() {
return {

View File

@ -1,11 +1,11 @@
# _template-story — A-model「剧情互动」品类黄金骨架(3 层架构)
一个**可玩的完整分支剧情游戏**:读场景文本 → 在 2~3 个选项里做选择 → 属性随选择积累 → 剧情沿分支推进 → 走到某个结局结算(多结局 + 结局图鉴,menu→play→over)。这份骨架**已经把整套剧情推进循环接好了**(含让自动验收能跑通的取证契约)——你的活是按 brief **换故事**,不是重写推进机。
一个**可玩的完整分支剧情游戏**:读场景文本 → 在 2~3 个选项里做选择 → 属性随选择积累 → 剧情沿分支推进 → 走到某个结局结算(多结局 + 结局图鉴,menu→play→over)。这份骨架**已经把整套剧情推进循环接好了**(含可选的调试视图)——你的活是按 brief **换故事**,不是重写推进机。
**怎么改 = 给这段剧情换个故事,推进结构别动:**
- 换题材/文案/分支/结局/属性轴 → 改 `src/core.js` 顶部的「多样性参数空间」(`STORY_NODES``STORY_TITLE``STAT_NAMES`)。古宅悬疑写勇气/机智,恋爱线写好感/理智,武侠写侠义/内力——**同骨架讲出不雷同的故事**就靠这里。
- 换观感(氛围底/配色/文本卡样式)→ 改 `src/render.js`
- 想加机制(属性检定开选项/线索收集/隐藏结局)→ 在 `src/game-logic.js` 推进机里加,但**别破坏 `_forensicsView``targets`/`score` 契约**(改坏=九门 H 进展门直接挂)
- 想加机制(属性检定开选项/线索收集/隐藏结局)→ 在 `src/game-logic.js` 推进机里加。
- **别推倒重写推进机**:节点推进/属性结算/多结局 latch/结局图鉴持久都接好了,从它起步只换故事,远比从零写省。
**写故事图(core.js `STORY_NODES`)三铁律**:① 每个 `next` 必须真存在(死链=推进卡死);② **无回环**(任意路径有限步必达某结局);③ 结局 ≥3 且 outcome 有 win 有 lose。文本红线:节点 text ≤3 短句、选项 label ≤10 字(手机竖屏装得下)。
@ -31,7 +31,7 @@ export function createGame({ plugins, bundle, viewport }) {
render(g) { /* 画在 g;读 viewport.w/h */ },
handleTap(x, y) { /* ★输入入口:各 phase 点击逻辑;L1 在 pointerdown 时直达调用(别自己订阅输入) */ },
destroy() {},
_forensicsView() { return { state(){ /* phase/score/targets 实时算 */ }, measures(){} }; }, // ★剧情档自动验收契约
_forensicsView() { return { state(){ /* phase/score/targets 实时算 */ }, measures(){} }; }, // 可选调试接口(验收不读)
};
}
```
@ -44,9 +44,9 @@ export function createGame({ plugins, bundle, viewport }) {
- **零引擎 import**(引擎经 `boot.ctx.getEngine()`);游戏源**零裸** Math.random/Date.now/setTimeout/new AudioContext/addEventListener —— 时间随机经 boot.ctx,定时经 timer-scheduler,音效经 plugins.audioMusic;**输入不调 ctx.getInput**——写实例方法 `handleTap(x,y)`(点击)/`handleKey(key)`(键盘),L1 在 pointerdown/keydown 时直达调用。
- **插件方法名一律以 `src/plugins/<name>/api.d.ts` 为准**,别臆测(scene-fsm 是 `define` 不是 defineScenes)。本骨架已用对的母语写法:`sessionScore.add(n)` 计分、`sessionScore.win()/lose()` 锁结局、`juice.burst(x,y)` 爆点、`hudUi.drawButton(g,rect,opts)` 返回命中矩形可直接接住判命中、`save.get/set` 存结局图鉴。
- **画面读 `viewport.w/h`**,别硬编码 390×844。
- **`_forensicsView().state()` 的剧情契约**:`targets` 在 play 态列**当前每个选项**一项(中心坐标、`occupied:true` 可点——自动验收只点这些选项驱动剧情);menu/over 态返回**空数组**(键保留、不误点);`score` 随每次选择单调 +1(自动验收据它判真有进展)。**换故事加机制都别动这两个键的语义。**
- **选项命中区 = 绘制区 = 取证 targets 三者同源**:全部经 `core.layoutChoices(vw,vh,n)` 一处算(render 照 rects 画、handleTap 用 hitChoice 判、取证取 rects 中心)。别在 render 里自己另算按钮位置——一旦分叉,自动验收点的坐标就不再是真命中区
- **场景名钉死 `menu`/`play`/`over`**:自动验收认这三个名(起局扫点/进展判定/latch 终态都按它);**结局到 over 后驻留(latch),绝不自动重开**——「再读一次」只在玩家点按钮时触发。
- **`_forensicsView()` 是可选调试接口**:state() 实时导出当前节点/选项/属性等,供人工调试观测;W-AXIS-V2 起验收不再读它(验收 = 四门投影 ∧ 测试 agent 真玩)。
- **选项命中区 = 绘制区 同源**:全部经 `core.layoutChoices(vw,vh,n)` 一处算(render 照 rects 画、handleTap 用 hitChoice 判)。别在 render 里自己另算按钮位置——一旦分叉,玩家点中画出来的按钮却没反应
- **场景名约定 `menu`/`play`/`over`**(全品类统一结构);**结局到 over 后驻留(latch),绝不自动重开**——「再读一次」只在玩家点按钮时触发。
## 你写什么 vs 调什么

View File

@ -96,7 +96,7 @@ export const STORY_NODES = {
},
};
/* ── 选项按钮布局常量(几何身份:想换排版改这里;命中区与取证 targets 共用同一布局)── */
/* ── 选项按钮布局常量(几何身份:想换排版改这里;命中区与绘制共用同一布局)── */
export const CHOICE_W_RATIO = 0.8; // 按钮宽 = 视口宽 × 此比例
export const CHOICE_H = 62; // 按钮高(手指友好,别小于 ~48)
export const CHOICE_GAP = 16; // 按钮纵向间距
@ -125,8 +125,8 @@ export function allEndingIds(nodes) {
/**
* 算当前节点选项按钮矩形( viewport,不硬编码 390×844)
* 命中判定(handleTap)取证 targets(_forensicsView)都用本函数**同一布局单一来源,
* 保证验收 driver 点的坐标恒等于真实命中区**
* 命中判定(handleTap)绘制(render)都用本函数**同一布局单一来源,
* 保证画在屏上的按钮恒等于真实命中区**(两处各算一份迟早漂移,点中画出来的按钮却没反应)
* @param {number} viewW 视口宽
* @param {number} viewH 视口高
* @param {number} count 选项数

View File

@ -9,15 +9,15 @@
* · 节点推进机:读场景文本 点选项 属性结算 + 计分 + 音效粒子 进下一节点
* · 多结局结算:走到结局节点 win/lose 锁定 over 结算屏(latch 终态,不自动重开)
* · 结局图鉴:已解锁结局经 save 跨局持久,over 屏露出x/N + ???的重玩钩子
* · _forensicsView targets/score 契约(自动验收靠它驱动你的游戏,**改坏它=九门 H 直接挂**)
* · _forensicsView 调试视图(可选:host.state() 读它做调试观测,不影响验收)
*
* 你按 brief (故事身份层 · 多样性源,做出不雷同的故事):
* · 换题材/文案/分支/结局 core.js 顶部的 STORY_NODES / STORY_TITLE / STAT_NAMES
* · 换观感(氛围底/配色/文本卡样式) render.js
* · 想加机制(属性检定开选项/线索收集/隐藏结局) 在推进机里加,**别破坏 targets/score 契约**
* · 想加机制(属性检定开选项/线索收集/隐藏结局) 在推进机里加
*
* 入参(L1 已摊平):createGame({ plugins, bundle, viewport });受控面 ctx init(boot) boot.ctx
* 返回 GameInstance:init/update/render/destroy + handleTap + _forensicsView
* 返回 GameInstance:init/update/render/destroy + handleTap(+可选 _forensicsView)
*
*/
@ -34,7 +34,7 @@ import { renderMenu, renderPlay, renderOver, menuStartRect, overRestartRect } fr
// 资产装载(本骨架无外部美术,容错返空集;要加美术放 assets/manifest.json 再读)。
import { loadAssets } from './assets.js';
/* ── 场景名(剧情三幕:引子 → 阅读 → 结局)。钉死 menu/play/over:自动验收认这三个名。── */
/* ── 场景名(剧情三幕:引子 → 阅读 → 结局)。约定 menu/play/over 三态命名(全品类统一结构)。── */
const SCENE_MENU = 'menu';
const SCENE_PLAY = 'play';
const SCENE_OVER = 'over';
@ -153,7 +153,7 @@ export function createGame({ plugins, bundle, viewport }) {
return;
}
if (cur === SCENE_PLAY) {
// 剧情核心:单击选项即推进(1 步一选,自动验收据此涨分)。
// 剧情核心:单击选项即推进(1 步一选,点了就走、读者的手不被流程绊住)。
const idx = hitChoice(x, y, choiceRects);
if (idx >= 0) choose(idx);
}
@ -224,12 +224,9 @@ export function createGame({ plugins, bundle, viewport }) {
},
/**
* 取证视图自动验收契约·改坏=九门挂:state() **实时**(绝不外层先算好闭包返回)
* · targets:play = 当前每个选项一项(中心坐标,occupied=true 可点)自动验收只点
* occupied:true 的选项驱动剧情推进;menu/over = 空数组(键保留,值为空 driver 不误点)
* **键名固定为 `targets`,不得改成 items/choices **:判卷驱动器(C5) state 有无 `targets` 键选驱动器族,改名=选错族=盲驱动点不到=H 门挂
* · score:= 已做选择数,随推进单调上升自动验收据它判真有进展
* 加机制时这两个键的语义务必保持(targets 列可点选项score 随推进升)
* 取证视图(可选调试接口):host.state() 据此读当前节点/选项/属性等,供人工调试观测
* W-AXIS-V2 起验收不再读它(验收 = 四门投影 测试 agent 真玩);state() 内实时算是好习惯
* (外层闭包会定格成 boot 快照,调试时读到的恒是旧值)
*/
_forensicsView() {
return {

View File

@ -1,11 +1,11 @@
# _template-trpg — A-model「掷骰爬塔」TRPG 品类黄金骨架(3 层架构)
一个**可玩的完整掷骰冒险游戏**:勇者限时闯一座试炼塔,每层翻出几张遭遇卡(怪物/宝箱/奇遇,难度与奖励亮牌)→ 点一张卡 = 掷 d20 加等级加成对抗难度 → 大成功/成功/失败三档结算(战利品金币、经验升级、怪物卡失败扣血)→ 清层登上一层、难度渐升 → 登顶 win / HP 耗尽 lose / 60 秒到点结算(menu→play→over)。这份骨架**已经把整套掷骰爬塔循环接好了**(含让自动验收能跑通的取证契约)——你的活是按 brief **换皮**,不是重写循环。
一个**可玩的完整掷骰冒险游戏**:勇者限时闯一座试炼塔,每层翻出几张遭遇卡(怪物/宝箱/奇遇,难度与奖励亮牌)→ 点一张卡 = 掷 d20 加等级加成对抗难度 → 大成功/成功/失败三档结算(战利品金币、经验升级、怪物卡失败扣血)→ 清层登上一层、难度渐升 → 登顶 win / HP 耗尽 lose / 60 秒到点结算(menu→play→over)。这份骨架**已经把整套掷骰爬塔循环接好了**(含可选的调试视图)——你的活是按 brief **换皮**,不是重写循环。
**怎么改 = 给这场冒险换题材,循环结构别动:**
- 换题材/遭遇/数值 → 改 `src/core.js` 顶部的「多样性参数空间」(`ENCOUNTER_DECK` 遭遇表、层数、血量、难度曲线、经验曲线)。魔塔写史莱姆/骷髅卫,西游写小妖/山神庙,星际写巡逻机甲/补给舱——**同骨架做出不雷同的冒险**就靠这里。
- 换观感(塔长啥样/卡面画法/配色)→ 改 `src/render.js`
- 想加机制(道具/技能/事件链)→ 在 `src/game-logic.js` 循环里加,但**别破坏 `_forensicsView``targets`/`score` 契约**(改坏=九门 H 进展门直接挂)
- 想加机制(道具/技能/事件链)→ 在 `src/game-logic.js` 循环里加。
- **别推倒重写整个循环**:翻层/掷骰判定/三档结算/升级/连胜/登塔都接好了,从它起步只补身份,远比从零写省。
## 3 层 + 你只碰 L3
@ -29,7 +29,7 @@ export function createGame({ plugins, bundle, viewport }) {
render(g) { /* 画在 g;读 viewport.w/h */ },
handleTap(x, y) { /* ★输入入口:各 phase 点击逻辑;L1 在 pointerdown 时直达调用(别自己订阅输入) */ },
destroy() {},
_forensicsView() { return { state(){ /* phase/score/targets 实时算 */ }, measures(){} }; }, // ★掷骰档自动验收契约
_forensicsView() { return { state(){ /* phase/score/targets 实时算 */ }, measures(){} }; }, // 可选调试接口(验收不读)
};
}
```
@ -42,7 +42,7 @@ export function createGame({ plugins, bundle, viewport }) {
- **零引擎 import**(引擎经 `boot.ctx.getEngine()`);游戏源**零裸** Math.random/Date.now/setTimeout/new AudioContext/addEventListener —— 时间随机经 boot.ctx,定时经 timer-scheduler,音效经 plugins.audioMusic;**输入不调 ctx.getInput**——写实例方法 `handleTap(x,y)`(点击)/`handleKey(key)`(键盘),L1 在 pointerdown/keydown 时直达调用。
- **插件方法名一律以 `src/plugins/<name>/api.d.ts` 为准**,别臆测(scene-fsm 是 `define` 不是 defineScenes)。本骨架已用对的母语写法:`sessionScore.add(n)` 计分、`juice.burst(x,y)` 爆点、`hudUi.drawButton(g,rect,opts)` 返回命中矩形可直接接住判命中。
- **画面读 `viewport.w/h`**,别硬编码 390×844。
- **`_forensicsView().state()` 的掷骰档契约**:`targets` 列本层每张遭遇卡一项、可点(ready)的卡 `occupied:true`(自动验收只点这些卡驱动掷骰);`score` = 金币战利品,随结算单调上升——**失败也有安慰 1 金**,自动验收据它判真有进展。**换皮加机制都别动这两个键的语义。**
- **`_forensicsView()` 是可选调试接口**:state() 实时导出层数/血量/金币/卡态等,供人工调试观测;W-AXIS-V2 起验收不再读它(验收 = 四门投影 ∧ 测试 agent 真玩)。**失败也有安慰 1 金**(保底正反馈,连败不空手)。
- **掷骰过程可见**:判定式(d20:骰值+加成 vs 难度 → 结果)要飘字亮给玩家——绝不黑箱出结果,「看得见的随机」是 TRPG 手感之根。
## 你写什么 vs 调什么

View File

@ -142,8 +142,8 @@ export function buildFloor(floor, rng, viewW, viewH, idSeq) {
/**
* 结算一次遭遇(纯判定,不带副作用):掷骰 + 等级加成 对抗 卡难度
* 三档:roll=20 大成功(奖励双倍) / 总值难度 成功(全奖) / 否则失败(安慰 1 金币,怪物卡扣血)
* **两层奖励(设计钉死)**:任何结算至少 +1 金币 自动验收盲点也能过进展门;
* 会看难度挑卡吃连胜的真人拿的是好几倍技巧分不挡门门不封技巧
* **两层奖励(设计钉死)**:任何结算至少 +1 金币(保底正反馈,新手连败也不至于零收获弃游);
* 会看难度挑卡吃连胜的真人拿的是好几倍保底给上手缓冲倍率给技巧深度
* @param {object} card 遭遇卡
* @param {{level:number}} hero 勇者(等级给掷骰加成:+level-1每级+1,1 级加成 +0 heroBonus)
* @param {number} roll d20 结果(1..20)
@ -168,7 +168,7 @@ export function resolveEncounter(card, hero, roll, streak) {
return {
outcome: 'fail',
total,
gold: 1, // 安慰分:保证 score 单调上升(自动验收的进展契约),真人的差距拉在成功倍率上
gold: 1, // 安慰分:失败也有正反馈(保底不空手),真人的差距拉在成功倍率上
xp: 1,
hpDelta: card.kind === 'monster' ? -FAIL_HP_LOSS : 0, // 只有怪物卡打脸;宝箱/奇遇失败只是拿得少
};

View File

@ -11,15 +11,15 @@
* (金币 sessionScore.add + 掷骰飘字 + juice.burst + 音效;失败怪物卡扣血)
* · 经验升级(升级 掷骰加成 +1,成长真实进判定公式)+ 连胜追加金币(技巧分)
* · 清层登上一层(难度渐升层间横幅) 登顶 win / HP 耗尽 lose / 60 秒到点结算
* · _forensicsView targets/score 契约(自动验收靠它驱动你的游戏,**改坏它=九门 H 直接挂**)
* · _forensicsView 调试视图(可选:host.state() 读它做调试观测,不影响验收)
*
* 你按 brief (身份层 · 多样性源,做出不雷同的冒险):
* · 题材/遭遇/数值 core.js 顶部的多样性参数空间(ENCOUNTER_DECK层数血量难度曲线)
* · 画面观感(塔长啥样卡面画法配色) render.js
* · 想加机制(道具/技能/事件链) 在循环里加,**别破坏 targets/score 契约**
* · 想加机制(道具/技能/事件链) 在循环里加
*
* 入参(L1 已摊平):createGame({ plugins, bundle, viewport });受控面 ctx init(boot) boot.ctx
* 返回 GameInstance:init/update/render/destroy + handleTap + _forensicsView
* 返回 GameInstance:init/update/render/destroy + handleTap(+可选 _forensicsView)
*
*/
@ -144,7 +144,7 @@ export function createGame({ plugins, bundle, viewport }) {
return;
}
if (cur === SCENE_PLAY) {
// TRPG 核心:点中任一可点遭遇卡 = 掷骰结算(1 步完成,自动验收据此涨分)。
// TRPG 核心:点中任一可点遭遇卡 = 掷骰结算(1 步完成,爽点在挑哪张、不在按菜单)。
for (let i = 0; i < cards.length; i++) {
const c = cards[i];
if (!cardTappable(c)) continue;
@ -160,7 +160,7 @@ export function createGame({ plugins, bundle, viewport }) {
const r = resolveEncounter(card, hero, roll, streak);
card.state = 'resolved';
// ① 金币入账(两层奖励:失败也有安慰 1 金 → score 单调上升,自动验收的进展契约)。
// ① 金币入账(两层奖励:失败也有安慰 1 金,保底正反馈——连败玩家不至于零收获弃游)。
gold += r.gold;
sessionScore.add(r.gold);
@ -289,11 +289,9 @@ export function createGame({ plugins, bundle, viewport }) {
},
/**
* 取证视图自动验收契约·改坏=九门挂:state() **实时**(绝不外层先算好闭包返回)
* · targets:本层每张卡一项,occupied=true 表示可点的遭遇卡自动验收只点 occupied:true 的卡驱动掷骰
* **键名固定为 `targets`,不得改成 items/cards **:判卷驱动器(C5) state 有无 `targets` 键选驱动器族,改名=选错族=盲驱动点不到=H 门挂
* · score:= 金币战利品,随结算单调上升(失败也有安慰分)自动验收据它判真有进展
* 加机制时这两个键的语义务必保持(targets 列可点目标score 随进展升)
* 取证视图(可选调试接口):host.state() 据此读层数/血量/金币/卡态等,供人工调试观测
* W-AXIS-V2 起验收不再读它(验收 = 四门投影 测试 agent 真玩);state() 内实时算是好习惯
* (外层闭包会定格成 boot 快照,调试时读到的恒是旧值)
*/
_forensicsView() {
return {

View File

@ -87,12 +87,12 @@ test('resolveEncounter:总值≥难度成功(全奖)、roll=20 大成功(双倍)
assert.equal(capped.gold, 6 * 2 + STREAK_BONUS_CAP, `连胜追加应被 cap 在 ${STREAK_BONUS_CAP}`);
});
test('resolveEncounter:失败三性质——安慰 1 金(score 单调契约)、怪物卡扣血、宝箱/奇遇失败不伤人', () => {
test('resolveEncounter:失败三性质——安慰 1 金(保底正反馈)、怪物卡扣血、宝箱/奇遇失败不伤人', () => {
const hero = { level: 1 };
const monster = { kind: 'monster', difficulty: 15, gold: 6, xp: 3 };
const f = resolveEncounter(monster, hero, 3, 4);
assert.equal(f.outcome, 'fail');
assert.equal(f.gold, 1, '失败也有安慰 1 金——自动验收的 score 单调契约');
assert.equal(f.gold, 1, '失败也有安慰 1 金——保底正反馈,连败不空手');
assert.equal(f.hpDelta, -FAIL_HP_LOSS, '怪物卡失败应扣血');
const chest = resolveEncounter({ kind: 'chest', difficulty: 15, gold: 3, xp: 1 }, hero, 3, 0);
assert.equal(chest.hpDelta, 0, '宝箱失败只是拿得少,不伤人');

View File

@ -170,11 +170,9 @@ export function createGame({ plugins, bundle, viewport }) {
},
/**
* 取证视图(可选·建议):host.state()/onReady/九门据此读状态 + 度量(JSON 安全)
* targets 键契约·C5点目标玩法(点客/打地鼠/点离散目标)必须在 state() 里暴露 `targets` 数组
* [{x,y,occupied}]**键名固定为 `targets`,不得改成 items/objects **:判卷驱动器靠 state 有无
* `targets` 键选驱动器族(点目标族 vs 循环按键族),改名=选错族=盲驱动器点不到你的目标=H 进展门挂
* 本款是单目标点击,暴露 `target`(单数,同被驱动器识别);多目标一律用 `targets` 数组
* 取证视图(可选调试接口):host.state() 据此读状态 + 度量(JSON 安全),供人工调试观测
* W-AXIS-V2 起验收不再读它验收 = 四门机械投影(装载/异常//渲染) 测试 agent 视觉引导真玩;
* 保留实现是给调试与日志一个统一出口,写不写导出什么键都不影响过门
*/
_forensicsView() {
return {

View File

@ -17,7 +17,7 @@
import { writeFileSync, readFileSync, mkdirSync, cpSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url'; // 入口守卫用:判定本模块是否被「直接执行」(见文件末尾)
import { scaffold, scaffoldSaa, readFileTool, writeFileTool, listDirTool, check, build, buildSaa, stage, gameDir, ensurePlaySpec } from './tools.mjs';
import { scaffold, scaffoldSaa, readFileTool, writeFileTool, listDirTool, check, build, buildSaa, stage, gameDir } from './tools.mjs';
import { chat, MODEL } from './m3.mjs';
import { TOOLS, buildSystemPrompt, DESIGN_TOOLS, buildDesignSystemPrompt } from './prompt.mjs';
import { play, smokeBoot } from './play.mjs';
@ -251,11 +251,9 @@ async function main() {
if (!lastSmoke.ok) { gate = false; gateMsg += ` smoke=FAIL(booted=${lastSmoke.booted} framesAdvanced=${lastSmoke.framesAdvanced} err=${lastSmoke.bootErr || '-'})`; }
else {
gateMsg += ' stage=OK smoke=PASS';
// 链路真绿(终结假绿):据 smoke 抓到的 _forensicsView().state() 形态自动产 play-spec(缺失才补),
// 让下游九门 driven=true → E_live/H_progress 由 advisory 升为致命(纯 harness 裸跑不再假绿)。
const ps = ensurePlaySpec(id, lastSmoke.state || null);
if (ps.wrote) rec(`done 门:自动产 play-spec(driver=${ps.driverType};state.targets=${Array.isArray(lastSmoke.state && lastSmoke.state.targets) ? '有→tap-targets' : '无→key-cycle'})`);
else rec(`done 门:play-spec ${ps.ok ? '已存在不覆盖' : '产失败:' + ps.reason}`);
// W-AXIS-V2 波2:play-spec 自动产已退役——便宜档验收权威改为四门投影(A/B/C/D)∧ 测试 agent 真玩,
// 不再靠 tap-targets 驱动器把九门驱成 driven;下游九门以 undriven 硬集(A/B/C/D + F/G/I)跑,
// floor 投影只取 A/B/C/D。ensurePlaySpec 实现在 tools.mjs 保留一个版本窗口(仅封存工具复跑用)。
}
}
}

View File

@ -11,7 +11,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { _shapeGateErrors } from '../tools.mjs';
import { _shapeGateErrors, _pluginCallPresenceErrors } from '../tools.mjs';
/** 是否报了形状门错误。 */
const hasShape = (errs) => errs.some((e) => e.startsWith('形状门'));
@ -116,3 +116,36 @@ test('零误杀:非插件键的同名 void 方法(自定义对象 reset)不误
const errs = _shapeGateErrors(wrap(`const r = myThing.reset();`), 'game-logic.js');
assert.equal(hasShape(errs), false);
});
// ───────── 插件接线存在断言(W-AXIS-V2 波2,接替 F_wiring 的真接线护栏)─────────
test('插件接线:解构形态调用(sceneFsm.define)→ 通过', () => {
const errs = _pluginCallPresenceErrors({ 'game-logic.js': `const { sceneFsm } = plugins; sceneFsm.define('menu', { onEnter(){} });` });
assert.deepEqual(errs, [], '解构后调用插件应算接线');
});
test('插件接线:属性形态调用(plugins.juice.hitStop)→ 通过', () => {
const errs = _pluginCallPresenceErrors({ 'game-logic.js': `plugins.juice.hitStop(80);` });
assert.deepEqual(errs, [], '属性链上的插件键方法调用应算接线');
});
test('插件接线:调用散在 render.js 也算(三文件合并判)', () => {
const errs = _pluginCallPresenceErrors({
'game-logic.js': `export function createGame(){ return { handleTap(){} }; }`,
'render.js': `export function draw(g){ hudUi.drawText(g, 'hi', 0, 0, {}); }`,
});
assert.deepEqual(errs, [], '任一游戏源文件出现插件调用即接线');
});
test('插件接线:零插件调用(徒手裸写)→ catch', () => {
const errs = _pluginCallPresenceErrors({ 'game-logic.js': `export function createGame(){ let s = 0; return { update(dt){ s += dt; }, handleTap(){ s += 1; } }; }` });
assert.equal(errs.length, 1, '零插件调用必须拦');
assert.ok(errs[0].includes('插件接线断言'), '错误应可归类到插件接线断言');
});
test('插件接线:注释/字符串里的插件调用名不算(stripCode 后)→ catch', () => {
const errs = _pluginCallPresenceErrors({ 'game-logic.js': `// 该用 sceneFsm.define('menu',{}) 的
const tip = "juice.hitStop(80)";
export function createGame(){ return { handleTap(){} }; }` });
assert.equal(errs.length, 1, '注释/字符串内的调用名不应算接线');
});

View File

@ -267,6 +267,32 @@ export function _shapeGateErrors(rawSrc, f) {
return errs;
}
/**
* _pluginCallPresenceErrors 源码级插件调用存在断言(W-AXIS-V2 波2,接替 F_wiring 的真接线护栏)
*
* F_wiring 的期望前缀集唯一来源是 play-spec;play-spec/驱动器随取证契约退役后,F 坍缩成任意引擎调用即过,
* 不再证真接线真接线的机械半移交本断言(词法层确定性,不判玩法):游戏源(game-logic/core/render 合并)
* stripCode 去注释字符串后,必须至少出现一次对已注入插件键的方法调用 `<插件键>.<方法>(`( PLUGIN_KEY_DIR
* 11 ;解构形态 `sceneFsm.define(` 与属性形态 `plugins.sceneFsm.define(` 都命中)零命中 = 模型抛开插件库
* 徒手裸写(通常伴随裸 canvas/裸事件,红线另拦)= 未接线,拦下运行时半(音效/计分/表现真实发生)归测试员观察
* 漏报优于误杀:同名局部变量的方法调用会被当成插件调用放行(存在断言方向上安全)
*
* @param {Object<string,string>} sources 文件名原始源码(存在的游戏源文件)
* @returns {string[]} errors(零命中时一条;命中则空)
*/
export function _pluginCallPresenceErrors(sources) {
const keys = Object.keys(PLUGIN_KEY_DIR);
for (const src of Object.values(sources || {})) {
const s = stripCode(String(src || ''));
for (const m of s.matchAll(/\b(\w+)\.(\w+)\s*\(/g)) {
if (keys.includes(m[1])) return []; // 任一文件出现任一插件键的方法调用即算接线
}
}
return [`插件接线断言: 游戏源(${Object.keys(sources || {}).join('/') || 'game-logic.js'})未发现任何插件调用——` +
`L1 已注入全 11 件能力插件(${keys.join('/')}),必须用它们承担场景机/计分/HUD/定时/音效等能力` +
`(如 const { sceneFsm, sessionScore } = plugins; sceneFsm.define('menu',{...})),别抛开插件库徒手裸写`];
}
// ───────────────────────── 词法真实红线判定(W-AXIS 波2)─────────────────────────
// 动机(诊断档 §2.1/§2.4):红线八条与 .boot.ctx/getInput 的命中判定原本是「stripCode 抹字符串/注释 → 对纯文本跑
// 正则」。stripCode 修好后(0be0f61c 单遍状态机)字符串内 // 已不 desync,但「对纯文本跑正则」仍是近似:token 间
@ -487,10 +513,9 @@ export function check(id) {
if (!/\bbundle\.tick\s*\(/.test(g)) {
errors.push('game-logic.js 的 update(dt) 内必须调 bundle.tick(dt)(否则插件 onFrame 不推进、timer 永不到期、一局不结束)');
}
// 可测性红线:必须实现 _forensicsView()(九门据其 state() 验"真活+有进展";不写则 E_live/H_progress 读不到状态)。
if (!/_forensicsView\s*\(/.test(g)) {
errors.push('game-logic.js 必须实现 _forensicsView()(返回 {state(),measures()};state() 导出 phase/score 等可观测态——九门据此验真活+进展,可测性红线)');
}
// _forensicsView「必须实现」拦截已摘(W-AXIS-V2 波2):自动验收契约随 tap-targets 驱动器退役,验收改由
// 测试 agent 视觉引导真玩(看截图、点所见),不再读状态导出。_forensicsView 降级为可选调试接口——写了
// 不拦、不写也不拦;handleTap 输入契约与 ctx.log 日志纪律保留(服务真人与测试员,见下)。
// ctx 契约硬化(治 M3 实测错:把 boot.ctx 写成 boot.boot.ctx → 多套一层 → undefined → ctx=null → 不订阅输入 →
// 游戏点不动、卡菜单;且九门默认未驱动时 G_input skip → 漏判,带病过门)。host 传入的 boot 已是 {ctx,mainContext,canvas,seed,assets},
// 受控面就是 boot.ctx。命中判定走词法(W-AXIS 波2):序列 .boot.ctx(boot.boot.ctx / b.boot.ctx 皆中),正确的 boot.ctx 无前导「.boot」不误伤。
@ -567,6 +592,17 @@ export function check(id) {
for (const e of _shapeGateErrors(readFileSync(abs, 'utf8'), f)) errors.push(e);
}
// 5.5) 插件接线存在断言(W-AXIS-V2 波2,接替 F_wiring 的真接线护栏;词法层确定性,判定见 _pluginCallPresenceErrors):
// 游戏源三文件合并后必须至少调一次已注入插件(零命中=抛开插件库徒手裸写=未接线)。
{
const srcs = {};
for (const f of ['game-logic.js', 'core.js', 'render.js']) {
const abs = join(gd, 'src', f);
if (existsSync(abs)) srcs[f] = readFileSync(abs, 'utf8');
}
for (const e of _pluginCallPresenceErrors(srcs)) errors.push(e);
}
// 6) 未定义标识符 + import 断链静态扫描(4 例实证同族:解谜 gen3 未定义 cy / gen6 未定义 stepUntilMs /
// 非遗首败 import 断链——语法过 check、运行时才炸,九门才拦得住,浪费整局;node --check 只查语法不查引用)。
// esbuild(树内现货)当作用域分析器,判定与漏报口径见 check-undef.mjs;esbuild 不可用时函数自身降级返回空。
@ -782,6 +818,8 @@ export function stage(gameId) {
/**
* ensurePlaySpec 据游戏 _forensicsView().state() 形态自动产 _wg1-gen/<gameId>/play-spec.json(" harness driverdriven=falseE/H advisory绿")
* 退役·保留一个版本窗口(W-AXIS-V2 波2,2026-07-10)gen.mjs done 门已摘除调用便宜档验收权威 =
* 四门投影(A/B/C/D) 测试 agent 真玩,九门以 undriven 硬集跑实现保留仅供封存对照工具历史复跑;窗口期满一并清理
* driver 推断( play.cdp.cjs driver 家族):
* · state targets 数组(点击类:打地鼠/经营点客/点离散目标) tap-targets + targetMode:'occupied'(只点 occupied===true 的目标, phaseplay 时自动 start 起局);
* · 否则(按键类, targets) key-cycle(循环按方向键)

View File

@ -164,7 +164,10 @@ def test_judge_backward_compatible_no_params():
def test_judge_green_no_sidecar(tmp_path):
"""门全绿不产反馈对象、不落 sidecar(C6:门绿就不该有反馈)。"""
v = {"pass": True, "guards": {"A_boot": {"pass": True}}}
# W-AXIS-V2:放行判据 = 四门投影(A/B/C/D 全绿)∧ guards 非空,fixture 给四门全绿。
v = {"pass": True, "guards": {
"A_boot": {"pass": True}, "B_uncaught": {"pass": True},
"C_frame": {"pass": True}, "D_render": {"pass": True}}}
j = judge_cheap_verdict(v, game_id="g10", staged_dir=tmp_path)
assert j.passed is True
assert not (tmp_path / "evidence" / "verdict-feedback.json").exists()

View File

@ -5,12 +5,26 @@ from worker.gate_judge import GateJudgment, judge_cheap_verdict
def test_cheap_green_passes():
# 顶层 pass=True 且各门 pass=True → passed=True、无失败门。
v = {"pass": True, "guards": {"A_boot": {"pass": True}, "H_progress": {"pass": True}}}
# W-AXIS-V2 放行判据 = 四门投影(A_boot/B_uncaught/C_frame/D_render 全绿)∧ guards 非空;
# E/G/H/I/F 降观测不进取值(此处 H 故意 False 以证不影响放行)。
v = {"pass": False, "guards": {
"A_boot": {"pass": True}, "B_uncaught": {"pass": True},
"C_frame": {"pass": True}, "D_render": {"pass": True},
"H_progress": {"pass": False}, # 降观测门失败不拦(undriven 下 H 本就 advisory)
}}
j = judge_cheap_verdict(v)
assert isinstance(j, GateJudgment)
assert j.passed is True
assert j.failed_gates == []
assert j.failed_gates == ["H_progress"] # 诊断列仍如实列出,只是不作放行输入
def test_cheap_floor_gate_missing_not_passed():
# 四门任一缺失(如 D_render 没跑出)→ fail-closed 不放行(无四门证据不放行)。
v = {"pass": True, "guards": {
"A_boot": {"pass": True}, "B_uncaught": {"pass": True}, "C_frame": {"pass": True},
}}
j = judge_cheap_verdict(v)
assert j.passed is False
def test_cheap_pass_but_empty_guards_not_passed():

View File

@ -490,7 +490,7 @@ def judge_cheap_verdict(play_result: dict, *, game_id=None, staged_dir=None) ->
failed_gates = [k for k, g in guards.items() if isinstance(g, dict) and g.get("pass") is False]
if passed:
# 全绿:feedback 只作日志/trace 用(GateJudgment 契约:passed=True 时不回喂),给准确描述。
return GateJudgment(passed=True, failed_gates=failed_gates, feedback="九门全绿,放行。")
return GateJudgment(passed=True, failed_gates=failed_gates, feedback="四门地板绿(A_boot/B_uncaught/C_frame/D_render),放行。")
obj = build_cheap_feedback_obj(v, game_id=game_id, staged_dir=staged_dir)
if obj is not None:
feedback = _render_cheap_feedback(obj)