"""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}"