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