Spaces:
Sleeping
Sleeping
File size: 4,283 Bytes
78738de | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | """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}
|