test(wg1): gamedef 路 lili-mac 快走查驱动 + U3 cutover 门早读(pong 9/9 真过)

plan 2026-06-18-001 U3 验证:run.py 的 gamedef 版——generate(GAMEDEF_SYSTEM,从 SaaPrompts.java 抽取 drift-free)
→ build-from-source.mjs(校验 schema/引用/静态扫描+装配)→ scaffold/build/play 九门(harness 复用零改)。

早读(deepseek-v4-flash 便宜档,lili-mac 小样):
- gd-pong 9/9 真过门(声明式 gameDefinition→可玩→满九门,范式端到端证实)。
- gd-runner/whack 8/9、gd-flappy 7/9,共性缺口 G_input——本快走查跳了 design 步、多数 brief 的 play_spec
  无 controlCheck/driver(gatespec),故 G_input 无控制驱动可施=快走查 harness 工件,非 gamedef 生成缺陷
  (游戏真渲染/真构建/输入真接线:runner justTapped→jump 在;pong 满过)。
- authoritative ≥60% cutover 门(全图含 design 产 gatespec) = mini-desktop SaaFullGraphE2eTest -Dsaa.e2e=1。

密钥经 _client 读 .env(gitignored)、绝不入码;throwaway 生成游戏(gd-*)已清,不入仓。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-18 00:25:13 -07:00
parent fd5b245baa
commit d8df5f27ec

View File

@ -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.pyharness 零改
GAMEDEF_SYSTEM **从后端 SaaPrompts.java 抽取**drift-free跑的就是后端真用的 prompt
模型/网关/代理旁路复用 _clientplay-spec/brief 复用现有 briefs/*.jsongatespec driver 同口径
用法.venv/bin/python gamedef_quickcheck.py runner flappy whack pong # 默认这 4
lili-mac 快走查小样早读authoritative 60% cutover 门在 mini-desktopSaaFullGraphE2eTest
密钥经 _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 + playfactory 已由 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()