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