lili 4836a6f519 feat(wg1-scale20): 20基准放量(14款)+ 三方门验证 + driver库泛化 + 宿主键盘缺口定位
主力批 7/8 真可玩(Opus 对抗审计 0 假绿)、压测 match3 ;失败收敛:3 键盘款(2048/Tetris/Asteroids)挂宿主缺 keydown 桥=唯一真·L0 门面缺口(已 spawn_task 交 L0)、multiball/flappy/愤怒小鸟=driver-coverage(非模型)。spec 预判 CCD/文本HUD/网格/match/手势缺口全推翻(便宜 flash 自带 swept CCD/自写网格文本)。

- driver 库 1→6 型(tap-targets/flap-to-gap/seek-x/tap-pairs/key-cycle/paddle-intercept)+ drag 输入事件 + key() Arrow 修
- M3 视觉 player 当生产过滤器实证(与门 8/9 一致、抓显性空心)→ 门(地板)+M3(主观过滤)+创始人(人锚)三层互补
- 短板量化报告定稿 + 两 skill 沉淀(game-e2e §8 driver库/门承重/键盘缺口 · cheap-model §9 模型能力画像)
- 2 个 ultracode 对抗 workflow(审计10+诊断7 agents)抓 saolei 弱断言、纠 flappy 归因(driver 非模型)、定位宿主键盘根因

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:30:06 -07:00

97 lines
4.1 KiB
Python

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