"""Quét seed bàn mẫu "thoáng" cho app — BRIEF 31/07/2026 việc B.5. Sinh bàn 9 bi ngẫu nhiên (ĐÚNG generator ``_gen_table`` của eval_fullrack — import, không copy), rồi TỰ CHƠI bằng chính zone V2 trong sim: mỗi cú lấy top-1 của ``recommend_v2``, áp ``balls_final``, lặp tới khi ăn bi 9 (thắng) hoặc hết đường. Seed nào chạy HẾT BỘ mới vào danh sách; app runtime chỉ bốc ngẫu nhiên trong danh sách đó (``app/static/demo_boards.json``). Sim tất định ⇒ seed đạt là đạt mãi — VỚI ĐÚNG BẢN TIÊU CHÍ NÀY (design §8 V2, 31/07/2026). Đổi tiêu chí là phải quét lại; trường ``criteria`` trong JSON tồn tại để lần đổi đó không im lặng. KHÔNG PHẢI EVAL (BRIEF bối cảnh 11b): đây là curation cho demo/UX. Mọi phép đo sau này dùng seed riêng của script eval như cũ — file này không được dùng chung với chúng, kẻo "tỉ lệ chạy hết" của demo bị đọc nhầm thành số đo. Số PHẢI BÁO trong HANDOFF: quét bao nhiêu seed, đạt bao nhiêu — tỉ lệ này chính là số "hết đường tự nhiên" trên bàn ngẫu nhiên (Bối cảnh 4). python scripts/gen_demo_boards.py --want 10 --max-seeds 100 """ from __future__ import annotations import argparse import json import sys import time from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "src")) sys.path.insert(0, str(ROOT)) # import scripts.eval_fullrack OUT = ROOT / "app" / "static" / "demo_boards.json" MAX_SHOTS = 12 # ván 9-ball tối đa 9 cú; dư là có gì đó sai def self_play(env_h, balls): """Tự chơi bằng V2. Trả (ok, n_shots, reason).""" import numpy as np from poolcoach_rl.recommend import recommend_v2 cur = {bid: np.asarray(xy, dtype=np.float64) for bid, xy in balls.items()} for i in range(MAX_SHOTS): res = recommend_v2(cur, env_h=env_h, alternatives=0, n_render=0) if not res.shots: return False, i, f"het duong o cu {i + 1} (target {res.target})" s = res.shots[0] if s.win: return True, i + 1, "an bi 9" cur = {bid: np.asarray(xy, dtype=np.float64) for bid, xy in s.balls_final.items() if xy is not None} if not any(b != "cue" for b in cur): # hết bi mà chưa win — bàn không có bi 9? không xảy ra với full return False, i + 1, "het bi ma khong win" return False, MAX_SHOTS, "qua 12 cu — vong lap bat thuong" def main(): ap = argparse.ArgumentParser() ap.add_argument("--want", type=int, default=10, help="số bàn đạt cần gom đủ thì dừng") ap.add_argument("--max-seeds", type=int, default=100, help="trần số seed quét (chặn thời gian chạy)") ap.add_argument("--start-seed", type=int, default=1000, help="seed đầu; CỐ Ý xa dải seed của các script eval " "(eval dùng 7/42/123...) — bối cảnh 11b") args = ap.parse_args() import numpy as np from poolcoach_rl.envs import PositionPlayEnv from poolcoach_rl.envs.position_env import BALL_R from poolcoach_rl.recommend import warmup from scripts.eval_fullrack import _gen_table env_h = PositionPlayEnv() print("[demo] warmup JIT (~40s lan dau)...", flush=True) warmup(env_h) boards, scanned = [], 0 t0 = time.time() for seed in range(args.start_seed, args.start_seed + args.max_seeds): if len(boards) >= args.want: break scanned += 1 rng = np.random.default_rng(seed) balls = _gen_table(rng, env_h.w, env_h.l, BALL_R, full=True) ok, n, reason = self_play(env_h, balls) print(f"[demo] seed {seed}: {'DAT' if ok else 'rot'} " f"({n} cu, {reason})", flush=True) if ok: boards.append({ "seed": seed, "n_shots": n, "balls": {bid: [round(float(xy[0]), 6), round(float(xy[1]), 6)] for bid, xy in balls.items()}, }) out = { "criteria": "ZonePlanner V2 - design S8, chot 31/07/2026 " "(loc i-v, xep roll_len -> |d-1m| -> enumerate)", "note": "Curation cho demo/UX, KHONG phai eval (BRIEF 31/07 boi canh " "11b). Doi tieu chi la phai quet lai bang " "scripts/gen_demo_boards.py.", "generator": "scripts.eval_fullrack._gen_table, seed ghi tung bang", "scanned": scanned, "passed": len(boards), "boards": boards, } OUT.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8") el = time.time() - t0 print(f"\n[demo] quet {scanned} seed -> {len(boards)} dat " f"({100.0 * len(boards) / max(scanned, 1):.0f}%) trong {el:.0f}s") print(f"[demo] ghi {OUT}") return 0 if boards else 1 if __name__ == "__main__": sys.exit(main())