"""m3_player_test.py —— 用 MiniMax-M3(视觉)跑生产 player 判定的离线测试。 目的:验证「便宜视觉模型 M3」能否担当生产三层的 player 层(看首帧/玩后截图 + 九门数据判可玩/好玩), 与「确定性九门」「Opus 审计」三方对照。production-faithful:用 roles.player_system 同款人格 + 同款多模态消息, 不引 agentscope(只用 _client/openai 兼容网关)。读各游戏已存 evidence,不重开服务=便宜可复跑。 """ import base64 import json import sys import time from pathlib import Path ROOT = Path(__file__).resolve().parent # wg1/gen-worker sys.path.insert(0, str(ROOT)) from worker import _client, run # noqa: E402 (裸客户端 + GEN_DIR;均不引 agentscope) from worker.agent_loop import roles # noqa: E402 (纯 prompt 字符串) try: from json_repair import repair_json except ImportError: repair_json = None VIS_MODEL = "MiniMax-M3" # 用户指定:M3 有视觉能力,作生产 player 视觉位 PERSONA = "急性子休闲玩家,凭第一眼观感和反馈下判断" GAMES = ["tictactoe", "saolei", "jianfeng", "breakout", "runner", "invaders", "doodle", "flappy", "tetris"] def b64(p): p = Path(p) return "data:image/png;base64," + base64.b64encode(p.read_bytes()).decode() if p.exists() else None def play_summary(v): """把九门 verdict 摘成给 player 的运行数据(同 studio._play_summary 口径)。""" if not v: return "(无 verdict:疑似装载即崩)" g = v.get("guards") or {} return "九门真玩取证:\n" + "\n".join( "- %s: %s %s" % (k, "过" if x.get("pass") else "未过", json.dumps(x, ensure_ascii=False)[:140]) for k, x in g.items() ) def parse(raw): obj = None if repair_json is not None: try: obj = repair_json(raw, return_objects=True) except Exception: obj = None if not isinstance(obj, dict): try: obj = json.loads(raw) except Exception: obj = None return obj if isinstance(obj, dict) else {"raw": (raw or "")[:160]} def main(): client = _client.get_client() out = [] for g in GAMES: bp = ROOT / "briefs" / (g + ".json") brief = json.loads(bp.read_text(encoding="utf-8"))["brief"] if bp.exists() else g vp = run.GEN_DIR / g / "evidence" / "verdict.json" v = json.loads(vp.read_text(encoding="utf-8")) if vp.exists() else None gate = bool(v and v.get("pass")) user_text = (f"游戏题面:\n{brief}\n\n{play_summary(v)}\n\n" "请基于以上证据(及截图,若有)给出你的玩家评判。") content = [{"type": "text", "text": user_text}] ev = run.GEN_DIR / g / "evidence" for fn in ("first-paint.png", "after-play.png"): u = b64(ev / fn) if u: content.append({"type": "text", "text": "【%s】" % fn}) content.append({"type": "image_url", "image_url": {"url": u}}) msgs = [{"role": "system", "content": roles.player_system(PERSONA)}, {"role": "user", "content": content}] t0 = time.perf_counter() try: r = client.chat.completions.create(model=VIS_MODEL, messages=msgs, max_tokens=900, temperature=0.3, stream=False) j = parse(r.choices[0].message.content or "") u = r.usage row = {"game": g, "gate": "PASS" if gate else "FAIL", "m3": {k: j.get(k) for k in ("completeness", "fun", "verdict", "note")}, "tok": [u.prompt_tokens, u.completion_tokens], "wall_s": round(time.perf_counter() - t0, 1)} except Exception as e: row = {"game": g, "gate": "PASS" if gate else "FAIL", "m3": {"error": str(e)[:200]}} out.append(row) print("%-11s gate=%-4s M3=%s" % (row["game"], row["gate"], json.dumps(row["m3"], ensure_ascii=False)[:220])) (ROOT / "calibration" / "m3_test.json").write_text(json.dumps(out, ensure_ascii=False, indent=2), encoding="utf-8") print("\nsaved → wg1/gen-worker/calibration/m3_test.json") if __name__ == "__main__": main()