File size: 5,108 Bytes
78738de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""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())