masterdanh's picture
deploy: snapshot for HF Space
78738de
Raw
History Blame Contribute Delete
7.29 kB
"""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,
}