From 47d6394347d64ffd69fa76bd76ed0e589339e4bd Mon Sep 17 00:00:00 2001 From: lili Date: Mon, 29 Jun 2026 17:01:06 -0700 Subject: [PATCH] =?UTF-8?q?feat(cheap-worker):=20per-genre=20=E8=84=9A?= =?UTF-8?q?=E6=89=8B=E6=9E=B6=E8=B7=AF=E7=94=B1=E6=8E=A5=E5=85=A5=20scaffo?= =?UTF-8?q?ld=5Ftemplate/scaffold=5Fdesc(=E6=89=A9=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=82=B9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 扩模板需让难品类 AI 从预置品类骨架起步、少写。接入(全缺省回落 _template、create 路零改): - tools.mjs scaffoldSaa(gameId, templateName='_template'):clone 指定模板;CLI 加第 3 参;模板缺失 warn 回落。 - cheap_run.scaffold(game_id, template=None):shell-out 透传模板名。 - cheap_studio.run_studio(scaffold_template, scaffold_desc):线程到 prep + build_system_prompt。 - cheap_roles:system prompt 脚手架描述参数化(⟦SCAFFOLD_DESC⟧),让 AI 知道起点已是该品类完整游戏、换皮而非重写。 管线 routing 单测 27/27、prompt 参数化 8/8。 Co-Authored-By: Claude Opus 4.8 (1M context) --- cheap-worker/cheap_roles.py | 15 +++++++++++---- cheap-worker/cheap_run.py | 14 +++++++++++--- cheap-worker/cheap_studio.py | 8 +++++--- game-runtime/tools/amodel-gen/tools.mjs | 23 ++++++++++++++++------- 4 files changed, 43 insertions(+), 17 deletions(-) diff --git a/cheap-worker/cheap_roles.py b/cheap-worker/cheap_roles.py index ea560fe1..bc412940 100644 --- a/cheap-worker/cheap_roles.py +++ b/cheap-worker/cheap_roles.py @@ -25,7 +25,7 @@ _SYSTEM_PROMPT = """你是 A-model 游戏生成 agent。目标产物 = 一款** 【这一档的定位(钉死)】AI 参与深度低 ≠ 做简单玩具——「轻量≠简单」。手感、物理、碰撞、粒子、调色这些复杂能力,平台已用工程脚手架 + L2 插件库替你承担,你**少写、多调插件**就能做出卖相与深度;你的活是在脚手架上把 brief 做成一款**真能让人玩进去**的游戏,**不是产个没人会玩的简单玩具**。 【工作目录已就绪】 -- 我已把克隆起点 _template 拷到 ⟦G⟧/(一个跑通的脚手架起点「点圆得分」:menu→play→over 骨架 + 五法齐全 + 编排插件示范)。你的活是据 brief **借插件能力把它改造成一款高质游戏**——别把它当"这档就该这么简单"的天花板。 +- 我已把克隆起点拷到 ⟦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/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。 @@ -63,17 +63,24 @@ A 读手册(1)+ 读你的起点 game-logic.js(4)→ B 按 brief 定玩 现在开始:**先 read_file 读手册,别直接写码;核心玩法实现完、check+build 绿了就立即 finish。**""" -def build_system_prompt(game_id: str) -> str: - """据 game_id 拼出本 run 的 system prompt(把 ⟦G⟧ 占位替换成 repo 根相对的 game 目录)。 +# 默认脚手架描述(create 路 = 通用 _template「点圆得分」起点)。扩模板:per-genre 黄金骨架传各自描述覆盖, +# 让 AI 知道起点已是一款该品类的完整游戏、按 brief 换皮而非重写(与 scaffold_template 配对,见 run_studio)。 +_DEFAULT_SCAFFOLD_DESC = "一个跑通的脚手架起点「点圆得分」:menu→play→over 骨架 + 五法齐全 + 编排插件示范" + + +def build_system_prompt(game_id: str, scaffold_desc: str = None) -> str: + """据 game_id 拼出本 run 的 system prompt(把 ⟦SCAFFOLD_DESC⟧/⟦G⟧ 占位替换)。 Args: game_id: 本 run 的游戏 id(cheap-worker 统一用 cheap- 前缀,与 Node 旧路 node- 隔离)。 + scaffold_desc: 起点脚手架的一句话描述;None=默认通用「点圆得分」(create 路不变); + 扩模板传 per-genre 描述(如经营=「完整经营点客游戏…按 brief 换皮」),让 AI 从品类骨架换皮、少写。 Returns: 薄 system prompt 字符串(指 agent read 真 skill,不手抄)。 """ g = f"game-runtime/games/amgen-{game_id}" - return _SYSTEM_PROMPT.replace("⟦G⟧", g) + return _SYSTEM_PROMPT.replace("⟦SCAFFOLD_DESC⟧", scaffold_desc or _DEFAULT_SCAFFOLD_DESC).replace("⟦G⟧", g) def _contract_block() -> str: diff --git a/cheap-worker/cheap_run.py b/cheap-worker/cheap_run.py index 78892fd5..30d3432c 100644 --- a/cheap-worker/cheap_run.py +++ b/cheap-worker/cheap_run.py @@ -153,10 +153,18 @@ def _shell_env() -> dict: return env -def scaffold(game_id: str) -> dict: - """scaffold:shell-out `node tools.mjs scaffold-saa `(clone _template + SAA 信封 __GameBundle)。返回 {ok, output}。""" +def scaffold(game_id: str, template: Optional[str] = None) -> dict: + """scaffold:shell-out `node tools.mjs scaffold-saa [template]`(clone 模板 + SAA 信封 __GameBundle)。返回 {ok, output}。 + + template=None(默认)→ 通用 _template(点圆得分起点);扩模板:传 per-genre 黄金骨架目录名 + (经营=_template-shop 等),让难品类 AI 从预置经营循环起步、少写。模板不存在则 node 侧回落 _template。 + """ try: - r = _run_node_tools("scaffold-saa", game_id) + argv = ["node", str(_AMODEL_GEN / "tools.mjs"), "scaffold-saa", game_id] + if template: + argv.append(template) + r = subprocess.run(argv, cwd=str(_GAME_RUNTIME), capture_output=True, text=True, + timeout=90.0, check=False) return {"ok": r.returncode == 0, "output": (r.stdout + r.stderr).strip()} except Exception as e: # noqa: BLE001 return {"ok": False, "output": f"scaffold shell-out 异常:{e}"} diff --git a/cheap-worker/cheap_studio.py b/cheap-worker/cheap_studio.py index 54295fd5..9c668070 100644 --- a/cheap-worker/cheap_studio.py +++ b/cheap-worker/cheap_studio.py @@ -110,7 +110,8 @@ def build_trace_source(verdict, driver_type, attempts, stage, model) -> dict: async def run_studio(game_id, brief, *, max_iters=40, max_resumes=6, max_tokens=16000, port=4320, cdp_port=9222, run_gates=True, - system_prompt=None, initial_kick=None, write_whitelist=None, prepare=None): + system_prompt=None, initial_kick=None, write_whitelist=None, prepare=None, + scaffold_template=None, scaffold_desc=None): """跑便宜档一局生成(scaffold → ReAct 写 src/ → done 门 → 收口 stage+smoke+九门)。返回 run-summary dict。 run_gates=True(默认,spike 单局):收口跑全套 stage→smoke→ensure_play_spec→九门 play。 @@ -129,7 +130,8 @@ async def run_studio(game_id, brief, *, max_iters=40, max_resumes=6, max_tokens= t0 = time.time() _rec(f"model={_bootstrap.SPIKE_MODEL} id={game_id} brief=「{brief}」max_iters={max_iters} max_resumes={max_resumes}") - prep = prepare or cheap_run.scaffold # create=scaffold clone _template;modify=上游已预备的 noop + # create=scaffold clone 模板(扩模板:scaffold_template 传 per-genre 黄金骨架名,如经营=_template-shop);modify=上游已预备的 noop + prep = prepare or (lambda gid: cheap_run.scaffold(gid, scaffold_template)) sc = prep(game_id) if not sc["ok"]: return {"ok": False, "gameId": game_id, "fail": "准备失败:" + sc["output"]} @@ -146,7 +148,7 @@ async def run_studio(game_id, brief, *, max_iters=40, max_resumes=6, max_tokens= writer = Agent( name="cheap-writer", - system_prompt=system_prompt or build_system_prompt(game_id), # modify 传 build_modify_system_prompt + system_prompt=system_prompt or build_system_prompt(game_id, scaffold_desc), # modify 传 build_modify_system_prompt;扩模板传 scaffold_desc model=model, toolkit=toolkit, middlewares=[tracer, breaker], # trace 外层、熔断内层(列表序=洋葱序) diff --git a/game-runtime/tools/amodel-gen/tools.mjs b/game-runtime/tools/amodel-gen/tools.mjs index d9ac655a..15448bf9 100644 --- a/game-runtime/tools/amodel-gen/tools.mjs +++ b/game-runtime/tools/amodel-gen/tools.mjs @@ -378,16 +378,25 @@ export function saaGameDir(gameId) { } /** - * scaffoldSaa:clone _template → amgen-/,换上 A-model SAA 信封 plumbing(治 review B2)。 + * scaffoldSaa:clone 模板 → amgen-/,换上 A-model SAA 信封 plumbing(治 review B2)。 * - entry-bundle.js = entry-bundle.amodel.template.js(导出 bootGameHost,经 --global-name=__GameBundle 挂 window.__GameBundle) * - index.html = _wg1-gen/_shared/index.template.html(调 __GameBundle.bootGameHost,置 __genBooted) * - 去 spike 入口(entry.js/src/main.js;SAA 不用)+ dist/test * agent 仍只写 src/{game,core,render,host-config,balance,assets}.js(与 spike 同;信封差异全在 plumbing)。 + * + * @param {string} gameId 本 run 游戏 id。 + * @param {string} [templateName='_template'] 起始模板目录名(扩模板:经营=_template-shop 等 per-genre 黄金骨架; + * 缺省 _template=通用点圆得分起点)。模板不存在则回落 _template(不静默失败,打 warn 可查)。 */ -export function scaffoldSaa(gameId) { +export function scaffoldSaa(gameId, templateName = '_template') { const gd = gameDir(gameId); + let tmplDir = join(GAMES_DIR, templateName || '_template'); + if (!existsSync(tmplDir)) { + console.warn(`[scaffold-saa] 模板 ${templateName} 不存在,回落 _template`); + tmplDir = TEMPLATE_DIR; + } if (existsSync(gd)) rmSync(gd, { recursive: true, force: true }); - cpSync(TEMPLATE_DIR, gd, { recursive: true }); + cpSync(tmplDir, gd, { recursive: true }); rmSync(join(gd, 'dist'), { recursive: true, force: true }); rmSync(join(gd, 'test'), { recursive: true, force: true }); // 换 SAA 信封 plumbing(覆盖 _template 的 spike 信封 entry.js/index.html) @@ -482,9 +491,9 @@ export function ensurePlaySpec(gameId, state) { // ───────────────────────── CLI(独立验证用)───────────────────────── // 用法:node tools.mjs scaffold | check | build if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { - const [, , cmd, id] = process.argv; + const [, , cmd, id, arg3] = process.argv; if (!cmd || !id) { - console.error('用法:node tools.mjs '); + console.error('用法:node tools.mjs [templateName]'); process.exit(2); } if (cmd === 'scaffold') { @@ -500,8 +509,8 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur console.log('[build]', r.ok ? 'PASS' : 'FAIL'); if (!r.ok) { console.log(r.error); process.exit(1); } } else if (cmd === 'scaffold-saa') { - const gd = scaffoldSaa(id); - console.log('[scaffold-saa] →', relative(REPO_ROOT, gd), '(SAA 信封 plumbing)'); + const gd = scaffoldSaa(id, arg3); + console.log('[scaffold-saa] →', relative(REPO_ROOT, gd), `(SAA 信封 plumbing, 模板=${arg3 || '_template'})`); } else if (cmd === 'build-saa') { const r = buildSaa(id); console.log('[build-saa]', r.ok ? 'PASS' : 'FAIL');