poolcoach / scripts /eval_fullrack.py
masterdanh's picture
deploy: snapshot for HF Space
78738de
Raw
History Blame Contribute Delete
5.61 kB
"""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()