File size: 3,671 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
"""Hình học chọn lỗ — tách nguyên từ scripts/recommend_shot.py (23/07/2026)."""

from __future__ import annotations


def feasible_pockets(env_h, cue_xy, b1_xy):
    """[(pocket_idx, phi_deg, cos_cut)] — cùng logic _ghost_dirs_any, giữ
    kèm pocket_idx để gọi tên lỗ. Fallback best_cut nếu không lỗ nào dương."""
    import numpy as np

    from poolcoach_rl.envs.position_env import BALL_R

    to_tgt = b1_xy - cue_xy
    to_tgt = to_tgt / np.linalg.norm(to_tgt)
    out = []
    for i, p in enumerate(env_h._pockets):
        v = p - b1_xy
        n = float(np.linalg.norm(v))
        if n < 1e-9:
            continue
        v = v / n
        cut = float(np.dot(to_tgt, v))
        if cut <= 0.0:
            continue
        g = (b1_xy - 2.0 * BALL_R * v) - cue_xy
        gn = float(np.linalg.norm(g))
        if gn < 1e-9:
            continue
        g = g / gn
        out.append((i, float(np.degrees(np.arctan2(g[1], g[0])) % 360.0), cut))
    if not out:  # hiếm — mọi lỗ ngược hướng: dùng lỗ cut lớn nhất
        best_i, best_cut = 0, -np.inf
        for i, p in enumerate(env_h._pockets):
            v = p - b1_xy
            n = float(np.linalg.norm(v))
            if n < 1e-9:
                continue
            cut = float(np.dot(to_tgt, v / n))
            if cut > best_cut:
                best_i, best_cut = i, cut
        g = env_h._ghost_aim_point(cue_xy, b1_xy) - cue_xy
        g = g / np.linalg.norm(g)
        out.append((best_i,
                    float(np.degrees(np.arctan2(g[1], g[0])) % 360.0),
                    best_cut))
    return out


def _seg_point_dist(p, a, b):
    """Khoảng cách từ điểm p đến ĐOẠN thẳng ab (không phải đường thẳng)."""
    import numpy as np

    ab = b - a
    t = float(np.dot(p - a, ab)) / max(float(np.dot(ab, ab)), 1e-12)
    t = min(max(t, 0.0), 1.0)
    return float(np.linalg.norm(p - (a + t * ab)))


def feasible_pockets_full(env_h, balls, target):
    """Full-rack (phase B, §4.3): như feasible_pockets nhưng biết bi chắn.

    Bi khác (không cue, không target) nằm cách đường ``cue→ghost`` hoặc
    ``target→pocket`` < 2R → lỗ bị đánh dấu ``blocked`` và hạ xuống CUỐI
    danh sách — KHÔNG loại hẳn (đôi khi sim vẫn thành công nhờ sượt qua).
    Mục đích duy nhất: thứ tự ưu tiên grid; sự thật vẫn do sim quyết.

    Trả [(pocket_idx, phi_deg, cos_cut, blocked)] — nhóm thoáng trước,
    trong nhóm giữ nguyên thứ tự của feasible_pockets.
    """
    import numpy as np

    from poolcoach_rl.envs.position_env import BALL_R

    cue_xy, tgt_xy = balls["cue"], balls[target]
    others = [xy for bid, xy in balls.items()
              if bid not in ("cue", target)]
    out = []
    for i, phi, cut in feasible_pockets(env_h, cue_xy, tgt_xy):
        p = env_h._pockets[i]
        v = p - tgt_xy
        n = float(np.linalg.norm(v))
        ghost = tgt_xy - 2.0 * BALL_R * (v / n) if n > 1e-9 else tgt_xy
        blocked = any(
            _seg_point_dist(o, cue_xy, ghost) < 2 * BALL_R
            or _seg_point_dist(o, tgt_xy, p) < 2 * BALL_R
            for o in others)
        out.append((i, phi, cut, blocked))
    out.sort(key=lambda t: t[3])  # stable: thoáng trước, thứ tự cũ giữ nguyên
    return out


def pocket_name(env_h, idx: int) -> str:
    p = env_h._pockets[idx]
    w, l = env_h.w, env_h.l
    lr = "trái" if p[0] < w / 2 else "phải"
    if p[1] < l / 3:
        return f"góc dưới-{lr}"
    if p[1] > 2 * l / 3:
        return f"góc trên-{lr}"
    return f"giữa-{lr}"