Spaces:
Sleeping
Sleeping
| """Tầng SIM — chỉ nói sự thật vật lý, không luật, không xếp hạng. | |
| 24/07/2026 (FullRack phase A): `simulate_shot_multi` nhận dict {id: xy} bất | |
| kỳ, trả FACTS (potted list / scratch / first_contact / balls_final). | |
| 31/07/2026 thêm hai facts cho V2 (`bb_events`, `cue_roll_after` — xem | |
| docstring hàm). `simulate_shot` 3-bi ở lại làm wrapper cho `warmup`. | |
| 31/07/2026 (việc D): các khoá xếp hạng EV (`rank_key`, `rank_key_factory`, | |
| `rank2_key`, `EV2_MAX`) xoá theo engine oracle/hybrid — chúng sống ở nhánh | |
| `v1-full`. POS_COEF/SCRATCH_EV Ở LẠI: `rules.judge_shot` và reward env vẫn | |
| định nghĩa Q/EV bằng chúng (hồ sơ nghiên cứu, xem BRIEF bối cảnh 5). | |
| """ | |
| from __future__ import annotations | |
| POS_COEF = 0.5 # EV = pot·(1 + POS_COEF·Q) — khớp reward env | |
| SCRATCH_EV = -0.3 # khớp scratch_penalty env | |
| def path_len_after(xy, ts, t1, *, eps=1e-12): | |
| """Độ dài polyline (mét) của ``xy`` từ thời điểm ``t1`` trở đi — THUẦN. | |
| ``xy``: mảng ``(n, 2)`` vị trí theo thời gian; ``ts``: ``(n,)`` mốc thời | |
| gian tương ứng. Nguồn dự kiến là ``ball.history`` event-based của pooltool | |
| (KHÔNG cần continuize): snapshot tại MỌI event nên mọi cú dội băng và mọi | |
| chuyển pha sliding→rolling đều là đỉnh polyline. Nghiệm thu 31/07/2026 so | |
| với continuize ``dt=0.01`` trên 6 cú trộn xoáy/lực: lệch 0.2–8.9 mm — | |
| nhỏ hơn nhiều bước lưới ứng viên, đủ cho xếp hạng. | |
| ``eps`` nới mốc so sánh: ``t1`` chép từ ``event.time`` và ``ts`` chép từ | |
| cùng nguồn, nhưng qua hai đường float khác nhau — so ``>=`` trần trụi có | |
| thể rớt đúng snapshot va chạm. | |
| """ | |
| import numpy as np | |
| xy = np.asarray(xy, dtype=np.float64).reshape(-1, 2) | |
| m = np.asarray(ts, dtype=np.float64).ravel() >= float(t1) - eps | |
| if int(m.sum()) < 2: | |
| return 0.0 | |
| return float(np.linalg.norm(np.diff(xy[m], axis=0), axis=1).sum()) | |
| def simulate_shot_multi(env_h, balls, phi, v0, a, b, render=False): | |
| """1 cú với N bi bất kỳ. ``balls = {"cue": xy, "1": xy, "5": xy, ...}`` | |
| (bắt buộc có "cue"; id bi = chuỗi số như FE/API gửi). | |
| Chỉ trả FACTS vật lý — không luật, không EV (tầng đó ở phase B): | |
| system : pooltool System nếu render=True (vẽ quỹ đạo) | None | |
| potted : list id bi VÀO LỖ trong cú này (không tính cue) | |
| scratch : bool — bi cái vào lỗ | |
| first_contact : id bi đầu tiên cue chạm | None nếu không chạm gì | |
| (event BALL_BALL đầu tiên có "cue" — luật 9-ball | |
| phase B sẽ so với target để bắt foul) | |
| balls_final : {id: np.ndarray (2,) | None nếu vào lỗ} — mọi bi, | |
| kể cả cue (nút "Đánh cú này" của FE dùng dict này) | |
| HAI KEY THÊM 31/07/2026 (ZonePlanner V2, BRIEF bối cảnh 7 — MỞ RỘNG | |
| THUẦN: hai phép gán, không nhánh cũ nào đọc chúng): | |
| bb_events : tuple[(t, (id, id))] — MỌI event BALL_BALL theo thứ tự | |
| thời gian. Lọc (i)/(iii) của V2 đọc từ đây ("đường bi | |
| mục tiêu không chạm bi khác", "bi cái sau va chạm không | |
| đụng bi nào — kể cả double-kiss"). | |
| cue_roll_after: float | None — quãng đường bi cái LĂN SAU cú chạm bi | |
| đầu tiên (mét, arc length event-polyline, xem | |
| ``path_len_after``) | None nếu cue không chạm bi nào. | |
| Khoá xếp hạng số 1 của V2 (§8.2 design). | |
| Trả None nếu pooltool ném exception (giữ hợp đồng cũ). | |
| """ | |
| import numpy as np | |
| import pooltool as pt | |
| import pooltool.constants as ptc | |
| system = pt.System( | |
| table=env_h.table, | |
| balls={bid: pt.Ball.create(bid, xy=tuple(xy)) | |
| for bid, xy in balls.items()}, | |
| cue=pt.Cue(cue_ball_id="cue"), | |
| ) | |
| system.cue.set_state(V0=v0, phi=phi, a=a, b=b) | |
| try: | |
| pt.simulate(system, inplace=True) | |
| except Exception: | |
| return None | |
| if render: | |
| try: | |
| pt.continuize(system, dt=0.01, inplace=True) | |
| except Exception: | |
| pass # fallback: vẽ theo history event-based | |
| def _pk(bid): | |
| return system.balls[bid].state.s == ptc.pocketed | |
| def _xy(bid): | |
| return np.asarray(system.balls[bid].state.rvw[0][:2], dtype=np.float64) | |
| first_contact = None | |
| for ev in system.events: | |
| if ev.event_type == pt.EventType.BALL_BALL and "cue" in ev.ids: | |
| first_contact = next(i for i in ev.ids if i != "cue") | |
| break | |
| bb_events = tuple((float(ev.time), tuple(ev.ids)) | |
| for ev in system.events | |
| if ev.event_type == pt.EventType.BALL_BALL) | |
| # quãng đường bi cái SAU cú chạm đầu — từ history event-based (có sẵn | |
| # không cần continuize; extract_trajectories vẫn fallback vào đúng nguồn | |
| # này). vectorize hỏng thì trả None thay vì làm chết cả sim: hai key mới | |
| # là mở rộng, không được phép kéo sập hợp đồng cũ. | |
| cue_roll_after = None | |
| t1 = next((t for t, ids in bb_events if "cue" in ids), None) | |
| if t1 is not None: | |
| try: | |
| rvw, _ss, ts = system.balls["cue"].history.vectorize() | |
| except Exception: | |
| rvw = None | |
| if rvw is not None and len(rvw) > 1: | |
| cue_roll_after = path_len_after(rvw[:, 0, :2], ts, t1) | |
| return { | |
| "system": system if render else None, | |
| "potted": [bid for bid in balls if bid != "cue" and _pk(bid)], | |
| "scratch": _pk("cue"), | |
| "first_contact": first_contact, | |
| "balls_final": {bid: (None if _pk(bid) else _xy(bid)) | |
| for bid in balls}, | |
| "bb_events": bb_events, | |
| "cue_roll_after": cue_roll_after, | |
| } | |
| def simulate_shot(env_h, cue_xy, b1_xy, b2_xy, phi, v0, a, b, render=False): | |
| """1 cú 3 bi (wrapper tương thích v1) — output y hệt bản cũ 23/07, | |
| thêm 2 key mới vô hại: ``first_contact``, ``balls_final``.""" | |
| balls = {"cue": cue_xy, "1": b1_xy} | |
| if b2_xy is not None: | |
| balls["2"] = b2_xy | |
| m = simulate_shot_multi(env_h, balls, phi, v0, a, b, render=render) | |
| if m is None: | |
| return None | |
| potted = "1" in m["potted"] | |
| scratch = m["scratch"] | |
| b2p = "2" in m["potted"] | |
| bf = m["balls_final"] | |
| q = 0.0 | |
| if b2_xy is not None and potted and not scratch: | |
| q = 1.0 if b2p else env_h._position_q(bf["cue"], bf["2"]) | |
| if scratch: | |
| ev = SCRATCH_EV | |
| elif potted: | |
| ev = 1.0 + POS_COEF * q | |
| else: | |
| ev = 0.0 | |
| return { | |
| "system": m["system"], | |
| "potted": potted, "scratch": scratch, "b2_potted": b2p, | |
| "q": q, "ev": ev, | |
| "cue_final": bf["cue"], # None khi scratch (bi cái pocketed) — như cũ | |
| "b2_final": bf["2"] if b2_xy is not None else None, # None khi b2 vào lỗ | |
| "first_contact": m["first_contact"], | |
| "balls_final": bf, | |
| } | |