poolcoach / tests /test_rules_v2.py
masterdanh's picture
deploy: snapshot for HF Space
78738de
Raw
History Blame Contribute Delete
5.25 kB
"""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ứ