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())