From c8a15db3498f4df9179501f58ef9d79055cff209 Mon Sep 17 00:00:00 2001 From: lili Date: Wed, 8 Jul 2026 21:30:36 -0700 Subject: [PATCH] =?UTF-8?q?feat(cheap-gen):=20=E8=BD=BBA=20=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=8C=96=E6=94=B6=E6=95=9B=E2=80=94=E2=80=94create=20?= =?UTF-8?q?=E8=B7=AF=20per-genre=20=E6=8D=A2=E7=9A=AE=E5=86=99=E9=94=81,?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=8F=AA=E5=86=99=20game-logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit n=5 收敛环第一性坐实便宜档失败主根:模型被放任重写引擎文件(render/core),大文件 整写截断丢导出→黑屏(heritage 坏死)、自造楼层推进死锁(trpg)。根治=生成面收窄: create 路 route 命中 per-genre 黄金模板时,write_whitelist 物理锁 render/core,模型 只据 brief 换皮 game-logic.js(+assets.js)。机制早存在(write_whitelist 只用于 modify、6 个 per-genre 完整可玩模板已建),只差 create 接线。 - cheap_roles: build_reskin_system_prompt(引擎锁死、只写 game-logic 语言,镜像 build_modify)+ SCAFFOLD_DESC_BY_TEMPLATE(5 品类换皮描述) - cheap_studio: run_studio 据 write_whitelist 自选换皮/通用提示 - cheap_service_driver / hard_genre_batch: create 路 route 命中即写锁 game-logic+assets it5 验证:heritage bright 0→178812、phase menu→play(真修好);trpg H_progress floor→6 登顶(死锁消失,生成码天赋分支补了 dealFloor);写锁逐字节生效(render/core 与模板相同);未锁对照组(idle/action,无品类模板)仍坏死=证明是锁起效。 route_genre 覆盖 4 类,idle-sim/action-physics 无模板=已知覆盖缺口(round-2)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- cheap-worker/cheap_roles.py | 51 +++++++++++++++ cheap-worker/cheap_service_driver.py | 17 +++-- cheap-worker/cheap_studio.py | 8 ++- cheap-worker/hard_genre_batch.py | 97 ++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 6 deletions(-) create mode 100644 cheap-worker/hard_genre_batch.py diff --git a/cheap-worker/cheap_roles.py b/cheap-worker/cheap_roles.py index d406edf4..cd5e6528 100644 --- a/cheap-worker/cheap_roles.py +++ b/cheap-worker/cheap_roles.py @@ -101,6 +101,19 @@ A 读手册(1 + 按品类 5/6/7/8/9)+ 读你的起点 game-logic.js(4) # 让 AI 知道起点已是一款该品类的完整游戏、按 brief 换皮而非重写(与 scaffold_template 配对,见 run_studio)。 _DEFAULT_SCAFFOLD_DESC = "一个跑通的脚手架起点「点圆得分」:menu→play→over 骨架 + 五法齐全 + 编排插件示范" +# 轻A(2026-07-08 n=5 收敛环坐实):per-genre 黄金模板换皮描述(单一事实源 = 此表)。route_genre 命中即喂 +# build_reskin_system_prompt,让模型知道「⟦G⟧ 已是一款完整可玩的该品类游戏、只据 brief 换皮 game-logic.js」, +# 配 write_whitelist={game-logic.js, assets.js} 物理锁死 render/core 引擎文件——根除模型重写 render.js 整写截断 +# 丢导出(#4/#12)、写坏 core.js 留孤儿(#18)、硬编码 viewport(#19)、自造楼层推进死锁(#1)等构建/坏死事故。 +# 键 = scaffold_template 名(对齐 cheap_genre_route._GENRE_RULES 与 cheap_verify.GENRE_BY_TEMPLATE)。 +SCAFFOLD_DESC_BY_TEMPLATE = { + "_template-feiyi": "一款完整可玩的非遗工艺游戏(工序链节拍 + 火候窗口判定 + 成品评分,menu→play→over 全链通过九门)", + "_template-trpg": "一款完整可玩的掷骰爬塔 TRPG(楼层推进 + 掷骰对抗结算 + 每几层升级选天赋,全链通过九门)", + "_template-shop": "一款完整可玩的经营点客游戏(顾客点单 + 上菜匹配 + 收银攒钱 + 3-4 级解锁阶梯,全链通过九门)", + "_template-puzzle": "一款完整可玩的解谜游戏(观察→推理→点解 + 规则递进关卡阶梯 + 卡壳兜底,全链通过九门)", + "_template-story": "一款完整可玩的剧情互动叙事游戏(分支选择 + 多结局 latch + 结局图鉴,全链通过九门)", +} + # ── C2a:cheap_roles 运行时热取配置 ───────────────────────────────────────────── # 把 _SYSTEM_PROMPT 从「硬编码字符串」改成「运行时从 contracts/prompts 读 + mtime 缓存 + 回落内置」。 @@ -379,3 +392,41 @@ def build_modify_system_prompt(game_id: str, intent: str) -> str: A 读手册 + 读当前 game-logic.js → B 定位「{intent}」要改玩法的哪一处 → C 读要用的插件 api.d.ts → D write_file 改写 **game-logic.js(只此一个文件)** 实现调整 → E 调 check,有错改到 PASS → F 调 build,有错改到 PASS → G check+build 都绿后调 finish。""" g = f"game-runtime/games/amgen-{game_id}" return (head + "\n\n" + _contract_block()).replace("⟦G⟧", g) + + +def build_reskin_system_prompt(game_id: str, brief: str, genre_desc: str = None) -> str: + """轻A(2026-07-08 n=5 收敛环坐实):per-genre 黄金模板已是完整可玩游戏 → create 路据 brief 只换皮 game-logic.js。 + + 与 create(build_system_prompt)的关键差异,正是 n=5 里 heritage 坏死的病根修复: + ① 起点是**该品类完整可玩游戏**(非通用「点圆得分」),据 brief 换皮而非从零造/重写引擎; + ② 写边界物理收窄到**只许写 game-logic.js(+assets.js)**——render/core/host-config 等引擎文件锁死(写会被 + toolkit 拒),根除模型重写 render.js 整写截断丢导出(#4/#12)、写坏 core 留孤儿(#18)、硬编码 viewport + (#19)、自造楼层推进死锁(#1)等构建/坏死事故。base create prompt 的「按需写 render/core」在此不适用。 + 红线契约块(入口五法 / 插件 API / footgun / 输入)经 _contract_block() 复用 create 同一份,不另抄。 + + Args: + game_id: 本 run 的游戏 id。 + brief: 用户的一句话游戏需求(据它换皮)。 + genre_desc: 该品类模板一句话描述(SCAFFOLD_DESC_BY_TEMPLATE 取);None → 通用回落。 + + Returns: + reskin system prompt 字符串(⟦G⟧ 已据 game_id 替换)。 + """ + desc = genre_desc or "一款完整可玩的便宜档 LittleJS 小游戏" + head = f"""你是 A-model 游戏生成 agent。⟦G⟧/ 下已经是**{desc}**——一款**能跑能玩、全链通过九门**的便宜档 LittleJS 小游戏(多文件 src/ 工程)。 + +【本次任务 = 据 brief 换皮,不是从零造、更不是重写引擎】 +- **唯一可写文件:⟦G⟧/src/game-logic.js(玩法本体)**,另可按需写 ⟦G⟧/src/assets.js(资产装载)。据 brief「{brief}」把这款已可玩游戏的**题材 / 文案 / 数值 / 画面参数 / 玩法细节**换成 brief 要的。 +- **引擎文件一律锁死、绝不要写(写会被工具直接拒绝)**:src/render.js(画面绘制)/ src/core.js(品类核心循环机制与数值常量)/ src/host-config.js / src/game.js / index.html / entry-bundle.js。它们是**数据驱动引擎**——你 read_file 读它们看有哪些绘制入口、核心机制与可喂的数据位,再**通过 game-logic.js 喂数据 / 参数**驱动它们,而不是改它们。**尤其别自己重写 core.js 的核心循环(楼层推进 / 工序链 / 资源环 / 障碍生成等)——用模板现成的,自己重写极易写出死锁或坏死(实测 n=5 栽点)。** +- 这是完整可玩游戏的**换皮改造**:先 read_file 读当前 ⟦G⟧/src/game-logic.js + core.js + render.js 看现在怎么玩、有哪些可喂的数据位,再据 brief 换皮;别推倒重来、别改 brief 没要求的机制。 + +【先读(必须;别凭记忆猜 API)】 +1. read_file('{SKILL_PATH}') —— code 层作业手册(结构 / 边界 / 红线 / 插件 API 速查)。 +2. read_file('⟦G⟧/src/game-logic.js') + read_file('⟦G⟧/src/core.js') —— 当前玩法与核心机制(你换皮的起点与可喂数据位)。 +3. read_file('⟦G⟧/README.md') —— 文件职责 +「你写什么 vs 调什么」边界 + ⚠bundle.tick 坑。 +4. 用到某插件先 read_file 它的 api.d.ts 看精确签名(如 game-runtime/src/plugins/scene-fsm/api.d.ts)。 + +【步骤】 +A 读手册 + 读当前 game-logic.js / core.js / render.js(看清核心循环怎么跑、哪些是可喂数据位)→ B 据 brief 定**换皮方案**(题材 / 文案 / 数值换成 brief,**复用模板核心循环别重写**;在 game-logic.js 顶部写 ≤8 行设计注释:⑨ 判定句三个空 + 核心数值锚 + ⑩ 三段难度)→ C 读要用的插件 api.d.ts → D write_file / edit_file 改写 **game-logic.js(+按需 assets.js)** 实现换皮(小改优先 edit_file)→ E 调 check,有错改到 PASS → F 调 build,有错改到 PASS → G check + build 都绿后调 finish。""" + g = f"game-runtime/games/amgen-{game_id}" + return (head + "\n\n" + _contract_block()).replace("⟦G⟧", g) diff --git a/cheap-worker/cheap_service_driver.py b/cheap-worker/cheap_service_driver.py index 7315d83b..38768cd8 100644 --- a/cheap-worker/cheap_service_driver.py +++ b/cheap-worker/cheap_service_driver.py @@ -249,7 +249,7 @@ async def drive_cheap_generation(job: dict, *, base_url: str | None = None, user import cheap_otlp_sink # noqa: PLC0415 —— 面四三跳传播:取当前 context 的 W3C carrier(顶层零重依赖) import cheap_run # noqa: PLC0415 import cheap_verify # noqa: PLC0415 - from cheap_roles import build_system_prompt # noqa: PLC0415 + from cheap_roles import build_system_prompt, build_reskin_system_prompt, SCAFFOLD_DESC_BY_TEMPLATE # noqa: PLC0415 from service.control_plane import _wait_for_turn_end # noqa: PLC0415 —— 复用 tier2 SSE 等回合 game_id = job.get("gameId") or job.get("job_id") @@ -271,10 +271,17 @@ async def drive_cheap_generation(job: dict, *, base_url: str | None = None, user from cheap_genre_route import route_genre # noqa: PLC0415 scaffold_template, genre = route_genre(brief) - if scaffold_template: - print(f"[cheap-driver] game={game_id} 品类路由命中:genre={genre} template={scaffold_template}", flush=True) scaffold_desc = None write_whitelist = None + if scaffold_template: + # 轻A(2026-07-08 n=5 收敛环坐实):per-genre 黄金模板已完整可玩 → 物理锁写边界到 game-logic.js(+assets), + # 引擎文件(render/core/host-config)锁死,模型只据 brief 换皮玩法(用 build_reskin_system_prompt)——根除 + # 模型重写 render.js 整写截断丢导出(#4/#12)、写坏 core 留孤儿(#18)、自造楼层推进死锁(#1)等构建/坏死事故。 + # 通用 _template 回退路(route 无命中,如重力翻转动作)不锁:通用骨架非游戏专用、仍需模型写 render/core。 + write_whitelist = {"game-logic.js", "assets.js"} + scaffold_desc = SCAFFOLD_DESC_BY_TEMPLATE.get(scaffold_template) + print(f"[cheap-driver] game={game_id} 品类路由命中(轻A 换皮锁):genre={genre} " + f"template={scaffold_template} whitelist=game-logic.js,assets.js", flush=True) # 失控兜底轮数(与三闸 150 对齐;成本上界=max_repairs=6 优雅终止,而非轮数闸;放开防单 POST 多续修被 EXCEED_MAX_ITERS 提前切断)。 max_iters = genconfig.get("budget", "cheap_max_iters", 150) max_tokens = genconfig.get("model", "max_tokens", 16000) @@ -304,7 +311,9 @@ async def drive_cheap_generation(job: dict, *, base_url: str | None = None, user ctx_cfg = config.build_context_config() agent_body = { "name": "cheap-writer", - "system_prompt": build_system_prompt(game_id, scaffold_desc), + # 轻A:write_whitelist 收窄(route 命中 per-genre 模板)→ 换皮 prompt(引擎锁死、只写 game-logic);否则通用 create prompt。 + "system_prompt": (build_reskin_system_prompt(game_id, brief, scaffold_desc) + if write_whitelist else build_system_prompt(game_id, scaffold_desc)), "context_config": ctx_cfg.model_dump(mode="json"), "react_config": {"max_iters": max_iters}, } diff --git a/cheap-worker/cheap_studio.py b/cheap-worker/cheap_studio.py index 9df25b25..74ea3525 100644 --- a/cheap-worker/cheap_studio.py +++ b/cheap-worker/cheap_studio.py @@ -21,7 +21,7 @@ from pathlib import Path # 跨包 import 兜底 + key 注入(_bootstrap 模块级把 tier2/gen-worker 加进 sys.path)。 sys.path.insert(0, str(Path(__file__).resolve().parent)) # → cheap-worker/(CLI 直跑兼容) import _bootstrap # noqa: E402,F401 -from cheap_roles import build_system_prompt # noqa: E402 +from cheap_roles import build_system_prompt, build_reskin_system_prompt # noqa: E402 from cheap_toolkit import CheapSession, build_toolkit # noqa: E402 import cheap_budget # noqa: E402 预算两段式同源工厂(CLI 与生产 Service 读同一配置源) import cheap_run # noqa: E402 @@ -266,7 +266,11 @@ 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, scaffold_desc), # modify 传 build_modify_system_prompt;扩模板传 scaffold_desc + # 轻A:write_whitelist 收窄(create 路 route 命中 per-genre 模板)→ 用换皮 prompt(引擎锁死、只写 game-logic), + # 否则用通用 create prompt(从零造);modify 路由调用方显式传 system_prompt=build_modify_system_prompt。 + system_prompt=system_prompt or ( + build_reskin_system_prompt(game_id, brief, scaffold_desc) if write_whitelist + else build_system_prompt(game_id, scaffold_desc)), model=model, toolkit=toolkit, middlewares=[tracer, breaker], # trace 外层、熔断内层(列表序=洋葱序) diff --git a/cheap-worker/hard_genre_batch.py b/cheap-worker/hard_genre_batch.py new file mode 100644 index 00000000..2c09f5d2 --- /dev/null +++ b/cheap-worker/hard_genre_batch.py @@ -0,0 +1,97 @@ +"""hard_genre_batch.py — 便宜档 n=5 收敛 loop 的【难品类】批跑(复现 39% 玩法坏死缺口)。 + +bake_off 只有 3 个带 golden spec 的易品类(click-score/whack-mole/shop-serve),首轮 6/6 全过=样本不代表。 +39% 缺口在更难/更多样的品类(叙事/TRPG/非遗/放置/动作)——这些玩法复杂,续修回路不一定救得回。 +本脚本用 cheap_studio.run_studio(run_gates=True,generic scaffold=对齐"一句话不选模板"生产流)跑 5 个难 brief, +无 golden spec 时 ensure_play_spec 据 smoke 抓的 state 自动产 driver,九门 verdict 客观 pass/fail。 + +跑:cheap-worker/.venv/bin/python cheap-worker/hard_genre_batch.py +env:NO_PROXY + NEWAPI_KEY(见凭据档)。 +""" + +import asyncio +import json +import sys +import time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import cheap_studio # noqa: E402 +from cheap_genre_route import route_genre # noqa: E402 轻A:brief→per-genre 黄金模板路由(对齐生产 create 路) +from cheap_roles import SCAFFOLD_DESC_BY_TEMPLATE # noqa: E402 轻A:per-genre 换皮一句话描述 + +# 5 个难/多样品类 brief——各压不同玩法坏死面(状态机/掷骰数值/工序序列/自动进展/物理输入时序)。 +HARD_BRIEFS = [ + ("narrative", "一个分支选择推进的互动文字冒险:每屏一段剧情+2-3个选项,选择影响走向,最终3个不同结局,有结局图鉴"), + ("trpg", "一个掷骰战斗的地牢爬塔:每层遇一个敌人,点击掷骰(可见骰点)按攻防结算,击败进下一层,失败结算,每3层升级选天赋"), + ("heritage", "一个还原传统榫卯木工的非遗小游戏:按工序节拍(选料→画线→凿卯→修整)逐步操作,每步有节奏判定,完成得成品评分"), + ("idle-sim", "一个放置挂机农场:作物自动生长产出金币,点击收获,金币升级解锁新作物与自动化,有离线收益结算"), + ("action-physics", "一个重力翻转动作游戏:角色自动向前跑,点击切换上下重力躲避障碍与尖刺,吃金币计分,撞到结算"), +] + + +async def _run_one(sem, genre, brief, port, cdp): + async with sem: + gid = f"hard-{genre}" + t0 = time.time() + # 轻A(2026-07-08):对齐生产 create 路——brief→route_genre 选 per-genre 黄金模板 + 换皮写锁。 + # 命中品类(非遗/TRPG/经营/解谜/剧情)→ 锁引擎文件、只写 game-logic(build_reskin prompt,run_studio 据 write_whitelist 自选); + # 无命中(action-physics 重力翻转,route 无此类)→ scaffold_template=None 走通用模板、不锁(已知覆盖缺口,如实记账)。 + scaffold_template, routed_genre = route_genre(brief) + write_whitelist = {"game-logic.js", "assets.js"} if scaffold_template else None + scaffold_desc = SCAFFOLD_DESC_BY_TEMPLATE.get(scaffold_template) if scaffold_template else None + try: + summary = await cheap_studio.run_studio( + gid, brief, run_gates=True, port=port, cdp_port=cdp, + scaffold_template=scaffold_template, scaffold_desc=scaffold_desc, + write_whitelist=write_whitelist, genre=routed_genre) + except Exception as e: # noqa: BLE001 + return {"genre": genre, "gid": gid, "ok": False, "fail": f"{type(e).__name__}: {e}", "wallSec": round(time.time() - t0, 1)} + v = summary.get("verdict") or {} + rich = summary.get("richness") or {} + return { + "genre": genre, "gid": gid, + "template": scaffold_template or "_template(通用)", "locked": write_whitelist is not None, + "ok": bool(summary.get("ok")), + "finished": bool(summary.get("finished")), + "verdictPass": v.get("pass"), + "failedGates": v.get("failedGates"), + "attempts": summary.get("attempts"), + "breaker": summary.get("breaker"), + "costRmb": summary.get("costRmb"), + "richness": rich.get("score"), + "richMax": rich.get("max"), + "wallSec": round(time.time() - t0, 1), + } + + +async def main(): + sem = asyncio.Semaphore(2) # conc=2(本机 CPU+chrome 负载) + tasks = [] + for i, (genre, brief) in enumerate(HARD_BRIEFS): + tasks.append(_run_one(sem, genre, brief, 4340 + i * 2, 9242 + i * 2)) + results = await asyncio.gather(*tasks) + print("\n\n========== 难品类 n=5 收敛结果 ==========") + # 【达标判据 = 九门 verdict.pass】(真玩过门),不是 summary.ok(=finished,可能 finished-但-坏死=false pass)。 + # iteration 4 坐实:heritage finished(ok=True)但 verdict.pass=False(D/E/G/H 全挂玩法坏死)→ 旧判据虚高。对齐 bake_off。 + passed = 0 + for r in results: + vp = r.get("verdictPass") is True + passed += 1 if vp else 0 + mark = "✅" if vp else ("⚠️FALSE-PASS" if r.get("ok") else "❌") + print(f"{mark} {r['genre']:16s} tpl={r.get('template','?')} lock={r.get('locked')} " + f"verdictPass={r.get('verdictPass')} ok(finished)={r.get('ok')} " + f"failedGates={r.get('failedGates')} attempts={r.get('attempts')} " + f"rich={r.get('richness')}/{r.get('richMax')} breaker={r.get('breaker')} " + f"cost={r.get('costRmb')} wall={r.get('wallSec')}s " + f"{('FAIL='+r['fail']) if r.get('fail') else ''}") + print(f">>> 难品类达标(九门 verdict.pass):{passed}/{len(results)} 过门率={passed/len(results):.0%}" + f"(⚠️FALSE-PASS=finished 但九门坏死,不计达标)") + out = Path(__file__).resolve().parent / "results" / "hard-genre-r1.json" + out.parent.mkdir(exist_ok=True) + out.write_text(json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8") + print(f">>> 结果 → {out}") + + +if __name__ == "__main__": + asyncio.run(main())