Spaces:
Sleeping
Sleeping
| """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 | |
| def env_stub() -> EnvStub: | |
| return EnvStub() | |