poolcoach / scripts /check_zone_v2_gates.py
masterdanh's picture
deploy: snapshot for HF Space
78738de
Raw
History Blame Contribute Delete
9.54 kB
"""Gate G3 + G4 của ZonePlanner V2 trên SIM THẬT — thế dựng tay (31/07/2026).
Vai trò: vế "sim thật" của hai gate mà ``tests/test_zone_v2.py`` chỉ khoá
được phần logic (conftest cố ý nhét stub pooltool rỗng). Chạy:
python scripts/check_zone_v2_gates.py
Kiểm những gì (BRIEF 31/07):
G3.1 thế có double-kiss / bi cái đụng bi sau va chạm → ứng viên bị (iii)
loại, và bb_events thật sự ghi cú chạm thứ hai của cue.
G3.2 (iv) khớp số đo: mọi ứng viên QUA lọc có d ∈ [0.15, 1.50]; mọi ứng
viên chết (iv) có d ngoài dải; phải thấy CẢ hai phía trên bộ thế.
G3.3 (v) so bằng giá trị grid — 0.5 loại, 0.8888... qua.
G4.1-2 cú được chọn ĐÚNG BẰNG min lexicographic của pool.
G4.3 ứng viên có chạm băng sau va chạm: roll_len > Euclid(điểm chạm →
điểm đáp) — bắt lỗi cài nhầm đường thẳng. Điểm chạm đọc lại từ
history của chính sim đó.
Thoát code 0 = mọi gate xanh; khác 0 = có gate đỏ (in rõ gate nào).
"""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
import numpy as np # noqa: E402
FAIL = []
def check(name, cond, detail=""):
tag = "XANH" if cond else "DO"
print(f" [{tag}] {name}" + (f" — {detail}" if detail else ""))
if not cond:
FAIL.append(name)
# Bốn thế dựng tay. Toạ độ mét, bàn 0.9906 × 1.9812.
BOARDS = {
# Bi kế "2" chắn ngay đường lăn của cú follow thẳng: các ứng viên follow
# đụng nó sau va chạm → (iii); các ứng viên khác thoát → còn cú để chọn.
"blocker_tren_duong_lan": {
"cue": (0.45, 0.30), "1": (0.42, 0.95), "2": (0.38, 1.45),
"9": (0.80, 0.40)},
# Bi mục tiêu sát băng trên, đánh thẳng lên: bi 1 dội băng quay lại gặp
# bi cái đang lăn tới — double-kiss cue↔1 thật (không pot được, nhưng
# (iii) phải GHI NHẬN được cú chạm thứ hai).
"doi_bang_double_kiss": {
"cue": (0.45, 1.20), "1": (0.45, 1.80), "2": (0.15, 0.40),
"9": (0.80, 0.40)},
# Bi kế "2" đặt SÁT một điểm đáp đã đo (cú kéo vert=-0.2 v0=3.222 đáp
# (0.326, 1.259) trên thế nền — dò bằng probe 31/07), lệch hẳn đường pot:
# cú đó phải chết (iv) phía DƯỚI (d ≈ 0.09 < 0.15).
"bi_ke_sat_diem_dap": {
"cue": (0.45, 0.30), "1": (0.42, 0.95), "2": (0.26, 1.20),
"9": (0.80, 0.40)},
# Bi kế tít góc trên trong khi bi cái đánh nhẹ từ dưới: cú lực yếu đáp xa
# bi kế hơn 1.5 m → (iv) phía trên phải xuất hiện.
"bi_ke_rat_xa": {
"cue": (0.45, 0.25), "1": (0.42, 0.60), "2": (0.85, 1.92),
"9": (0.15, 1.60)},
}
def main():
import pooltool as pt
from poolcoach_rl.envs import PositionPlayEnv
from poolcoach_rl.recommend import warmup
from poolcoach_rl.recommend.zone_v2 import (D_MAX, D_MIN, force_ok,
plan_shot_v2, rank_key_v2,
v2_specs)
from poolcoach_rl.recommend.zone_v2 import default_v0_grid
env_h = PositionPlayEnv()
print("[gates] warmup JIT (~40s lan dau)...", flush=True)
warmup(env_h)
# ---- G3.3: (v) so bằng giá trị grid — thuần, kiểm luôn cho đủ hồ sơ
v0s = default_v0_grid()
print("\n[G3.3] nấc lực so bằng giá trị grid")
check("v0_grid[0]=0.5 bị loại", not force_ok(float(v0s[0])))
check(f"v0_grid[1]={float(v0s[1]):.10f} qua", force_ok(float(v0s[1])))
check("literal 0.889 KHÔNG qua", not force_ok(0.889))
check("specs V2 = 99 ô, không nấc 1",
len(v2_specs()) == 99
and all(v0 > 0.5 for _a, _b, v0 in v2_specs()))
ball_r = 0.028575
seen = {"iii": 0, "iii_double_kiss": 0, "iv_gan": 0, "iv_xa": 0,
"pass": 0, "cushion_ok": 0}
for name, raw in BOARDS.items():
balls = {bid: np.array(xy, dtype=np.float64)
for bid, xy in raw.items()}
target = "1"
plan = plan_shot_v2(env_h, balls, target)
key = rank_key_v2()
print(f"\n[{name}] n_cand={plan.n_cand} "
f"pass={len(plan.pool)} "
f"fails={[ (p.rank, p.fail_counts) for p in plan.pockets ]}")
# --- thu thập bằng chứng qua các ứng viên của mọi lỗ đã thử
# (plan chỉ giữ pool của lỗ chọn; muốn soi từng ứng viên thì đi lại
# từng lỗ — dùng chính specs + sim để không chép lại máy lọc)
from poolcoach_rl.recommend.rules import judge_shot
from poolcoach_rl.recommend.simulate import simulate_shot_multi
from poolcoach_rl.recommend.zone_v2 import (cue_hits_after_contact,
target_path_dirty)
for pk in plan.picks[:1 if plan.pocket_rank in (None, 1) else 2]:
for side, vert, v0 in v2_specs():
m = simulate_shot_multi(env_h, balls, pk.phi, v0, side, vert)
if m is None:
continue
j = judge_shot(env_h, m, balls, target)
bb = m["bb_events"]
if cue_hits_after_contact(bb):
seen["iii"] += 1
cue_evs = [ids for _t, ids in bb if "cue" in ids]
if len(cue_evs) > 1 and target in cue_evs[1]:
seen["iii_double_kiss"] += 1
clean = (not j["foul"] and list(m["potted"]) == [target]
and not target_path_dirty(bb, target)
and not cue_hits_after_contact(bb))
if clean and j["next"] is not None:
cf = m["balls_final"]["cue"]
d = float(np.linalg.norm(cf - balls[j["next"]]))
if d < D_MIN:
seen["iv_gan"] += 1
elif d > D_MAX:
seen["iv_xa"] += 1
# --- bất biến của kết quả
if plan.pool:
seen["pass"] += len(plan.pool)
check(f"{name}: mọi cú qua lọc có d trong [{D_MIN}, {D_MAX}]",
all(c.d_land is None or D_MIN <= c.d_land <= D_MAX
for c in plan.pool))
check(f"{name}: shot = min lexicographic của pool (G4.1-2)",
plan.shot is min(plan.pool, key=key)
and sorted(plan.pool, key=key)[0] is plan.shot)
check(f"{name}: mọi cú qua lọc KHÔNG có double-kiss/đụng bi",
all(not cue_hits_after_contact(c.facts["bb_events"])
and not target_path_dirty(c.facts["bb_events"], target)
for c in plan.pool))
# --- G4.3: cú có chạm băng sau va chạm → roll > Euclid chạm→đáp
for c in plan.pool:
sysm = simulate_shot_multi(env_h, balls, c.phi, c.v0, c.side,
c.vert, render=True)
if sysm is None:
continue
system = sysm["system"]
t1 = next((ev.time for ev in system.events
if ev.event_type == pt.EventType.BALL_BALL
and "cue" in ev.ids), None)
has_cush = any(
ev.event_type in (pt.EventType.BALL_LINEAR_CUSHION,
pt.EventType.BALL_CIRCULAR_CUSHION)
and "cue" in ev.ids and ev.time > t1
for ev in system.events)
if not has_cush:
continue
rvw, _ss, ts = system.balls["cue"].history.vectorize()
i1 = int(np.argmax(np.asarray(ts) >= t1 - 1e-12))
contact = rvw[i1, 0, :2]
final = sysm["balls_final"]["cue"]
if final is None:
continue
euclid = float(np.linalg.norm(final - contact))
check(f"{name}: G4.3 roll {c.roll_len:.3f} > euclid "
f"{euclid:.3f} (cú dội băng v0={c.v0:.2f})",
c.roll_len > euclid + 1e-4)
seen["cushion_ok"] += 1
break
print("\n[tổng hợp bằng chứng trên cả bộ thế]")
check("G3.1: có ứng viên chết (iii)", seen["iii"] > 0,
f"{seen['iii']} ứng viên")
check("G3.1: trong đó có double-kiss cue↔target thật",
seen["iii_double_kiss"] > 0, f"{seen['iii_double_kiss']} ứng viên")
check("G3.2: thấy cú pot sạch đáp GẦN hơn 0.15 m (bị iv loại)",
seen["iv_gan"] > 0, f"{seen['iv_gan']} ứng viên")
check("G3.2: thấy cú pot sạch đáp XA hơn 1.50 m (bị iv loại)",
seen["iv_xa"] > 0, f"{seen['iv_xa']} ứng viên")
check("có cú QUA đủ lọc để app còn thứ mà gợi ý", seen["pass"] > 0,
f"{seen['pass']} cú")
check("G4.3 đã kiểm được trên ≥1 cú dội băng", seen["cushion_ok"] > 0,
f"{seen['cushion_ok']} cú")
print(f"\n[gates] {'XANH — tất cả' if not FAIL else 'ĐỎ: ' + str(FAIL)}")
return 1 if FAIL else 0
if __name__ == "__main__":
sys.exit(main())