diff --git a/wg1/gen-worker/gamedef_quickcheck.py b/wg1/gen-worker/gamedef_quickcheck.py new file mode 100644 index 00000000..80e27aa9 --- /dev/null +++ b/wg1/gen-worker/gamedef_quickcheck.py @@ -0,0 +1,123 @@ +"""gamedef_quickcheck.py —— plan 2026-06-18-001 U3 gamedef 路 lili-mac 快走查(cutover 门早读)。 + +run.py 的 gamedef 版:把 generate(iife) → validate(静态) 换成 + generate(GAMEDEF_SYSTEM) → build-from-source.mjs(校验 schema/引用/静态扫描 + 装配出工厂), +其余(scaffold/build/play 九门)复用 run.py(harness 零改)。 + +GAMEDEF_SYSTEM **从后端 SaaPrompts.java 抽取**(drift-free:跑的就是后端真用的 prompt)。 +模型/网关/代理旁路复用 _client;play-spec/brief 复用现有 briefs/*.json(gatespec driver 同口径)。 + +用法:.venv/bin/python gamedef_quickcheck.py runner flappy whack pong # 默认这 4 +仅 lili-mac 快走查(小样早读);authoritative ≥60% cutover 门在 mini-desktop(SaaFullGraphE2eTest)。 +密钥经 _client 读 .env,绝不打印。 +""" +import json +import re +import shutil +import subprocess +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from worker import _client, run # noqa: E402 复用 chat + scaffold/build/play + +REPO_ROOT = Path(__file__).resolve().parents[2] +SAA_PROMPTS = (REPO_ROOT / "game-cloud/game-module-aigc/game-module-aigc-server/src/main/java/" + "com/wanxiang/huijing/game/module/aigc/saa/SaaPrompts.java") +GAME_RUNTIME = REPO_ROOT / "game-runtime" +GEN_DIR = GAME_RUNTIME / "games" / "_wg1-gen" +BRIEFS = Path(__file__).resolve().parent / "briefs" + + +def extract_gamedef_system(): + """从 SaaPrompts.java 抽 GAMEDEF_SYSTEM 文本块(去 Java 文本块 12 空格缩进)。""" + java = SAA_PROMPTS.read_text(encoding="utf-8") + m = re.search(r'GAMEDEF_SYSTEM = """\n(.*?)\n\s*""";', java, re.DOTALL) + if not m: + raise RuntimeError("未能从 SaaPrompts.java 抽取 GAMEDEF_SYSTEM") + lines = [(ln[12:] if ln.startswith(" " * 12) else ln) for ln in m.group(1).split("\n")] + return "\n".join(lines) + + +def extract_json_obj(content): + """从模型输出抠最外层 {...}(去 ```json 围栏/前后说明)。""" + s = content.strip() + s = re.sub(r"^```(?:json)?\s*", "", s) + s = re.sub(r"\s*```$", "", s) + lo, hi = s.find("{"), s.rfind("}") + if lo < 0 or hi <= lo: + return None + try: + return json.loads(s[lo:hi + 1]) + except Exception: + return None + + +def assemble_via_cli(game_id, gamedef_obj): + """写 source.json → 跑 build-from-source.mjs 校验+装配 → generated-factory.js。返回 (ok, err)。""" + gdir = GEN_DIR / game_id + gdir.mkdir(parents=True, exist_ok=True) + src = gdir / "source.json" + src.write_text(json.dumps(gamedef_obj, ensure_ascii=False), encoding="utf-8") + r = subprocess.run( + ["node", "src/host/build-from-source.mjs", str(src), str(gdir / "generated-factory.js")], + cwd=str(GAME_RUNTIME), capture_output=True, text=True, timeout=60, + ) + return r.returncode == 0, (r.stdout + r.stderr).strip() + + +def run_one(game_id, gamedef_system, model): + brief_text, play_spec = run._load_brief(game_id) + user = "请为下面这款游戏产出一份 gameDefinition(只输出一个 JSON 对象,不要 ```代码块、不要解释):\n\n" + brief_text + gid = "gd-" + game_id + out = {"game_id": gid, "stage": None, "pass": False, "guards": {}, "err": ""} + try: + resp = _client.chat(model, gamedef_system, user, max_tokens=16000) + except Exception as e: + out["stage"] = "generate"; out["err"] = str(e)[:200]; return out + out["tokens"] = (resp.get("prompt_tokens"), resp.get("completion_tokens")) + gd = extract_json_obj(resp["content"]) + if gd is None: + out["stage"] = "parse"; out["err"] = "未解析出 gameDefinition JSON"; return out + ok, err = assemble_via_cli(gid, gd) + if not ok: + out["stage"] = "validate/assemble"; out["err"] = err[:400]; return out + # 复用 run.py 的 scaffold 余下(拷模板 + play-spec)+ build + play(factory 已由 CLI 写好)。 + gdir = GEN_DIR / gid + shutil.copyfile(GEN_DIR / "_shared" / "entry-bundle.template.js", gdir / "entry-bundle.js") + shutil.copyfile(GEN_DIR / "_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") + bok, blog = run.build(gid) + if not bok: + out["stage"] = "build"; out["err"] = blog[:400]; return out + rc, _, verdict = run.play(gid) + out["stage"] = "play" + out["guards"] = {k: g.get("pass") for k, g in ((verdict or {}).get("guards") or {}).items()} + out["pass"] = bool(rc == 0 and verdict and verdict.get("pass")) + if not out["pass"]: + out["err"] = run._verdict_feedback(verdict)[:300] + return out + + +def main(): + games = sys.argv[1:] or ["runner", "flappy", "whack", "pong"] + model = _client.DEFAULT_MODEL + gds = extract_gamedef_system() + print(f"[gamedef-qc] model={model} games={games} GAMEDEF_SYSTEM={len(gds)}字 (extracted from SaaPrompts.java)\n") + results = [] + for g in games: + r = run_one(g, gds, model) + results.append(r) + mark = "✅ PASS" if r["pass"] else f"❌ {r['stage']}" + gpass = sum(1 for v in r["guards"].values() if v) + gtot = len(r["guards"]) + print(f" {r['game_id']:14s} {mark:16s} gates={gpass}/{gtot} {('' if r['pass'] else r['err'][:120])}") + npass = sum(1 for r in results if r["pass"]) + print(f"\n[gamedef-qc] 过门 {npass}/{len(results)} (cutover 门基线=iife 60%; 本为 lili-mac 小样早读, authoritative=mini-desktop)") + Path(__file__).resolve().parent.joinpath("results", "gamedef-quickcheck.json").write_text( + json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8") + sys.exit(0) + + +if __name__ == "__main__": + main()