Spaces:
Sleeping
Sleeping
| """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() | |