lili cc5fa486ac feat(wg1): driver-coverage 闭口 + P2 三档对照——flappy/multiball/asteroids 翻绿,0 模型造不出
创始人 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>
2026-06-15 02:00:07 -07:00

189 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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()