Spaces:
Sleeping
Sleeping
| """Luật 9-ball MVP + EV full-rack — phase B (FullRack design §3 + §4.2). | |
| Tầng LUẬT tách khỏi tầng vật lý: `simulate_shot_multi` chỉ trả facts | |
| (potted/scratch/first_contact/balls_final), module này chấm điểm theo luật. | |
| EV v2 — 25/07/2026 (Production design §2.1, "anti lucky-9"): | |
| foul (scratch HOẶC sai first contact) → ev = None (LOẠI khỏi rank — §3.2, | |
| không phải EV thấp: cấm gợi ý cú | |
| phạm luật) | |
| target = "9" vào lỗ sạch → ev = WIN_EV (ăn bi 9 trực tiếp, | |
| đáng tin như pot thường) | |
| target < 9 vào lỗ sạch → ev = 1 + POS_COEF × Q(next); | |
| bi 9 rơi kèm (combo) KHÔNG cộng | |
| gì — bonus tự nhiên, không thưởng | |
| target KHÔNG vào (kể cả bi 9 rơi) → ev = 0.0 — combo thuần may mắn bị | |
| tính như miss, KHÔNG gợi ý | |
| Vì sao đổi (data 24/07): bảng EV cũ cho WIN_EV cho MỌI cú có bi 9 rơi hợp lệ | |
| → top-1 là WIN ở 13/15 bàn, tức oracle khai thác combo bi 9 "may mắn" trong | |
| grid. Sim deterministic thì chắc chắn, bàn thật có noise — đúng bài học | |
| b2-lucky 44% ngày 20/07. App coach không khuyên cú ăn may. | |
| `win` VẪN là fact từ sim (bi 9 vào lỗ trong cú hợp lệ, kể cả combo) — giữ | |
| nguyên để FE hiện badge "⚑ bi 9 vào lỗ" và dừng ván khi xảy ra thật; nó chỉ | |
| không còn ảnh hưởng EV nữa. | |
| Bi khác (không target, không 9) vào lỗ trong cú hợp lệ: biến mất, không | |
| bonus/phạt (§3.4). Chưa xử lý MVP (§3.6): luật chạm băng sau va chạm, | |
| ball-in-hand sau foul. | |
| """ | |
| from __future__ import annotations | |
| from .simulate import POS_COEF | |
| WIN_EV = 2.0 # §3.3 — đủ lớn hơn max EV pot thường (1 + 0.5×1 = 1.5) | |
| def target_ball(balls) -> str: | |
| """Bi mục tiêu = bi số NHỎ NHẤT còn trên bàn (§3.1 — BE tự suy, | |
| không tin FE). balls = {"cue": xy, "3": xy, ...}.""" | |
| nums = [int(bid) for bid in balls if bid != "cue"] | |
| if not nums: | |
| raise ValueError("Bàn không có bi mục tiêu (cần ≥1 bi 1..9)") | |
| return str(min(nums)) | |
| def judge_shot(env_h, m, balls, target): | |
| """Chấm outcome ``m`` của simulate_shot_multi theo luật 9-ball MVP. | |
| Trả dict: | |
| foul : bool — scratch hoặc first_contact != target (kể cả không chạm gì) | |
| win : bool — bi 9 vào lỗ trong cú hợp lệ (FACT, kể cả combo — KHÔNG | |
| còn ảnh hưởng ev từ 25/07, xem docstring module) | |
| next : id bi nhỏ nhất CÒN LẠI sau cú | None nếu hết bàn | |
| q : Q(cue_final, next sau cú) — chỉ khi pot target sạch và còn next | |
| ev : None nếu foul (loại); ngược lại như bảng EV v2 ở docstring module | |
| """ | |
| potted = m["potted"] | |
| foul = m["scratch"] or (m["first_contact"] != target) | |
| win = (not foul) and ("9" in potted) | |
| remaining = [bid for bid in balls | |
| if bid != "cue" and bid not in potted] | |
| nxt = str(min(int(bid) for bid in remaining)) if remaining else None | |
| q, ev = 0.0, None | |
| if foul: | |
| pass # loại — caller không đưa vào rank | |
| elif target not in potted: | |
| # miss hợp lệ — KỂ CẢ khi win=True (bi 9 rơi mà target không vào là | |
| # combo thuần may mắn: chấm như miss để không bao giờ gợi ý — §2.1) | |
| ev = 0.0 | |
| elif target == "9": | |
| ev = WIN_EV # ăn bi 9 trực tiếp = thắng đáng tin | |
| elif nxt is None: | |
| # hết bàn mà không có bi 9 (bàn tuỳ đặt, bi 9 bị tắt): | |
| # pot sạch bi cuối là trọn vẹn — không cần position | |
| ev = 1.0 | |
| else: | |
| q = float(env_h._position_q(m["balls_final"]["cue"], | |
| m["balls_final"][nxt])) | |
| ev = 1.0 + POS_COEF * q # combo 9 rơi kèm: KHÔNG cộng thêm | |
| return {"foul": foul, "win": win, "next": nxt, "q": q, "ev": ev} | |