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}