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:
parent
824b4eaba3
commit
c8a15db349
@ -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)
|
||||
|
||||
@ -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},
|
||||
}
|
||||
|
||||
@ -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 外层、熔断内层(列表序=洋葱序)
|
||||
|
||||
97
cheap-worker/hard_genre_batch.py
Normal file
97
cheap-worker/hard_genre_batch.py
Normal 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 轻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())
|
||||
Loading…
x
Reference in New Issue
Block a user