games-development-ai/cheap-worker/hard_genre_batch.py
lili 85f48228cd
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
feat(cheap-gen): W-AXIS 波3 知识层补强+收尾——tsc 顾问门+recipes+暗资产接线+n=5 基线台账
- tsc 顾问门:typescript devDependency + tsc-advisory 测试档;顾问性(永不阻断),
  金标 10 款零误伤抽验
- recipes/ 五篇高频场景索引(命中矩形/计时器/资产回退/场景机/结算演出,「抄这段形态
  +头号病根」)+ littlejs-game-dev §0.5 卡壳就查接线
- 暗资产接线:plugin-capability-map(11 注入+1 取证+6 储备逐件裁定)+
  engine-capabilities-brief(≤3KB 蒸馏)+ api.d.ts/game-host.d.ts 补插件指针;
  三方一致性对账门 plugin-surface-gate.py(含 cwd 锚定修)挂 pre-commit+Gitea Actions
- hard_genre_batch 参数化(五品类 n=5 批跑驱动,checkpoint 续跑)
- n=5 基线台账入库(gitignore 例外 add -f,只入台账不入游戏产物):
  wax-baseline.jsonl 25 局逐局记录(含判定仪器订正批注:cap1500 重判/图像盲 M3 重判,
  只加字段零改值)——终数:操作口径 14/25=56%、质量口径 18/25=72%;
  品类 narrative/heritage 5/5、trpg 4/5、puzzle/sim-business 2/5(缺口逐局有名有姓,
  根因见 07-10 v2 plan §1);旧「80%/39%」口径正式作废存照

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 04:27:18 -07:00

200 lines
12 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.

"""hard_genre_batch.py — 便宜档【5 已覆盖品类】n=5 重测基线批跑W-AXIS 收尾)。
2026-07-09 换轴后重锚:验收权威 = accepted = 机械九门预筛 ∧ 独立模型玩法判定(glm-5.2 看真玩证据),
verdictPass 只是预筛地板、finished 只是模型自称收敛。本脚本作废换轴前那批全绿的污染数字(旧口径把
「九门 pass」当验收通过),在新链路(turns.jsonl 真相层 + per-run 归档 + 三层归因)与新语义下重测。
品类 = 5 个【有 per-genre 模板】的已覆盖品类(heritage/trpg/sim-business/puzzle/narrative);idle/action
无模板品类不入本轮(归 W-GENRE)。每品类各跑 n=5 局(distinct gid `hard-<genre>-r<round>`,绝不同 gid 重跑
洗数字——同 gid 会触发归档覆盖)。串行跑(本机 CPU+chrome 负载,别开并发)。
跑法(分级防无人值守烧钱):
冒烟 n=1×5: cheap-worker/.venv/bin/python cheap-worker/hard_genre_batch.py 1 1
全量补 r2-5: cheap-worker/.venv/bin/python cheap-worker/hard_genre_batch.py 4 2
参数:argv[1]=本次跑几轮(默认 1) argv[2]=起始轮号(默认 1);gid 后缀 = r<轮号>。
env:NO_PROXY(网关直连绕代理) + NEWAPI_KEY(_bootstrap 自动从凭据档解析)。
每局逐行追加 results/wax-baseline.jsonl(断点续跑不丢已完成局);累计成本超 ¥200 硬停(PARTIAL)。
"""
import asyncio
import json
import sys
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import cheap_studio # noqa: E402
from cheap_genre_route import route_genre # noqa: E402 brief→per-genre 黄金模板路由(对齐生产 create 路)
from cheap_roles import SCAFFOLD_DESC_BY_TEMPLATE # noqa: E402 per-genre 起点一句话描述
from worker import genconfig # noqa: E402 与生产同读 genconfigmax_tokens 单一源)
# max_tokens 与生产 create 路cheap_service_driver同源 genconfig.model.max_tokens2026-07-09 调 32K16K 是自设限、
# 实测 M3 单轮能输出 17000+ tokensinline thinking 吃额度致大文件整写截断——给 thinking+代码留足)。别用 run_studio 的 16K 默认。
_MAXTOK = genconfig.get("model", "max_tokens", 16000)
# 累计成本硬停线(¥):无人值守跑 25 局,任何一轮累计越线立即停机 PARTIAL(单局另有 ¥10 软/¥15 硬 breaker,此为跨局兜底)。
_HARD_STOP_RMB = 200.0
# 结果逐行落盘(断点续跑不丢):每完成一局即 append 一行 JSON。
_JSONL = Path(__file__).resolve().parent / "results" / "wax-baseline.jsonl"
# 5 个【已覆盖品类】brief——各命中一个 per-genre 黄金模板(route_genre 关键词确定性路由),取自批跑器既有 brief 池:
# narrative/trpg/heritage 用同题 brief(HARD 池);puzzle/sim-business 用跨主题 brief(XTHEME 池,该两类同题无 brief)。
# 路由核对:文字冒险/分支→story、掷骰→trpg、非遗→feiyi、华容道/解谜→puzzle、经营/奶茶店→shop。
COVERED_BRIEFS = [
("narrative", "一个分支选择推进的互动文字冒险:每屏一段剧情+2-3个选项,选择影响走向,最终3个不同结局,有结局图鉴"),
("trpg", "一个掷骰战斗的地牢爬塔:每层遇一个敌人,点击掷骰(可见骰点)按攻防结算,击败进下一层,失败结算,每3层升级选天赋"),
("heritage", "一个还原传统榫卯木工的非遗小游戏:按工序节拍(选料→画线→凿卯→修整)逐步操作,每步有节奏判定,完成得成品评分"),
("puzzle", "一个数字华容道解谜:滑动数字块把它们按序归位,规则逐关递进,卡壳给提示,通关计步数炫耀成绩"),
("sim-business", "一个经营奶茶店:顾客排队点单,按配方调对饮品上对得分,收银攒钱升级解锁新品与自动出杯设备"),
]
def _row_total_cost(row: dict) -> float:
"""一局的总成本 = 生成 costRmb + 判定 judge.costRmb(判定用独立档,与生成台账隔离,累计兜底要两者都算)。"""
gen = row.get("costRmb") or 0.0
j = row.get("judge") or {}
jc = j.get("costRmb") or 0.0
try:
return float(gen) + float(jc)
except (TypeError, ValueError):
return 0.0
def _prior_cumulative() -> float:
"""读已落盘 JSONL 里所有历史局的累计成本(跨多次调用续跑时,¥200 兜底要含之前轮次已花)。"""
if not _JSONL.is_file():
return 0.0
total = 0.0
for line in _JSONL.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line:
continue
try:
total += _row_total_cost(json.loads(line))
except json.JSONDecodeError:
continue
return total
async def _run_one(genre, brief, port, cdp, round_no):
"""跑一局并返回批次账行(串行,无 semaphore)。gid=hard-<genre>-r<round>,distinct、绝不同 gid 重跑。"""
gid = f"hard-{genre}-r{round_no}"
t0 = time.time()
# 对齐生产 create 路:brief→route_genre 选 per-genre 黄金模板作起点 + scaffold_desc 描述引导。
scaffold_template, routed_genre = route_genre(brief)
# 不写锁(2026-07-08/09 创始人纠偏,W-AXIS 波2 拆 reskin 管路):game-logic/core/render/assets 全放开,
# 只锁绝对通用 plumbing=L1_FIXED;写坏靠 harness 门 + 独立判定兜、不靠限死模型的手。
write_whitelist = None
scaffold_desc = SCAFFOLD_DESC_BY_TEMPLATE.get(scaffold_template) if scaffold_template else None
try:
summary = await cheap_studio.run_studio(
gid, brief, run_gates=True, port=port, cdp_port=cdp,
scaffold_template=scaffold_template, scaffold_desc=scaffold_desc,
write_whitelist=write_whitelist, genre=routed_genre, max_tokens=_MAXTOK)
except Exception as e: # noqa: BLE001 harness 级异常也原样计入基线(失败也是基线的一部分)
return {"genre": genre, "gid": gid, "round": round_no,
"accepted": False, "verdictPass": None, "finished": False, "ok": False,
"judge": None, "fail": f"{type(e).__name__}: {e}", "wallSec": round(time.time() - t0, 1)}
v = summary.get("verdict") or {}
rich = summary.get("richness") or {}
layer = summary.get("failureLayer") or {} # classify_cheap_failure_layer 返回 {layer,reason,failedGates}
j = summary.get("judge") or {} # apply_gameplay_judge 写入(仅 run_gates∧finished 时有)
return {
"genre": genre, "gid": gid, "round": round_no,
"template": scaffold_template or "_template(通用)",
# ── 三个验收信号分开记(2026-07-09 新语义,别混):
"accepted": bool(summary.get("accepted")), # ★最终权威 = 预筛 ∧ 独立模型玩法判定
"verdictPass": v.get("pass"), # 预筛(机械九门),只是地板不是验收
"finished": bool(summary.get("finished")), # 模型自称收敛(check+build 绿),不是验收
"ok": bool(summary.get("ok")), # run_studio 收口 ok(blocking 下 = accepted)
# ── 判定段(verdict/rejectClasses/degraded/costRmb + 诊断):
"judge": {
"ran": j.get("ran"), "verdict": j.get("verdict"),
"rejectClasses": j.get("rejectClasses"), "degraded": j.get("degraded"),
"costRmb": j.get("costRmb"), "reason": j.get("reason"),
"model": j.get("model"), "frames": j.get("frames"),
} if j else None,
# ── 失败归因三层(driver_contract/mechanical/gameplay/none)+ 归档指针:
"failedGates": v.get("failedGates"),
"failureLayer": layer.get("layer"),
"failureReason": layer.get("reason"),
"archivedPriorRunTo": summary.get("archivedPriorRunTo"), # distinct gid 首跑恒 None
"attempts": summary.get("attempts"),
"breaker": summary.get("breaker"),
"costRmb": summary.get("costRmb"), # 生成成本(不含判定)
"richness": rich.get("score"), "richMax": rich.get("max"),
"wallSec": round(time.time() - t0, 1),
}
def _mark(r: dict) -> str:
"""一局的达标标记(accepted 口径):
✅ accepted=True(最终权威过)
⚠️判定拒 预筛过(verdictPass=True)但判定拒(accepted=False)——机械门放行、独立判定逮住的坏游戏
finished-only 模型自称收敛(finished=True)但预筛未过(旧 FALSE-PASS 改名,新语义)
❌ 未收敛/harness 挂
"""
if r.get("accepted"):
return ""
if r.get("verdictPass") is True:
return "⚠️判定拒"
if r.get("finished"):
return "finished-only"
return ""
async def main():
n_rounds = int(sys.argv[1]) if len(sys.argv) > 1 else 1
start_round = int(sys.argv[2]) if len(sys.argv) > 2 else 1
_JSONL.parent.mkdir(exist_ok=True)
cumulative = _prior_cumulative()
print(f">>> W-AXIS n=5 重测基线:本次跑 {n_rounds} 轮(r{start_round}..r{start_round + n_rounds - 1})× {len(COVERED_BRIEFS)} 品类,"
f"串行;已落盘历史累计成本 ¥{cumulative:.2f}(硬停线 ¥{_HARD_STOP_RMB:.0f})")
session_rows = []
stopped = False
for round_no in range(start_round, start_round + n_rounds):
if stopped:
break
for cat_i, (genre, brief) in enumerate(COVERED_BRIEFS):
# 串行:每局固定 port(同 index),前一局 chrome 已收才起下一局。
r = await _run_one(genre, brief, 4340 + cat_i * 2, 9242 + cat_i * 2, round_no)
session_rows.append(r)
# 逐行落盘(断点续跑不丢)。
with _JSONL.open("a", encoding="utf-8") as f:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
cumulative += _row_total_cost(r)
j = r.get("judge") or {}
print(f"{_mark(r):>13s} r{round_no} {genre:14s} tpl={r.get('template','?')} "
f"accepted={r.get('accepted')} verdictPass={r.get('verdictPass')} finished={r.get('finished')} "
f"judge={j.get('verdict')}/{j.get('rejectClasses')} degraded={j.get('degraded')} "
f"layer={r.get('failureLayer')} attempts={r.get('attempts')} "
f"genCost=¥{r.get('costRmb')} judgeCost=¥{j.get('costRmb')} wall={r.get('wallSec')}s "
f"累计=¥{cumulative:.2f} {('FAIL='+r['fail']) if r.get('fail') else ''}")
# ¥200 跨局硬停:立即停机,已完成局全在 JSONL,报 PARTIAL。
if cumulative > _HARD_STOP_RMB:
print(f"\n!!! 累计成本 ¥{cumulative:.2f} 越硬停线 ¥{_HARD_STOP_RMB:.0f} → 停机 PARTIAL(已完成 {len(session_rows)} 局本次)")
stopped = True
break
# ── 本次汇总(accepted 口径为主,verdictPass/finished 并列对照)──
n = len(session_rows)
acc = sum(1 for r in session_rows if r.get("accepted"))
vp = sum(1 for r in session_rows if r.get("verdictPass") is True)
fin = sum(1 for r in session_rows if r.get("finished"))
deg = sum(1 for r in session_rows if (r.get("judge") or {}).get("degraded"))
print("\n========== 本次汇总(新验收语义)==========")
print(f">>> 真实率(accepted 最终权威):{acc}/{n} = {acc / n:.0%}" if n else ">>> 无完成局")
if n:
print(f">>> 对照:预筛过(verdictPass) {vp}/{n}={vp / n:.0%} | 收敛(finished) {fin}/{n}={fin / n:.0%} | 判定 degraded {deg}/{n}")
print(f">>> 差额 = 预筛过但判定拒 {vp - acc} 局(机械门放行、独立判定逮住);全 finished-only={fin - vp} 局(收敛但预筛未过)")
print(f">>> 累计成本(本次+历史)= ¥{cumulative:.2f}")
# 本次聚合另存一份(便于取用);逐局真相仍以 JSONL 为准。
out = _JSONL.parent / f"wax-baseline-r{start_round}-{start_round + n_rounds - 1}.json"
out.write_text(json.dumps(session_rows, ensure_ascii=False, indent=2), encoding="utf-8")
print(f">>> 本次结果 → {out};逐局全量 → {_JSONL}")
if __name__ == "__main__":
asyncio.run(main())