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:
parent
68472bb560
commit
57c30909cd
@ -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.8–2.8s** / 同屏上限 **3** / 顾客耐心 **~5–7s**;高峰期间隔 **~1.0–1.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 校验;**注:通用九门不加品类语义,断言是每局喂的**)。其余(情感锚/放置回归/滚雪球)靠真玩判,不机检。
|
||||
|
||||
---
|
||||
|
||||
## 相关
|
||||
|
||||
@ -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', '一个简单的点击得分小游戏');
|
||||
|
||||
@ -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 秒脚本)· 主机制 1–2 个(标用哪些插件)· **经营品类必含「进货→库存→售卖→补货」资源环** · 解锁阶梯(3–6 级 + 阈值)· 数值骨架(初始/成长率/卡点)· **客流/出现节奏目标值**(出现间隔·同屏上限·耐心·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 三个工具;不写文件、不写代码。`;
|
||||
}
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user