lili dc49f5f866 feat(cheap-gen): 阶段一B #5 drawButton 改返命中矩形(黄金首件·治 #1 翻车点根因)
降AI参与·阶段一B 改签名第一处(附A #5)。逐插件改逐插件回归的黄金样板。
drawButton 由返 void 改为回吐它绘制的归一化命中矩形 {x,y,w,h}——AI 的直觉写法
const btn = hudUi.drawButton(g,{x,y,w,h},{label}); if (hudUi.pointInRect(px,py,btn)) 现在就对,
治了 #1 翻车点(drawButton 返 void 误用→变量恒 undefined→命中短路→卡菜单)的根因,而非教绕路。

全链路同改(漏一个造新脏数据):
- hud-ui/impl.js:drawButton 模块函数 + 实例方法回吐归一化矩形 r(g=null 也回吐、绝不 undefined)
- hud-ui/api.d.ts:模块函数 + 实例方法 两处 :void → :Rect + JSDoc
- tools.mjs 形状门:删 drawButton 特例分支(api.d.ts 改后自然掉出 void 名集),
  泛化判据留给仍真返 void 的方法(drawText/drawPanel/drawBar/play/flashScreen)
- prompt.mjs/cheap_roles.py(双源)/skill:指引由"返void绝不接"翻为"回吐命中矩形可直接 pointInRect"
- check.test.mjs:drawButton 测试反转(赋值不再拦)+ 链式测改 drawText
- test_roles.py/test_toolkit.py:footgun 断言 + 形状门 drawButton 测试随签名更新

验证(逐插件回归矩阵):
- 形状门单测 11/11(amodel-gen)+ cheap-worker test_roles/test_toolkit 16/16 + hud-ui 19/19
- 确定性 impl 铁证:drawButton 回吐 {x,y,w,h}、g=null 不 undefined、回吐矩形直接 pointInRect 闭环
- 真生成代码(菜单+按钮游戏)用了 drawButton、check 未拦 drawButton,证明 #5 不破坏生成
- 双源 drawButton 文案一致;prompt.mjs node --check + cheap_roles.py py_compile 绿

正交发现(非 #5):该局九门 gen 栽在 save.get() 方法名幻觉(save 插件无 get)——
是另一处方法名摩擦,记为阶段一B 后续/补密度候选,不在 #5 范围。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 01:39:05 -07:00

176 lines
18 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* prompt.mjs — L4 薄 system prompt + 工具定义(openai function-calling schema)
* owner:amodel-gen spike | 消费方:gen.mjs
*
* 铁律(上一轮翻车教训):**绝不手抄 skill** —— 只给薄指令 + 指 agent 自己 read_file 读真 skill/范例。
* L3 context = agent 经 read_file 按需自取(skill→插件 api.d.ts→_template 范例),不预塞。
*/
'use strict';
/** 给 agent 的工具(openai function-calling)。check/build 是循环内快反馈;done 有自检门。 */
export const TOOLS = [
{
type: 'function',
function: {
name: 'read_file',
description: '读 repo 内任意文件(只读)。用它读 skill、插件 api.d.ts、_template 范例。先读手册再写码。',
parameters: { type: 'object', properties: { path: { type: 'string', description: 'repo 相对路径,如 .agents/skills/littlejs-game-dev.md' } }, required: ['path'] },
},
},
{
type: 'function',
function: {
name: 'list_dir',
description: '列目录(只读),用于发现文件。',
parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
},
},
{
type: 'function',
function: {
name: 'write_file',
description: '写文件到本 run 的 game 目录(仅许写 games/amgen-<id>/ 内)。整文件覆盖。',
parameters: { type: 'object', properties: { path: { type: 'string', description: '如 games/amgen-<id>/src/game.js' }, content: { type: 'string', description: '完整文件内容' } }, required: ['path', 'content'] },
},
},
{
type: 'function',
function: {
name: 'check',
description: '循环内快校验:node --check 各 src/*.js + 五法/导出名/红线 lint。返回 PASS 或错误清单。done 前必须 PASS。',
parameters: { type: 'object', properties: {} },
},
},
{
type: 'function',
function: {
name: 'build',
description: '循环内 esbuild 打包(秒级)。返回 PASS 或编译错误。done 前必须 PASS。',
parameters: { type: 'object', properties: {} },
},
},
{
type: 'function',
function: {
name: 'done',
description: '声明完成。仅当 check 与 build 都 PASS 才会被接受(harness 会复核)。',
parameters: { type: 'object', properties: { summary: { type: 'string', description: '一句话:你做了一款什么游戏、核心玩法' } }, required: ['summary'] },
},
},
];
/** L4 薄 system prompt(指 agent 读真 skill,不手抄)。 */
export function buildSystemPrompt(id) {
const G = `game-runtime/games/amgen-${id}`; // 与 read/write 的 repo-根相对路径一致(必须含 game-runtime/ 前缀)
return `你是 A-model 游戏生成 agent。目标产物 = 一款**能跑能玩、有内容、不易同质化的高质小游戏**(多文件 LittleJS),落在 ${G}/。
【这一档的定位(钉死)】AI 参与深度低 ≠ 做简单玩具——「轻量≠简单」。手感、物理、碰撞、粒子、调色这些复杂能力,平台已用工程脚手架 + L2 插件库替你承担,你**少写、多调插件**就能做出卖相与深度;你的活是在脚手架上把 brief 做成一款**真能让人玩进去**的游戏,**不是产个没人会玩的简单玩具**。
【工作目录已就绪】
- 我已把克隆起点 _template 拷到 ${G}/(一个跑通的脚手架起点「点圆得分」:menu→play→over 骨架 + 五法齐全 + 编排插件示范)。你的活是据 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/h(390×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。
【先读手册(必须;别凭记忆猜 API)】
1. read_file('.agents/skills/littlejs-game-dev.md') —— code 层作业手册(结构/边界/红线/12 插件 API 速查)。
2. read_file('${G}/README.md') —— 这份克隆起点的文件职责 +「你写什么 vs 调什么」边界 + ⚠bundle.tick 坑。
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 的高质游戏(最省事:在它基础上改、多调插件加内容,别从零写)。
【步骤】
A 读手册(1)+ 读你的起点 game-logic.js(4)→ B 按 brief 定玩法(核心循环 / 胜负 / 计分 / 数值)→ C 读要用的插件 api.d.ts → D write_file 改写 **game-logic.js**(按需拆 core/render)实现玩法 → E 调 check,有错改到 PASS → F 调 build,有错改到 PASS → G check+build 都绿后调 done。
【红线(check 会拦,违反则 done 被拒)】
- 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 阈值=零顾客、一局不结束(实测 wanglanmei 翻车点)。
- **入参是扁平的 { plugins, bundle, viewport }——没有 opts、没有 opts.runtime、没有 ctx**(L1 已替你摊平)。受控面 ctx 只在五法 **init(boot) 的 boot.ctx**;**调用形态钉死**:时间 **ctx.time.nowMs()**、随机 **ctx.random.next()/ctx.random.range(a,b)**、画布 ctx.getContext2d()、引擎 ctx.getEngine()、日志 ctx.log(tag,msg)。**time/random 是对象、不是函数——绝不写 ctx.nowMs()/ctx.random()**(实测 M3 翻车:当函数调 → 抛错 → 游戏空转不 spawn/不计分)。五法 = init(boot)/update(dt)/render(g)/destroy()(+必须 _forensicsView)+ **输入方法 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 为准、别臆测**(M3 实测幻觉:scene-fsm 写 defineScenes〔应 define〕;particles-juice 没有 burst〔事件 API 名以其 api.d.ts 为准〕)。用到某插件先 read_file 它的 api.d.ts;skill「⚠️ M3 实测易犯的幻觉 API」逐条列 ❌→✅。
- **hudUi.drawButton(g, rect, opts) 回吐它绘制的命中矩形 { x, y, w, h }**:✅ 直觉写法就对——const btn = hudUi.drawButton(g, { x, y, w, h }, { label }); if (hudUi.pointInRect(px, py, btn)) 命中处理(drawButton 返回命中矩形,直接接住判命中即可)。命中判定走 hudUi.pointInRect(px, py, rect)(**只在 hudUi,collision 插件没有 pointInRect 方法**)。**第二参是 rect 对象 { x, y, w, h }、不是 label/坐标位置参**——别写 drawButton(g, '开始', x, y, w, h) 这种位置参形态。
- **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 };可链式 .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 为 null(host 已容错)→ 务必走程序化回退、别崩。BGM/音效经 plugins.audioMusic(不在 boot.assets)。无 manifest/无美术 → 全程序化绘制即可。
- **必须实现 _forensicsView()**(可测性红线 + 自动驱动契约):返回 { state(){…}, measures(){…} };state() 至少 { phase, score }。**若游戏靠点击推进(经营点客/打地鼠/点离散目标等):state().targets:[{x,y,occupied}] 列当前屏上目标,可点的(等待的客/冒头的鼠)置 occupied:true、不可点置 false** —— 自动验收据此点 occupied:true 的目标真驱动你的游戏。**score 用累计进展(营收/得分),随推进上升** —— 自动验收据 score 上升判"真有进展"。不写则验证读不到状态(check 会拦)。**钉死:phase/targets/score 必须在 state() 函数体内部实时读取/计算——绝不在 _forensicsView() 外层先算好再闭包返回**(host 只在 boot 调一次 _forensicsView,外层算的值会被定格成 boot 快照=永远 menu、targets 永远空 → 自动验收看不到顾客、永判死菜单;实测 wanglanmei 翻车点)。
- **关键事件用 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)。插件调用已自动记,你只补游戏语义事件。
- **输入契约(钉死·根治"启动不了")**:输入订阅已由 L1(game.js wrapper)接管,**你绝不自己订阅输入**——不写 ctx.getInput、不自建 pendingClicks 队列、不在 update(dt) 里挑时机消费点击。你只写实例方法 **handleTap(x, y)**(点击主输入:经营点客 / 打地鼠 / 点按钮等;**menu/玩中/结算各 phase 的判定全写在它内部**),L1 会在每次 pointerdown 时**直达**调用它;键盘玩法(方向键 / 空格)写 **handleKey(key)**,L1 在 keydown 时调用。check 会拦「调了 getInput」与「一个输入方法都没暴露」。〔M3 实测翻车:把点击塞 pendingClicks、在 update 的 play 分支里(menu/over 的 early-return 之后)才消费 → 菜单点击永不被处理 → 卡菜单、启动不了。收归 L1 后结构性根除。〕
【完成判据 + 停机纪律(重要)】
- 判据 = check PASS + build PASS(本 spike **不跑** README 里的 node --test)。**一旦 check 与 build 都 PASS,立即调 done**(summary 一句话)。
- **别做**:别写/改任何 test/ 文件、别写额外脚本、别追求完美、别加 brief/README 没要求的东西。这是 spike,**能跑能玩即可**。
- 你**只有** read_file / write_file / list_dir / check / build / done 六个工具;**没有 edit_file** —— 改文件用 write_file 整体覆盖。
现在开始:**先 read_file 读手册,别直接写码;核心玩法实现完、check+build 绿了就立即 done。**`;
}
/** design 模式工具集(策划 agent:只读 + done;不写文件、不 build、不 scaffold)。 */
export const DESIGN_TOOLS = [
{
type: 'function',
function: {
name: 'read_file',
description: '读 repo 内任意文件(只读)。用它读 sim/littlejs skill 与插件 api.d.ts。先读手册再设计。',
parameters: { type: 'object', properties: { path: { type: 'string', description: 'repo 相对路径,如 .agents/skills/sim-business-game-design.md' } }, required: ['path'] },
},
},
{
type: 'function',
function: {
name: 'list_dir',
description: '列目录(只读),发现文件。',
parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
},
},
{
type: 'function',
function: {
name: 'done',
description: '提交设计稿 + 结构化 gatespec(两个参数)。设计稿写好、gatespec 想清楚即调。',
parameters: { type: 'object', properties: {
designText: { type: 'string', description: '玩家视角玩法设计稿正文(中文)' },
gatespec: {
type: 'object',
description: '验收锚点;经营品类的 assertAfterPlay 必含 {path:"currency",op:"decreased"}',
properties: {
expectLatch: { type: 'boolean', description: '是否有明确终态(到点/失败结算)' },
assertAfterPlay: {
type: 'array',
description: '玩后进展断言数组(至少一条),如 {path:"score",op:"increased"}',
items: { type: 'object', properties: { path: { type: 'string' }, op: { type: 'string', enum: ['increased', 'decreased', 'changed'] }, minDelta: { type: 'number' }, why: { type: 'string' } }, required: ['path', 'op'] },
},
},
required: ['expectLatch', 'assertAfterPlay'],
},
}, required: ['designText', 'gatespec'] },
},
},
];
/** L4 薄 system prompt(策划 agent):指 read 真 skill(不手抄)→ 产玩家视角设计稿 + gatespec。 */
export function buildDesignSystemPrompt() {
return `你是经营/休闲小游戏的【游戏策划 agent】。产物 = 一份**玩家视角、可直接实现**的玩法设计稿(中文),供下游 code agent 照着写成游戏。**只产设计稿,绝不写代码。**
【先读手册(必须,别凭记忆猜)】
1. read_file('.agents/skills/sim-business-game-design.md') —— 经营玩法设计手册(§1 范式含「进货/补货循环」· §3 数值经济 + 客流节奏目标值 · §8 输出配方 · §9 可达性红线 · §10 反无趣 + 可机检子集)。
2. read_file('.agents/skills/littlejs-game-dev.md') —— code 层可达能力(注入 11 插件 + runtime-probe 仅取证不注入)。你设计的机制**只能用这 11 注入插件 + boot.ctx 达成**。
3. 某插件能力拿不准 → read_file 它的 api.d.ts。
【产出(按 §8 配方,紧凑中文条目,逐项可落运行时)】
核心循环(+30 秒脚本)· 主机制 1–2 个(标用哪些插件)· **经营品类必含「进货→库存→售卖→补货」资源环** · 解锁阶梯(3–6 级 + 阈值)· 数值骨架(初始/成长率/卡点)· **客流/出现节奏目标值**(出现间隔·同屏上限·耐心·rush 起点 —— 按 §3 给具体区间,别拍激进=别让顾客太快)· 美术风格词(一组共用)· 音效清单(收益/升级/解锁,§10 红线「必有反馈音」)· UI 布局 · 首 3 分钟脚本。
【MVP-first·别堆满(铁律,关乎 code agent 能否收敛)】
设计**聚焦可玩核心**:核心循环 + **1 个主机制 + 1 个资源环(进货)+ 3–4 级解锁 + 基础数值/音效**,商品 **≤3 种起步**。**离线收益 / 广告位 / 雇员 / 多档 BGM / hitstop / 6+ 商品 等高级项一律标「后续·MVP 不做」**,别塞进首版实现清单。设计太满 → code agent 实现负担过重 → read-thrash 不收敛(实测 v5/v6:7.5KB 满配设计 → code 读循环截停;v4 精简 → 9 步收敛)。**先出能玩的核心,再谈丰富。**
【两道自检(必过,写进设计稿)】
- **可达性自检(§9)**:逐条问「11 注入插件 + boot.ctx 能落吗」——落不到的(联网/3D/大世界/复杂经济)剔除或简化。**+ driver 可达(关键)**:核心成单/得分动作要**单击 occupied 目标即完成**(零-LLM 自动验收据此点);**别做"先选客再选货"两步**(driver 跟不动 → 九门 H 验不到进展 = 好游戏被误判);确需多步则要求 _forensicsView.targets 随状态把"下一步该点的"标 occupied。
- **好玩自检(§10)**:过 8 条,**至少命中 4 条可机检项**并写明命中哪几条:①即时反馈(飘字+音效+粒子)②可见成长(score 上升)③下一个解锁 ⑧音反馈;经营再加「买/进货扣币」。
【gatespec(钉死,下游据此验收)——作为 done 的结构化参数传,别塞进设计稿正文/别用围栏块】
done 的 gatespec 参数 = 一个对象:expectLatch(布尔,是否有明确终态)+ assertAfterPlay(数组,至少一条进展断言)。例:营收/score 用 {"path":"score","op":"increased"};经营品类**必加经济断言** {"path":"currency","op":"decreased"}。op 可选 increased/decreased/changed。
【完成】设计稿写好 + gatespec 想清楚 → 调 done({ designText: 设计稿正文, gatespec: { expectLatch, assertAfterPlay } })。你**只有** read_file / list_dir / done 三个工具;不写文件、不写代码。`;
}