Spaces:
Sleeping
Sleeping
File size: 5,253 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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | """Gate 1 (ImplPlan Bước 1): bảng EV v2 của `judge_shot` — anti lucky-9.
Gọi thẳng `judge_shot` với env stub: tầng luật thuần logic, không đụng vật lý.
Điểm mấu chốt phải khoá lại: cú làm rơi bi 9 nhưng KHÔNG ăn được target là
combo may mắn → ev = 0.0 (case 4), và combo ăn kèm cũng không được thưởng
(case 3) — chỉ bi 9 đúng là target mới đáng WIN_EV (case 1).
"""
from __future__ import annotations
import numpy as np
import pytest
from poolcoach_rl.recommend.rules import WIN_EV, judge_shot
from poolcoach_rl.recommend.simulate import POS_COEF
Q_STUB = 0.6 # EnvStub._position_q trả cố định
EV_POT = 1.0 + POS_COEF * Q_STUB # 1.3
def make_m(potted, first_contact, balls, scratch=False):
"""Dựng facts y như `simulate_shot_multi` trả (chỉ phần luật cần đọc)."""
return {
"potted": list(potted),
"scratch": scratch,
"first_contact": first_contact,
# vị trí thật không quan trọng — _position_q của stub trả hằng số;
# chỉ cần đủ key để judge_shot index không KeyError
"balls_final": {bid: (None if bid in potted else np.array([0.5, 1.0]))
for bid in balls},
}
# ---------------------------------------------------------------- case 1-8
def test_case1_target_9_truc_tiep(env_stub):
"""target = "9" vào lỗ sạch → WIN_EV (cú duy nhất còn được thưởng)."""
balls = {"cue": None, "9": None}
r = judge_shot(env_stub, make_m(["9"], "9", balls), balls, "9")
assert r["foul"] is False
assert r["win"] is True
assert r["ev"] == pytest.approx(WIN_EV)
assert r["next"] is None
def test_case2_pot_thuong(env_stub):
"""pot target sạch, còn bi kế → 1 + POS_COEF × Q."""
balls = {"cue": None, "1": None, "2": None}
r = judge_shot(env_stub, make_m(["1"], "1", balls), balls, "1")
assert r["foul"] is False
assert r["win"] is False
assert r["next"] == "2"
assert r["q"] == pytest.approx(Q_STUB)
assert r["ev"] == pytest.approx(EV_POT)
def test_case3_combo_an_kem_khong_duoc_thuong(env_stub):
"""pot target VÀ bi 9 rơi kèm → vẫn chỉ 1 + POS_COEF × Q, KHÔNG phải WIN_EV.
win=True được giữ (FE hiện badge + dừng ván), nhưng EV không đổi.
"""
balls = {"cue": None, "1": None, "2": None, "9": None}
r = judge_shot(env_stub, make_m(["1", "9"], "1", balls), balls, "1")
assert r["win"] is True
assert r["ev"] == pytest.approx(EV_POT)
assert r["ev"] != pytest.approx(WIN_EV)
assert r["next"] == "2"
def test_case4_lucky_9_bi_cham_nhu_miss(env_stub):
"""BI 9 RƠI mà target KHÔNG vào → ev = 0.0 (đây là lỗi 13/15 bàn 24/07)."""
balls = {"cue": None, "1": None, "9": None}
r = judge_shot(env_stub, make_m(["9"], "1", balls), balls, "1")
assert r["foul"] is False # chạm đúng bi 1 trước, không scratch
assert r["win"] is True # fact: bi 9 đã vào lỗ
assert r["ev"] == pytest.approx(0.0)
def test_case5_foul_sai_first_contact(env_stub):
balls = {"cue": None, "1": None, "2": None}
r = judge_shot(env_stub, make_m(["1"], "2", balls), balls, "1")
assert r["foul"] is True
assert r["ev"] is None
def test_case6_foul_khong_cham_gi(env_stub):
balls = {"cue": None, "1": None}
r = judge_shot(env_stub, make_m([], None, balls), balls, "1")
assert r["foul"] is True
assert r["ev"] is None
def test_case7_foul_scratch_du_pot(env_stub):
balls = {"cue": None, "1": None, "2": None}
m = make_m(["1"], "1", balls, scratch=True)
r = judge_shot(env_stub, m, balls, "1")
assert r["foul"] is True
assert r["win"] is False # scratch → không tính thắng
assert r["ev"] is None
def test_case8_pot_bi_cuoi_khong_co_next(env_stub):
"""Bàn chỉ còn bi 1 (bi 9 bị tắt) → pot sạch = 1.0, không cần position."""
balls = {"cue": None, "1": None}
r = judge_shot(env_stub, make_m(["1"], "1", balls), balls, "1")
assert r["next"] is None
assert r["ev"] == pytest.approx(1.0)
# ------------------------------------------------------------------ case 9
def test_case9_filter_legal_loai_lucky_9(env_stub):
"""Biểu thức filter mới của `recommend_full` loại case 4 khỏi `legal`.
Kiểm thẳng biểu thức (core.py) chứ KHÔNG gọi `recommend_full` thật — hàm
đó đi xuyên pooltool (`pt.System` nằm NGOÀI try/except của
simulate_shot_multi), ngoài phạm vi test contract.
"""
target = "1"
balls = {"cue": None, "1": None, "2": None, "9": None}
lucky = make_m(["9"], "1", balls) # case 4: chỉ bi 9 rơi
lucky.update(judge_shot(env_stub, lucky, balls, target))
good = make_m(["1"], "1", balls) # case 2: pot target
good.update(judge_shot(env_stub, good, balls, target))
results = [lucky, good]
legal = [r for r in results if not r["foul"] and target in r["potted"]]
assert lucky not in legal # điều cần chứng minh
assert legal == [good] # và filter không "rỗng hoá" mọi thứ
|