Spaces:
Sleeping
Sleeping
File size: 9,542 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 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | """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())
|