게시판SGF 기보로 학습하는 바둑 LLM 인공지능예제 소스

예제 소스

SGF 기보로 학습하는 바둑 LLM 인공지능 · 파일 169개

play_two_models.py

#!/usr/bin/env python3
"""모델 목록에서 흑·백을 골라 대국한다.

수 상한 0은 안전 상한 1000수다. 예:

  python scripts/play_two_models.py --black gemma4 --white r5 --games 5 --max-moves 0
"""

from __future__ import annotations

import argparse
import importlib.util
import json
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))

SAFETY_PLIES = 1000
MOVE_TIMEOUT_S = 360.0


def _load(name: str, path: Path):
    spec = importlib.util.spec_from_file_location(name, path)
    if spec is None or spec.loader is None:
        raise RuntimeError(f"import failed: {path}")
    mod = importlib.util.module_from_spec(spec)
    sys.modules[name] = mod
    spec.loader.exec_module(mod)
    return mod


mon = _load("monitor_mariago_web_play", ROOT / "scripts" / "monitor_mariago_web_play.py")
bridge = _load("mariago_ws_bridge", ROOT / "scripts" / "mariago_ws_bridge.py")
mdb = _load("match_db", ROOT / "scripts" / "match_db.py")

from rl_env.board import estimate_area_score  # noqa: E402


def now() -> str:
    return datetime.now(timezone.utc).isoformat(timespec="seconds")


def load_models() -> dict:
    path = ROOT / "scripts" / "models.json"
    data = json.loads(path.read_text(encoding="utf-8"))
    return data["models"]


def ask(spec: dict, board: list[int], side: int, epoch: int, rejects: list[tuple[int, int]]) -> dict:
    return bridge.request_move_sync(
        "127.0.0.1",
        int(spec["port"]),
        board,
        side,
        epoch,
        rejects,
        MOVE_TIMEOUT_S,
    )


def play_turn(spec: dict, board: list[int], side: int, epoch: int, ko) -> tuple[str, Optional[int], Optional[int], list[int], object, str]:
    """한 수. 반환: kind, row, col, new_board, new_ko, detail."""
    rejects: list[tuple[int, int]] = []
    last = ""
    for _ in range(2):
        resp = ask(spec, board, side, epoch, rejects)
        if resp.get("type") == "error":
            last = str(resp.get("error") or "error")
            continue
        if resp.get("pass"):
            return "pass", None, None, board, ko, "pass"
        row, col = int(resp["row"]), int(resp["col"])
        ok, reason, nxt, _cap, new_ko = mon.try_play(board, row, col, side, ko)
        if ok:
            return "move", row, col, nxt, new_ko, "ok"
        last = reason
        rejects.append((row, col))
    return "illegal", None, None, board, ko, last


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--black", required=True, help="scripts/models.json 의 모델 이름")
    ap.add_argument("--white", required=True)
    ap.add_argument("--games", type=int, default=5)
    ap.add_argument("--max-moves", type=int, default=0, help="0이면 1000수 안전 상한")
    ap.add_argument("--out-dir", default="logs/model_match")
    args = ap.parse_args()

    models = load_models()
    for name in (args.black, args.white):
        if name not in models:
            raise SystemExit(f"unknown model {name}; have {sorted(models)}")
    black = models[args.black]
    white = models[args.white]
    cap = SAFETY_PLIES if args.max_moves <= 0 else args.max_moves
    out = ROOT / args.out_dir
    out.mkdir(parents=True, exist_ok=True)

    db = mdb.MatchDB(out / "matches.sqlite")
    run_id = db.start_run(
        now(),
        {"black": args.black, "white": args.white, "games": args.games, "max_moves": args.max_moves},
        f"{args.black}_vs_{args.white}",
        None,
    )
    print(f"[match] run={run_id} {args.black}(B:{black['port']}) vs {args.white}(W:{white['port']}) games={args.games} cap={cap}", flush=True)

    judge = None
    try:
        judge = mdb.FinalJudge(visits=64)
    except Exception as exc:
        print(f"[match] katago judge off: {exc}", flush=True)

    for game_id in range(1, args.games + 1):
        board = mon.empty_board()
        ko = None
        epoch = game_id
        db.start_game(game_id, epoch, now())
        passes = 0
        fails = 0
        issues: list[str] = []
        ply = 0
        color_turn = 0
        end = "max_plies"
        print(f"[match] game {game_id} start", flush=True)
        while ply < cap:
            side_name = "B" if color_turn % 2 == 0 else "W"
            side = mon.BLACK if side_name == "B" else mon.WHITE
            spec = black if side_name == "B" else white
            t0 = time.time()
            kind, row, col, nxt, ko, detail = play_turn(spec, board, side, epoch, ko)
            dt = time.time() - t0
            db.add_move(
                game_id=game_id,
                ply=ply,
                color=side_name,
                kind=kind,
                board=board,
                ts=now(),
                row=row,
                col=col,
                dt=dt,
                attempt="first",
                server_rec={"model": args.black if side_name == "B" else args.white, "detail": detail},
            )
            ply += 1
            if kind == "illegal":
                fails += 1
                issues.append(f"{side_name}_illegal:{detail}")
                print(f"[match] g{game_id} ply {ply} {side_name} illegal {detail}", flush=True)
                if fails >= 3:
                    end = "error"
                    break
                continue
            fails = 0
            color_turn += 1
            if kind == "pass":
                passes += 1
                print(f"[match] g{game_id} ply {ply} {side_name} pass ({dt:.0f}s)", flush=True)
                if passes >= 2:
                    end = "double_pass"
                    break
                continue
            passes = 0
            board = nxt
            if ply % 20 == 0:
                print(f"[match] g{game_id} ply {ply} {side_name} ({row},{col}) {dt:.0f}s", flush=True)

        b_area, w_area, _dame = estimate_area_score(bytes(int(v) & 0xFF for v in board), 19)
        lead = None
        if judge is not None and end != "error":
            try:
                lead = judge.lead_black(board)
            except Exception as exc:
                print(f"[match] judge failed: {exc}", flush=True)
        if lead is None:
            margin = b_area - (w_area + 6.5)
            winner = "black" if margin > 0 else "white" if margin < 0 else "draw"
        else:
            winner = "black" if lead > 0 else "white" if lead < 0 else "draw"
        score = {"winner": winner, "black_score": b_area, "white_score": w_area + 6.5, "katago_lead_b": lead}
        db.finish_game(game_id, ts=now(), end_reason=end, plies=ply, score=score, issues=issues, katago_lead_b=lead)
        print(f"[match] game {game_id} {end} plies={ply} winner={winner} area={b_area}-{w_area + 6.5:.1f} lead={lead}", flush=True)

    db.close()
    print(f"[match] done {out / 'matches.sqlite'}", flush=True)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())