"""把评测机一局的日志归档到 ``examples/replay/``,供无 referee-data 时复现插图。 ``referee-data/`` 与 ``output/`` 都在 .gitignore 里:本机清掉评测机数据后, ``docs/figures/*.png`` 就无法复现。本脚本把"作图用到的那几局"抽出来, 命名成 ``..jsonl`` 存进 ``examples/replay/``(入库、几万行以内), ``scripts/make_figures.py`` 在没有评测机数据时自动改读这里。 uv run python scripts/archive_replay_logs.py # 归档作图默认选中的两局 uv run python scripts/archive_replay_logs.py BBM9-HZG2-77N5-NKKC # 指定案例 uv run python scripts/archive_replay_logs.py --list # 只看有哪些局 归档是**内容等价**的:只做 JSON 重新格式化(逐行 compact 输出),不改字段, 并在文件头写入一条 ``session_start``(评测机日志本就自带),保持解析兼容。 """ from __future__ import annotations import argparse import json import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SESSIONS_DIR = ROOT / 'referee-data' / 'sessions' ARCHIVE_DIR = ROOT / 'examples' / 'replay' sys.path.insert(0, str(Path(__file__).resolve().parent)) from make_figures import discover_runs, pick_run # noqa: E402 def archive(run: dict, out_dir: Path) -> Path: """把一局日志写成归档 JSONL:逐行原样紧凑输出(字段不变,只重新格式化)。""" out_dir.mkdir(parents=True, exist_ok=True) target = out_dir / f"{run['case_code']}.{run['module']}.jsonl" lines: list[str] = [] for line in run["source"].read_text(encoding='utf-8').splitlines(): line = line.strip() if not line: continue try: doc = json.loads(line) except json.JSONDecodeError: continue lines.append(json.dumps(doc, ensure_ascii=False, separators=(',', ':'))) target.write_text('\n'.join(lines) + '\n', encoding='utf-8') return target def main() -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument('cases', nargs='*', help='案例编码;缺省=作图默认选中的那几局') parser.add_argument('--list', action='store_true', help='列出可选案例后退出') parser.add_argument('--out', default=str(ARCHIVE_DIR)) args = parser.parse_args() runs = discover_runs() if not runs: print('没有找到可归档的日志:referee-data/sessions/*/log.jsonl 与 ' 'examples/replay/*.jsonl 都为空。') return 1 if args.list: for run in runs: clears = sum(1 for a in run["actions"] if a["path"] == "/clear") print(f"{run['case_code']} {run['module']:<12} 真值 {len(run['truth']):2d} " f"动作 {len(run['actions']):4d} 清除 {clears:3d}") return 0 picked = [] if args.cases: wanted = set(args.cases) picked = [r for r in runs if r["case_code"] in wanted] missing = wanted - {r["case_code"] for r in picked} if missing: print(f'未找到案例:{sorted(missing)}') return 1 else: for module, truth in (('q3', 10), ('q4', 12)): run = pick_run(runs, module=module, truth=truth) if run and run not in picked: picked.append(run) for run in picked: path = archive(run, Path(args.out)) print(f"[archive] {run['case_code']:<20} {run['module']:<12} " f"真值 {len(run['truth']):2d} 动作 {len(run['actions']):4d} → " f"{path.relative_to(ROOT)}") return 0 if __name__ == '__main__': raise SystemExit(main())