feat(cheap-gen): 轻A 参数化收敛——create 路 per-genre 换皮写锁,模型只写 game-logic

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) <noreply@anthropic.com>
This commit is contained in:
lili 2026-07-08 21:30:36 -07:00
parent 824b4eaba3
commit c8a15db349
4 changed files with 167 additions and 6 deletions

View File

@ -101,6 +101,19 @@ A 读手册1 + 按品类 5/6/7/8/9+ 读你的起点 game-logic.js4
# 让 AI 知道起点已是一款该品类的完整游戏、按 brief 换皮而非重写(与 scaffold_template 配对,见 run_studio
_DEFAULT_SCAFFOLD_DESC = "一个跑通的脚手架起点「点圆得分」menu→play→over 骨架 + 五法齐全 + 编排插件示范"
# 轻A2026-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 + 结局图鉴,全链通过九门)",
}
# ── C2acheap_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:
"""轻A2026-07-08 n=5 收敛环坐实per-genre 黄金模板已是完整可玩游戏 → create 路据 brief 只换皮 game-logic.js。
createbuild_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)

View File

@ -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:
# 轻A2026-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),
# 轻Awrite_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},
}

View File

@ -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
# 轻Awrite_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 外层、熔断内层(列表序=洋葱序)

View File

@ -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 轻Abrief→per-genre 黄金模板路由(对齐生产 create 路)
from cheap_roles import SCAFFOLD_DESC_BY_TEMPLATE # noqa: E402 轻Aper-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()
# 轻A2026-07-08对齐生产 create 路——brief→route_genre 选 per-genre 黄金模板 + 换皮写锁。
# 命中品类(非遗/TRPG/经营/解谜/剧情)→ 锁引擎文件、只写 game-logicbuild_reskin promptrun_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())