Spaces:
Sleeping
Sleeping
| """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ứ | |