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