创始人 2026-06-15 定档「driver 残留闭口 + P2 对照(L1 自主)」;P1 校准放量 blocked-on-founder。 - play.cdp.cjs driver 库 1→8 型:+flap-to-gap vy 前瞻 / paddle-intercept 弃守排空 / drag-aiming(第7) / aim-fire(第8) - flappy ✅(重生成 score 0→5,vy 前瞻+可达缝隙 brief) - multiball ✅(重生成首发 retries=0,弃守排空 driver+底部丢球语义 brief) - asteroids ✅(enrich exportState 暴露陨石坐标+aim-fire driver,score 0→6=可测性边界转确定性闭口范式) - t2048(8/9,键盘桥已修 score 0→4;latch=2048 抗死锁体裁边界)/ angrybirds(8/9,隐藏弹道物理可测性边界)honest 留界 - 三分类铁论:0 模型造不出 / 0 3D 硬界——全是宿主键盘桥(L0 已修)/driver 覆盖(我域已增强)/可测性边界(游戏 exportState) - P2:同款 Breakout 三档对照(flash ¥0.194/M2.7 ¥0.514/pro ¥0.417)+ 创始人锚(pro 碰撞 bug 被两便宜 player 假绿=人锚不可替代) - 蒸馏:game-e2e §8(driver 1→8 型 + H 假阴四因归因铁律)、cheap-model §9(重生成区分实例bug/可测性/模型短板);W-G1 spec 执行进展回填;新增 L1 闭口报告 - 顺带:run.py/prompt.py 注释 六门→九门 对齐(我域,trivial) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
189 lines
8.0 KiB
Python
189 lines
8.0 KiB
Python
"""run.py —— W-G1 L1 生成 worker 编排(§3 job-in/result-out 接缝)。
|
||
|
||
job in :brief(题面 + play-spec)+ model + max_retries
|
||
result:{ pass, retries, attempts[{usage,stage,errors}], bundle 路径, verdict 四件套, factory 路径 }
|
||
|
||
闭环:generate → validate(静态+node --check) → scaffold+build(esbuild) → play(CDP 九门) →
|
||
任一阶段失败即把错因回喂模型重试,≤ max_retries 次。"能生成 ≠ 能玩"——必须过 play 才算 pass。
|
||
"""
|
||
|
||
import json
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
import time
|
||
from pathlib import Path
|
||
|
||
# 包内导入(python -m worker.run)兼容直接执行。
|
||
try:
|
||
from . import _client, prompt, validate
|
||
except ImportError: # 直接 python worker/run.py
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
import _client, prompt, validate # type: ignore
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||
GAME_RUNTIME = REPO_ROOT / "game-runtime"
|
||
GEN_DIR = GAME_RUNTIME / "games" / "_wg1-gen"
|
||
SHARED = GEN_DIR / "_shared"
|
||
WORKER_ROOT = Path(__file__).resolve().parents[1]
|
||
BRIEFS_DIR = WORKER_ROOT / "briefs"
|
||
RESULTS_DIR = WORKER_ROOT / "results"
|
||
|
||
|
||
def scaffold(game_id, factory_src, play_spec):
|
||
"""落 factory.js + 拷模板(entry-bundle/index) + 写 play-spec.json。返回 game 目录。"""
|
||
gdir = GEN_DIR / game_id
|
||
gdir.mkdir(parents=True, exist_ok=True)
|
||
# 官方靶名 generated-factory.js(entry-bundle 壳 import './generated-factory.js')。
|
||
(gdir / "generated-factory.js").write_text(factory_src, encoding="utf-8")
|
||
shutil.copyfile(SHARED / "entry-bundle.template.js", gdir / "entry-bundle.js")
|
||
shutil.copyfile(SHARED / "index.template.html", gdir / "index.html")
|
||
(gdir / "play-spec.json").write_text(json.dumps(play_spec, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
return gdir
|
||
|
||
|
||
def build(game_id):
|
||
"""esbuild 打 __GameBundle iife。返回 (ok, log)。"""
|
||
rel = f"games/_wg1-gen/{game_id}"
|
||
r = subprocess.run(
|
||
["node", "scripts/build.mjs", f"{rel}/entry-bundle.js", f"{rel}/bundle.iife.js", "--global-name=__GameBundle"],
|
||
cwd=str(GAME_RUNTIME), capture_output=True, text=True, timeout=120,
|
||
)
|
||
ok = r.returncode == 0
|
||
return ok, (r.stdout + r.stderr).strip()
|
||
|
||
|
||
def play(game_id, port=4320, cdp_port=None):
|
||
"""CDP 真玩七门。返回 (rc, stdout, verdict_dict|None)。
|
||
cdp_port 独立可传 → 多 run 并行时各占独立 Chrome 调试端口(serve-and-play.sh 第三参),互不撞。"""
|
||
args = ["bash", "games/_wg1-gen/_shared/serve-and-play.sh", game_id, str(port)]
|
||
if cdp_port is not None:
|
||
args.append(str(cdp_port))
|
||
r = subprocess.run(
|
||
args, cwd=str(GAME_RUNTIME), capture_output=True, text=True, timeout=180,
|
||
)
|
||
verdict = None
|
||
vpath = GEN_DIR / game_id / "evidence" / "verdict.json"
|
||
if vpath.exists():
|
||
try:
|
||
verdict = json.loads(vpath.read_text(encoding="utf-8"))
|
||
except Exception:
|
||
pass
|
||
return r.returncode, (r.stdout + r.stderr).strip(), verdict
|
||
|
||
|
||
def _verdict_feedback(verdict):
|
||
"""把失败的守卫摘成给模型的回喂文字。"""
|
||
if not verdict:
|
||
return "真玩阶段无 verdict 产出(疑似装载即崩/超时)。"
|
||
lines = []
|
||
for k, g in (verdict.get("guards") or {}).items():
|
||
if not g.get("pass"):
|
||
lines.append(f"- 守卫 {k} 未过:{json.dumps(g, ensure_ascii=False)[:200]}")
|
||
if verdict.get("error"):
|
||
lines.append("- 运行错误:" + str(verdict["error"])[:300])
|
||
return "真玩未过:\n" + "\n".join(lines) if lines else "真玩未过(原因未知)。"
|
||
|
||
|
||
def run_job(game_id, brief_text, play_spec, model, max_retries=2, port=4320, max_tokens=16000):
|
||
"""单 job 闭环。返回 result dict。"""
|
||
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
||
attempts = []
|
||
feedback = None
|
||
passed = False
|
||
final_verdict = None
|
||
gdir = GEN_DIR / game_id
|
||
|
||
for attempt in range(max_retries + 1):
|
||
rec = {"attempt": attempt, "stage": None, "errors": [], "usage": None}
|
||
# ① 生成
|
||
system, user = prompt.build_messages(brief_text, retry_feedback=feedback)
|
||
try:
|
||
resp = _client.chat(model, system, user, max_tokens=max_tokens)
|
||
except Exception as e:
|
||
rec["stage"] = "generate"; rec["errors"] = [f"模型调用失败:{e}"]
|
||
attempts.append(rec); feedback = str(e); continue
|
||
rec["usage"] = {k: resp[k] for k in ("prompt_tokens", "completion_tokens", "total_tokens", "wall_s", "id", "finish_reason")}
|
||
src = validate.extract_code(resp["content"])
|
||
|
||
# ② 静态 + 语法校验
|
||
ok, errs = validate.validate(src)
|
||
if not ok:
|
||
rec["stage"] = "validate"; rec["errors"] = errs
|
||
attempts.append(rec)
|
||
feedback = "校验未过:\n" + "\n".join("- " + e for e in errs)
|
||
continue
|
||
|
||
# ③ 落盘 + 打包
|
||
scaffold(game_id, src, play_spec)
|
||
bok, blog = build(game_id)
|
||
if not bok:
|
||
rec["stage"] = "build"; rec["errors"] = [blog[:600]]
|
||
attempts.append(rec)
|
||
feedback = "打包失败(esbuild):\n" + blog[:600]
|
||
continue
|
||
|
||
# ④ 真玩
|
||
rc, pout, verdict = play(game_id, port=port)
|
||
final_verdict = verdict
|
||
rec["stage"] = "play"
|
||
rec["play_pass"] = (rc == 0 and verdict and verdict.get("pass"))
|
||
rec["verdict_guards"] = {k: g.get("pass") for k, g in ((verdict or {}).get("guards") or {}).items()}
|
||
attempts.append(rec)
|
||
if rc == 0 and verdict and verdict.get("pass"):
|
||
passed = True
|
||
break
|
||
feedback = _verdict_feedback(verdict)
|
||
|
||
result = {
|
||
"game_id": game_id,
|
||
"model": model,
|
||
"pass": passed,
|
||
"retries": len(attempts) - 1,
|
||
"attempts": attempts,
|
||
"factory_path": str((gdir / "generated-factory.js").relative_to(REPO_ROOT)) if (gdir / "generated-factory.js").exists() else None,
|
||
"bundle_path": str((gdir / "bundle.iife.js").relative_to(REPO_ROOT)) if (gdir / "bundle.iife.js").exists() else None,
|
||
"verdict": final_verdict,
|
||
# 成本:累计 token(¥ 折算由 cost.py 经 new-api logs.quota 后填)。
|
||
"tokens": {
|
||
"prompt": sum((a.get("usage") or {}).get("prompt_tokens", 0) or 0 for a in attempts),
|
||
"completion": sum((a.get("usage") or {}).get("completion_tokens", 0) or 0 for a in attempts),
|
||
},
|
||
"request_ids": [(a.get("usage") or {}).get("id") for a in attempts if (a.get("usage") or {}).get("id")],
|
||
}
|
||
(RESULTS_DIR / f"{game_id}.json").write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
return result
|
||
|
||
|
||
def _load_brief(game_id, brief_path=None):
|
||
p = Path(brief_path) if brief_path else (BRIEFS_DIR / f"{game_id}.json")
|
||
data = json.loads(p.read_text(encoding="utf-8"))
|
||
return data["brief"], data.get("play_spec", {"inputs": [], "expectedEngineCallPrefixes": []})
|
||
|
||
|
||
def main():
|
||
import argparse
|
||
ap = argparse.ArgumentParser(description="W-G1 L1 生成 worker")
|
||
ap.add_argument("game_id")
|
||
ap.add_argument("--model", default=_client.DEFAULT_MODEL)
|
||
ap.add_argument("--max-retries", type=int, default=2)
|
||
ap.add_argument("--brief", default=None)
|
||
ap.add_argument("--port", type=int, default=4320)
|
||
ap.add_argument("--max-tokens", type=int, default=16000)
|
||
args = ap.parse_args()
|
||
|
||
brief_text, play_spec = _load_brief(args.game_id, args.brief)
|
||
print(f"[run] game={args.game_id} model={args.model} max_retries={args.max_retries}")
|
||
t0 = time.perf_counter()
|
||
res = run_job(args.game_id, brief_text, play_spec, args.model,
|
||
max_retries=args.max_retries, port=args.port, max_tokens=args.max_tokens)
|
||
dt = time.perf_counter() - t0
|
||
print(f"\n[run] {'✅ PASS' if res['pass'] else '❌ FAIL'} retries={res['retries']} "
|
||
f"tokens(prompt/completion)={res['tokens']['prompt']}/{res['tokens']['completion']} wall={dt:.1f}s")
|
||
print(f"[run] result → wg1/gen-worker/results/{args.game_id}.json")
|
||
sys.exit(0 if res["pass"] else 1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|