feat(amodel-gen): Phase1 设计 agent(--mode design,读真 sim skill)+ API-存在静态门 + sim skill 扩

承 Phase1 执行版(2026-06-23-design-agent-phase1-execution.md)U5/U1/U2/U4。

U5 sim skill 扩:补「进货→库存→售卖→补货」资源环范式(经营核心,治"没进货")+ 客流/出现节奏目标值
  区间(治"顾客太快":清闲 ~2.4s/3/6s、rush 起点 ≥35s)+ §10 标注可机检 N=4(即时反馈/可见成长/解锁/音反馈 + 经营加经济扣减)。

U1+U2 设计 agent:gen.mjs 加 --mode design(tool-using 策划 agent:read_file/list_dir/done,M3,scaffold-free,
  日志走 stderr / stdout 末行 JSON {ok,designText,gatespec})+ prompt.mjs buildDesignSystemPrompt(薄指令指
  **read 真 sim skill 不手抄** + 可达性自检 + 好玩 N-of-8 + 结构化 gatespec)+ normGatespec 容错(M3 偶把
  assertAfterPlay 包成 {item:[]}/expectLatch 写字符串 → 拍平)。
  验:一句话经营 brief → 读 sim skill → 8.5KB 玩家视角设计稿(含进货环 + 节奏目标值表 rush≥5min + 好玩自检)
  + clean gatespec(expectLatch:true / score↑ / currency↓ 经济断言 / inventory changed)。

U4 API-存在静态门(tools.mjs check,玩法无关):游戏调的 plugins.<键>.<m>()/ctx.<m>() 必须在 api.d.ts 真存在
  → 廉价根除"方法名幻觉"类(stopBgm/defineScenes/ctx.nowMs);键→dir 映射(juice→particles-juice 等);
  只查已知插件键+ctx、非键跳过(over-permissive 少误报)。验:_template 零误报 + 抓 stopBgm/ctx.nowMs
  + 不误伤 loop/ctx.log。

待:U3(designNode amodel 路 shell-out,Java/mini)+ 本地 e2e(design→code 验更好玩)+ U6 蒸馏。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-23 10:28:16 -07:00
parent 68472bb560
commit 57c30909cd
4 changed files with 194 additions and 2 deletions

View File

@ -34,6 +34,7 @@
| **合成升级(merge)** | 两个同级合成更高级(更高产出),制造"再合一个"冲动 | core 逻辑 + particles-juice 合成"啪" + hud-ui |
| **阶段解锁阶梯** | 攒够 → 解锁新商品/区域/角色,**永远露出下一个锁** | session-score 阈值 + scene-fsm/状态 + hud-ui 解锁庆祝弹窗 |
| **轻经营决策** | 定价/摆放/升级取舍/雇员——**有选择但不烧脑** | core 逻辑 + hud-ui 面板/按钮 |
| **进货/补货循环**(经营核心资源环) | 花软币**进货**→**库存**→售卖收钱→**缺货补货**;制造"赚→进→卖→再赚"的资源管理张力 | core 库存状态(数量/上限)+ session-score 货币(进货扣、售卖加)+ hud-ui 库存条/补货按钮 + particles-juice 售卖爽感 |
| **收集图鉴** | 凑齐商品/角色/装饰 | save-progress 存收集状态 + hud-ui 图鉴格 |
| **数值成长曲线** | 产出/成本指数成长 + 卡点,制造"差一点" | balance 数值表 |
@ -56,6 +57,7 @@
- **看广告点位(自愿、不打断爽点)**:加速产出 · 双倍离线收益 · 免费解锁/抽奖 · 复活/续命 · 翻倍领取。**包装成"福利"**(金光按钮 + "免费 ×2"),不是惩罚。
- **卡点设计**:让玩家**"差一点就能升级/解锁"**(成本曲线略陡)→ 自然引向看广告或放置等待。**别卡死**(挫败 = 流失);卡点是"诱惑"不是"墙"。
- **成长曲线**:产出/成本指数(如 ×1.15/级),配合放置收益,制造"滚雪球"暴富快感。
- **客流/出现节奏(点客·经营类必给目标值 —— 治"来太快/太慢")**:design 要给**具体区间锚**,别让 code agent 拍脑袋(实测拍出来偏激进 = 顾客太快)。参考:清闲期出现间隔 **~1.82.8s** / 同屏上限 **3** / 顾客耐心 **~57s**;高峰期间隔 **~1.01.5s** / 上限 **4**;**高峰别太早**——一局 60s 的话 rush 起点 **≥35s**(留足清闲铺垫,先松后紧)。这些目标值交 balance.js。
---
@ -147,6 +149,14 @@
> 8 条命中越多越好玩;命中 ≤3 条 ≈ catch-fruit 那种"能玩但无趣"。**设计阶段就过这张表,别等做出来才发现无趣。**
> **可机检子集(N≥4 必过 —— 接好玩门 / API 门 / per-game 断言)**:这 8 条里 **4 条可机检**,design 产出必须覆盖、且 code 必须实现:
> - **①即时反馈** = 主操作处真调 `particles-juice`(飘字/粒子)+ `audioMusic`(音效)→ code-presence 可查;
> - **②可见成长** = `score`/营收随操作上升 → H 门 `assertAfterPlay {path:score,op:increased}`;
> - **③下一个解锁** = 有解锁逻辑 + 阈值常量 → code-presence 可查;
> - **⑧音反馈** = 真调 `audioMusic.playSfx`(或引擎 zzfx)→ code-presence 可查。
>
> **经营/进货品类再加一条 per-game 运行时断言**:买/进货时货币真减 → gatespec 产 `{path:"currency",op:"decreased"}`(经现成 H 门 assertAfterPlay 校验;**注:通用九门不加品类语义,断言是每局喂的**)。其余(情感锚/放置回归/滚雪球)靠真玩判,不机检。
---
## 相关

View File

@ -18,7 +18,7 @@ import { writeFileSync, mkdirSync, cpSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { scaffold, scaffoldSaa, readFileTool, writeFileTool, listDirTool, check, build, buildSaa, stage, gameDir } from './tools.mjs';
import { chat, MODEL } from './m3.mjs';
import { TOOLS, buildSystemPrompt } from './prompt.mjs';
import { TOOLS, buildSystemPrompt, DESIGN_TOOLS, buildDesignSystemPrompt } from './prompt.mjs';
import { play, smokeBoot } from './play.mjs';
function arg(name, def) {
@ -51,8 +51,82 @@ function compactHistory(messages, keepLast = 8) {
}
}
/** 容错归一 gatespec(M3 偶把 assertAfterPlay 包成 {item:[…]} / expectLatch 写成字符串)→ 拍平成 {expectLatch:bool, assertAfterPlay:[]}。 */
function normGatespec(g) {
if (!g || typeof g !== 'object') return null;
let aap = g.assertAfterPlay;
if (aap && !Array.isArray(aap) && Array.isArray(aap.item)) aap = aap.item; // 拆 {item:[…]} 这层
if (!Array.isArray(aap)) aap = aap ? [aap] : [];
let latch = g.expectLatch;
if (typeof latch === 'string') latch = latch.trim().toLowerCase() === 'true';
return { expectLatch: !!latch, assertAfterPlay: aap };
}
/** design :tool-using agent(read_file/list_dir/done),稿+gatespec,/ build
* 日志走 stderr;stdout **末行**单行 JSON {ok,designText,fail}(designNode shell-out 据此解析, saa 契约) */
async function runDesign({ brief, maxSteps, maxWallMs }) {
const t0 = Date.now();
const rec = (m) => console.error(`[+${((Date.now() - t0) / 1000).toFixed(1)}s] ${m}`); // ★ design 日志走 stderr,保 stdout 末行=JSON 契约
rec(`design model=${MODEL} brief=「${brief}」maxSteps=${maxSteps}`);
const messages = [
{ role: 'system', content: buildDesignSystemPrompt() },
{ role: 'user', content: `请按这个 brief 产一份玩家视角玩法设计稿(含 gatespec):「${brief}」。先 read_file 读 sim skill 再设计。` },
];
let designText = null, gatespec = null, steps = 0;
while (!designText && steps < maxSteps) {
if (Date.now() - t0 > maxWallMs) { rec('design wall-time cap 超时,截停'); break; }
steps++;
let resp;
try { resp = await chat(messages, DESIGN_TOOLS); } catch (e) { rec(`step${steps} M3 调用失败: ${e.message}`); break; }
const msg = resp.message; messages.push(msg);
const calls = msg.tool_calls || [];
if (calls.length === 0) {
rec(`step${steps}: 无 tool_calls(content="${(msg.content || '').slice(0, 80)}")`);
messages.push({ role: 'user', content: '请用 read_file/list_dir 读手册后,产出设计稿+gatespec 并调 done。' });
continue;
}
for (const c of calls) {
const name = c.function.name;
let a = {}; try { a = JSON.parse(c.function.arguments || '{}'); } catch { /* 坏 JSON 当空参 */ }
let result;
if (name === 'read_file') {
const r = readFileTool(a.path);
result = r.ok ? (r.truncated ? '[内容已截断]\n' : '') + r.content : 'ERROR: ' + r.error;
rec(`read ${a.path} ${r.ok ? 'ok' : 'ERR ' + r.error}`);
} else if (name === 'list_dir') {
const r = listDirTool(a.path);
result = r.ok ? r.entries.join('\n') : 'ERROR: ' + r.error;
rec(`list ${a.path} ${r.ok ? 'ok' : 'ERR'}`);
} else if (name === 'done') {
designText = (a.designText || a.design || '').trim();
gatespec = normGatespec(a.gatespec || null);
result = `DONE 收到设计稿 ${designText.length}B + gatespec ${gatespec ? 'OK' : '缺'}`;
rec(result);
messages.push({ role: 'tool', tool_call_id: c.id, content: result });
break;
} else {
result = 'ERROR: 未知工具 ' + name;
}
messages.push({ role: 'tool', tool_call_id: c.id, content: String(result).slice(0, TOOL_RESULT_CAP) });
}
}
const ok = !!(designText && designText.length > 80);
// stdout 末行单行 JSON(designNode shell-out 解析;日志全在 stderr,不污染)。
console.log(JSON.stringify({ ok, designText: ok ? designText : null, gatespec: ok ? gatespec : null, fail: ok ? null : 'design agent 未产出设计稿' }));
return ok;
}
async function main() {
const mode = arg('mode', 'spike'); // 'spike'(默认,_template/__gameBooted 信封)| 'saa'(__GameBundle 信封 + stage 到 _wg1-gen)
const mode = arg('mode', 'spike'); // 'spike'(默认)| 'saa'(__GameBundle 信封)| 'design'(策划 agent,只产设计稿+gatespec)
// design 模式:tool-using 策划 agent,只产玩家视角设计稿+gatespec(供 designNode shell-out 喂 code agent),不 scaffold/build/play。
if (mode === 'design') {
const ok = await runDesign({
brief: arg('brief', '一个简单的经营小游戏'),
maxSteps: Number(arg('max-steps', '25')),
maxWallMs: Number(arg('max-wall-ms', '600000')),
});
process.exit(ok ? 0 : 1);
}
const isSaa = mode === 'saa';
const id = isSaa ? arg('game-id', arg('id', 'run1')) : arg('id', 'run1');
const brief = arg('brief', '一个简单的点击得分小游戏');

View File

@ -101,3 +101,68 @@ A 读手册(1)+ 读你的起点 game-logic.js(4)→ B 按 brief 定玩法(核心
现在开始:** 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 层可达能力(12 插件)你设计的机制**只能用这 12 插件 + boot.ctx 达成**
3. 某插件能力拿不准 read_file 它的 api.d.ts
产出( §8 配方,紧凑中文条目,逐项可落运行时)
核心循环(+30 秒脚本)· 主机制 12 (标用哪些插件)· **经营品类必含进货库存售卖补货资源环** · 解锁阶梯(36 + 阈值)· 数值骨架(初始/成长率/卡点)· **客流/出现节奏目标值**(出现间隔·同屏上限·耐心·rush 起点 §3 给具体区间,别拍激进=别让顾客太快)· 美术风格词(一组共用)· 音效清单(收益/升级/解锁,§10 红线必有反馈音)· UI 布局 · 3 分钟脚本
两道自检(必过,写进设计稿)
- **可达性自检(§9)**:逐条问12 插件 + boot.ctx 能落吗落不到的(联网/3D/大世界/复杂经济)剔除或简化
- **好玩自检(§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 三个工具;不写文件不写代码`;
}

View File

@ -142,6 +142,28 @@ function stripCode(s) {
.replace(/`(?:[^`\\]|\\.)*`/g, '``'); // 模板串
}
/** 插件键 → api.d.ts 目录(host-config 注入的 canonical 键名;特殊:juice/save/palettePost/physics 名≠dir)。 */
const PLUGIN_KEY_DIR = {
sceneFsm: 'scene-fsm', sessionScore: 'session-score', hudUi: 'hud-ui', timerScheduler: 'timer-scheduler',
save: 'save-progress', gamefeel: 'gamefeel', juice: 'particles-juice', palettePost: 'palette-post',
audioMusic: 'audio-music', collision: 'collision', physics: 'physics-lite',
};
/** PluginContext(boot.ctx)真方法集 + 运行时附加 log;time/random 是属性对象(ctx.time.nowMs()/ctx.random.next() 内层 time/random≠ctx,经"非键跳过"放行)。 */
const CTX_METHODS = new Set(['getContext2d', 'onFrame', 'getInput', 'getAudioContext', 'getEngine', 'log']);
const _apiCache = new Map();
/** 读某插件 api.d.ts 抽其声明的方法名集(宽松:取所有「名(」形态,over-permissive=少误报)。返回 Set|null。 */
function apiMethodSet(dir) {
if (_apiCache.has(dir)) return _apiCache.get(dir);
let set = null;
try {
const txt = readFileSync(join(GAME_RUNTIME, 'src', 'plugins', dir, 'api.d.ts'), 'utf8');
set = new Set();
for (const m of txt.matchAll(/(?:^|\n)\s*(\w+)\??\s*\(/g)) set.add(m[1]);
} catch { set = null; }
_apiCache.set(dir, set);
return set;
}
/**
* check:循环内快反馈门(便宜模型自纠语法/契约错的头号手段)
* 1) node --check src/*.js + entry.js(语法);
@ -232,6 +254,27 @@ export function check(id) {
}
}
// 4) API-存在静态门(玩法无关,治"方法名幻觉" stopBgm/defineScenes/ctx.nowMs):游戏调的 plugins.<键>.<m>() / ctx.<m>() 必须在 api.d.ts 真存在。
// 只查已知插件键(host-config 注入的 11)+ ctx 直接方法;非键(state/Math/g/bundle/time/random 内层…)跳过 → over-permissive 少误报。
for (const f of ['game-logic.js', 'core.js', 'render.js']) {
const abs = join(gd, 'src', f);
if (!existsSync(abs)) continue;
const s = stripCode(readFileSync(abs, 'utf8'));
for (const m of s.matchAll(/\b(\w+)\.(\w+)\s*\(/g)) {
const key = m[1], method = m[2];
if (key === 'ctx') {
if (!CTX_METHODS.has(method)) {
errors.push(`API 静态门 ${f}: ctx.${method}() 不存在——boot.ctx 真方法 = ${[...CTX_METHODS].join('/')};时间用 ctx.time.nowMs()、随机用 ctx.random.next()(time/random 是对象不是函数)`);
}
} else if (PLUGIN_KEY_DIR[key]) {
const ms = apiMethodSet(PLUGIN_KEY_DIR[key]);
if (ms && ms.size && !ms.has(method)) {
errors.push(`API 静态门 ${f}: ${key}.${method}() 不存在——见 src/plugins/${PLUGIN_KEY_DIR[key]}/api.d.ts 的真方法名(M3 方法名幻觉)`);
}
}
}
}
return { ok: errors.length === 0, errors };
}