File size: 5,612 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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
"""Nghiệm thu engine app trên bàn random — bản V2 (31/07/2026).

Gate giữ NGUYÊN nghĩa từ phase B: (1) KHÔNG cú gợi ý nào phạm luật,
(2) search median < 5s/cú. Engine đo là ZonePlanner V2 (`recommend_v2`) —
đường duy nhất còn trên `main` từ 31/07; bản đo oracle/hybrid sống ở nhánh
`v1-full` (mọi đối chứng sau này chạy ở đó, không phải ở đây).

Bàn: nửa đầu đủ 10 bi (cue + 1..9), nửa sau mid-game (cue + subset ngẫu
nhiên 2-6 bi, luôn giữ bi 9). Mỗi bàn gọi `recommend_v2` → check TỪNG cú
trả về, độc lập với máy lọc của chính V2: foul=False, scratch=False, pot
đúng target, KHÔNG bi nào khác rơi kèm (tiêu chí (i) — ăn trực tiếp).

Số "bàn hết đường" ở khối cuối KHÔNG phải lỗi: V2 cố ý không thang nới
(design §8.1), tỉ lệ này là số phải báo cáo cùng mọi kết quả khác.

Chạy:  python scripts/eval_fullrack.py --tables 20
"""

from __future__ import annotations

import argparse
import statistics
import sys
import time
from pathlib import Path

import numpy as np

sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")

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


def _place_random(rng, w, l, n, ball_r, min_gap=1.1):
    pts = []
    while len(pts) < n:
        p = np.array([rng.uniform(2 * ball_r, w - 2 * ball_r),
                      rng.uniform(2 * ball_r, l - 2 * ball_r)])
        if all(np.linalg.norm(p - q) >= 2 * ball_r * min_gap for q in pts):
            pts.append(p)
    return pts


def _gen_table(rng, w, l, ball_r, full):
    if full:
        ids = [str(i) for i in range(1, 10)]
    else:
        n = int(rng.integers(2, 7))  # 2-6 bi mục tiêu
        lows = sorted(rng.choice(np.arange(1, 9), size=n - 1, replace=False))
        ids = [str(int(i)) for i in lows] + ["9"]  # luôn giữ bi 9
    pts = _place_random(rng, w, l, len(ids) + 1, ball_r)
    return dict(zip(["cue"] + ids, pts))


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--tables", type=int, default=20)
    ap.add_argument("--seed", type=int, default=42)
    ap.add_argument("--alternatives", type=int, default=3)
    args = ap.parse_args()

    from poolcoach_rl.envs import PositionPlayEnv
    from poolcoach_rl.envs.position_env import BALL_R
    from poolcoach_rl.recommend import recommend_v2, warmup

    env_h = PositionPlayEnv()
    w, l = env_h.w, env_h.l
    rng = np.random.default_rng(args.seed)

    print("JIT warmup process chính...", flush=True)
    print(f"  xong sau {warmup(env_h):.1f}s")

    times, violations = [], []
    n_with_shot, n_win_top, rolls, v0s = 0, 0, [], []
    for k in range(args.tables):
        full = k < args.tables // 2
        balls = _gen_table(rng, w, l, BALL_R, full)
        t0 = time.time()
        res = recommend_v2(balls, env_h=env_h, alternatives=args.alternatives)
        dt = time.time() - t0
        times.append(dt)

        tag = "full-9" if full else f"mid-{len(balls) - 1}bi"
        for s in res.shots:
            # re-check độc lập từng điều kiện (không tin máy lọc của V2)
            errs = []
            if s.foul:
                errs.append("foul=True")
            if s.scratch:
                errs.append("scratch")
            if s.target not in s.potted:
                errs.append("không pot được bi target")
            if sorted(s.potted) != [s.target]:
                errs.append(f"rơi kèm bi khác {s.potted} — vi phạm (i)")
            for e in errs:
                violations.append(f"bàn {k} ({tag}) rank {s.rank}: {e}")
        if res.shots:
            n_with_shot += 1
            top = res.shots[0]
            n_win_top += int(top.win)
            rolls.append(float(top.roll_len))
            v0s.append(float(top.v0))
            d_txt = ("cuối ván" if top.d_land is None
                     else f"d {top.d_land:.2f} m")
            print(f"  bàn {k:2d} {tag:8s}: {dt:5.2f}s, target {res.target}, "
                  f"{res.n_legal_pot:3d} cú đạt tiêu chí, top lăn "
                  f"{top.roll_len:.2f} m, {d_txt}"
                  f"{' WIN' if top.win else ''}", flush=True)
        else:
            print(f"  bàn {k:2d} {tag:8s}: {dt:5.2f}s, target {res.target}, "
                  f"HẾT ĐƯỜNG ({res.n_results} sim) — nằm trong dự tính V2",
                  flush=True)

    med, mx = statistics.median(times), max(times)
    n_dry = args.tables - n_with_shot
    print("\n=== NGHIỆM THU ENGINE V2 ===")
    print(f"  {args.tables} bàn | search median {med:.2f}s | max {mx:.2f}s")
    print(f"  bàn có cú gợi ý  : {n_with_shot}/{args.tables}"
          f"   (hết đường: {n_dry} — SỐ PHẢI BÁO, không phải lỗi)")
    print(f"  top-1 WIN        : {n_win_top}")
    if rolls:
        print(f"  đường lăn top-1  : mean {statistics.mean(rolls):.2f} m"
              f"   max {max(rolls):.2f} m")
        print(f"  V0 top-1         : mean {statistics.mean(v0s):.2f}"
              f"   max {max(v0s):.2f}   (soi bão hoà biên §8.4(d))")
    print(f"  cú phạm luật trong gợi ý: {len(violations)}")
    for v in violations:
        print(f"    VI PHẠM: {v}")
    ok_legal, ok_speed = not violations, med < 5.0
    print(f"  [{'ĐẠT' if ok_legal else 'FAIL'}] 0 cú phạm luật"
          f"   [{'ĐẠT' if ok_speed else 'FAIL'}] search median < 5s (serial)")
    sys.exit(0 if (ok_legal and ok_speed) else 1)


if __name__ == "__main__":
    main()