File size: 3,735 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
"""Fixtures dùng chung cho test app (ImplPlan Bước T) — 25/07/2026.

Nguyên tắc: test kiểm CONTRACT (luật 9-ball, shape response, message), KHÔNG
kiểm vật lý. pooltool thật nặng (~40s JIT Numba lần đầu) và không cần cho các
gate này → nhét stub vào sys.modules trước khi import bất cứ thứ gì.
"""

from __future__ import annotations

import sys
import types
from pathlib import Path

# Repo KHÔNG cài package: phải tự đẩy src/ vào sys.path TRƯỚC mọi import
# poolcoach_rl. Đừng dựa vào app/main.py (nó cũng chèn src, nhưng chỉ khi
# được import → pass/fail sẽ phụ thuộc thứ tự collect của pytest).
ROOT = Path(__file__).resolve().parents[1]
for _p in (ROOT / "src", ROOT):
    if str(_p) not in sys.path:
        sys.path.insert(0, str(_p))

import numpy as np  # noqa: E402
import pytest  # noqa: E402


def _install_pooltool_stub() -> None:
    """Stub `pooltool` + `pooltool.constants` (module rỗng).

    `envs/position_env.py` import pooltool ở top-level nhưng mọi chỗ dùng
    `pt.` đều nằm trong thân hàm (annotation lazy nhờ `from __future__ import
    annotations`) → stub rỗng vẫn import lọt, đủ để `validate_full` lấy
    BALL_R. Cần gymnasium THẬT (venv có sẵn).

    Cố ý để stub thiếu `System`/`simulate`/`EventType`: nếu test nào lỡ chạm
    vào tầng vật lý, nó sẽ ném AttributeError ngay thay vì âm thầm chạy sim.
    """
    if "pooltool" in sys.modules:
        return
    pt = types.ModuleType("pooltool")
    constants = types.ModuleType("pooltool.constants")
    constants.pocketed = "pocketed"
    pt.constants = constants
    sys.modules["pooltool"] = pt
    sys.modules["pooltool.constants"] = constants


_install_pooltool_stub()


class EnvStub:
    """Env tối thiểu cho tầng luật + describe_full: hằng số bàn thật + Q cố định.

    Chỉ cần `w/l/_pockets/_bounds/_position_q` — `judge_shot` gọi
    `_position_q`, `pocket_name` đọc `_pockets/w/l`, `validate_full` đọc
    `_bounds`.
    """

    w = 0.9906
    l = 1.9812

    # Envelope vị trí nghỉ hợp lệ — SỐ DẪN RA TỪ BÀN THẬT, không phải số chọn
    # tay: `table_bounds(w, l, pt.Table.default().pockets.values())` =
    # hộp bao của (mặt bàn ∪ 6 đường tròn lỗ). Chép cứng vào đây vì stub không
    # có `table`, và conftest cố ý không có pooltool thật để dựng bàn.
    #
    # ĐỔI SPECS BÀN THÌ PHẢI SINH LẠI 4 SỐ NÀY. `test_validate_full.py` khoá cả
    # hai vế: một test kiểm `table_bounds` thật sự DẪN RA hộp từ hình học lỗ
    # (nạp bàn giả vào, kiểm mọi đường tròn lỗ nằm trọn trong hộp), một test
    # kiểm bộ số dưới đây đúng bằng cái `table_bounds` trả về trên specs bàn
    # mặc định của pooltool.
    _bounds = (-0.133, 1.1236, -0.09148635277547904, 2.072686352775479)

    def __init__(self, q: float = 0.6):
        self.q = q
        # 6 lỗ đúng bố cục bàn thật (pocket_name suy tên từ toạ độ)
        self._pockets = [
            np.array([0.0, 0.0]),           # 0 góc dưới-trái
            np.array([0.0, self.l / 2]),    # 1 giữa-trái
            np.array([0.0, self.l]),        # 2 góc trên-trái
            np.array([self.w, 0.0]),        # 3 góc dưới-phải
            np.array([self.w, self.l / 2]), # 4 giữa-phải
            np.array([self.w, self.l]),     # 5 góc trên-phải
        ]

    def _position_q(self, cue_xy, nxt_xy) -> float:
        return self.q


@pytest.fixture
def env_stub() -> EnvStub:
    return EnvStub()