diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 711302a..674130a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -42,7 +42,7 @@ jobs:
run: python scripts/site_facts.py --check
browser-physics:
- # The browser runs a hand port of src/cueai/physics/. This regenerates the
+ # The browser runs a hand port of src/pocket/physics/. This regenerates the
# reference outcomes from the Python simulator and replays them in Node, so
# the two implementations cannot drift apart without the build going red.
runs-on: ubuntu-latest
@@ -104,7 +104,7 @@ jobs:
pip install -e ".[dev]"
- name: Train on a small dataset
- run: python -m cueai.ml.train --n-samples 400 --epochs 10
+ run: python -m pocket.ml.train --n-samples 400 --epochs 10
- name: Benchmark
run: python scripts/benchmark.py --repeats 50
@@ -114,7 +114,7 @@ jobs:
- uses: actions/upload-artifact@v4
with:
- name: cueai-pipeline-output
+ name: pocket-pipeline-output
path: |
models/metrics.json
models/latency.json
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index fdc57d8..605e976 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -56,7 +56,11 @@ jobs:
steps:
- uses: actions/checkout@v4
+ # enablement turns Pages on for the repository if it is off, so the
+ # published link works without anyone visiting the settings page first.
- uses: actions/configure-pages@v5
+ with:
+ enablement: true
- uses: actions/upload-pages-artifact@v3
with:
diff --git a/LICENSE b/LICENSE
index 8bbc8b5..d1e0faa 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2026 CueAI contributors
+Copyright (c) 2026 Pocket Physics contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Makefile b/Makefile
index d4e8240..36e0c54 100644
--- a/Makefile
+++ b/Makefile
@@ -49,11 +49,11 @@ capture: ## Re-record the screenshots and the clip in the README
web: parity selfplay ## Every check that does not need a browser
play: ## Serve the game at http://localhost:$(PORT)
- @echo "CueAI is at http://localhost:$(PORT)"
+ @echo "Pocket Physics is at http://localhost:$(PORT)"
@cd web && $(PY) -m http.server $(PORT)
train: ## Generate data and train the residual model
- $(PY) -m cueai.ml.train --n-samples $(SAMPLES) --epochs $(EPOCHS)
+ $(PY) -m pocket.ml.train --n-samples $(SAMPLES) --epochs $(EPOCHS)
bench: ## Measure latency and rewrite docs/BENCHMARKS.md
$(PY) scripts/benchmark.py
@@ -65,10 +65,10 @@ facts: ## Rewrite the numbers the playable page quotes
$(PY) scripts/site_facts.py
api: ## Serve the prediction API on :8000
- $(PY) -m uvicorn cueai.api.main:app --reload --port 8000
+ $(PY) -m uvicorn pocket.api.main:app --reload --port 8000
ui: ## Launch the desktop table (needs the ui extra)
- $(PY) -m cueai.ui.app
+ $(PY) -m pocket.ui.app
all: train bench figures facts ## Reproduce every published number and figure
diff --git a/README.md b/README.md
index 5cf97bf..8904350 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,15 @@
-# CueAI
+# Pocket Physics
+### ▶ [Play eight-ball against the bot][play] — in the browser, nothing to install
+
+[][play]
[](https://github.com/BruceMoseti/cueai/actions/workflows/ci.yml)


+Pocket billiards, simulated properly — then made small enough to fit in a
+browser tab.
+
**A physics simulator for billiards, a closed-form solution that replaces it, and
a learned model that corrects what the closed form misses — roughly 7,600x
faster than integration, with the accuracy measured and the failure mode stated.
@@ -22,7 +28,7 @@ closed-form references.
## Play it
-### **[▶ Play eight-ball against the bot](https://brucemoseti.github.io/cueai/)**
+**[▶ Open the table and break][play]**

@@ -40,7 +46,7 @@ found afterwards, and which of those facts made a shot a foul.

The browser is not running a lookalike physics engine. `web/js/physics.js` is a
-hand port of `src/cueai/physics/`, and the port is measured rather than
+hand port of `src/pocket/physics/`, and the port is measured rather than
asserted: `scripts/export_parity_cases.py` runs 35 shots through the Python
simulator — draw, follow, english off two rails, thin cuts, clusters and full
sixteen-ball breaks — and `web/test/parity.mjs` replays every one of them in
@@ -106,7 +112,7 @@ else is a ball rolling.
**And there is prose under the table.**
-[The explainer](https://brucemoseti.github.io/cueai/#how) covers the cloth
+[The explainer][explainer] covers the cloth
model, the parity harness, the bot's search, where the learned surrogate helps
and where it does not, and
[the multi-ball contact bug](#the-bug-the-tests-could-not-see) the single-ball
@@ -406,7 +412,7 @@ parity check and a run of headless games have passed.
## Repository map
```
-src/cueai/
+src/pocket/
physics/
ball.py four-state cloth dynamics for one ball
collisions.py frictional ball-ball impulses, cushions, pockets
@@ -425,7 +431,7 @@ src/cueai/
web/ the playable table: dependency-free ES modules
js/
- physics.js hand port of src/cueai/physics/, checked against it
+ physics.js hand port of src/pocket/physics/, checked against it
rack.js the same rack geometry, ported
game.js eight-ball rules, fouls, group assignment
bot.js closed-form candidate pots, then simulated rollouts
@@ -491,3 +497,8 @@ published ranges in those sources.
## License
MIT — see [LICENSE](LICENSE).
+
+
+
+[play]: https://brucemoseti.github.io/cueai/#play
+[explainer]: https://brucemoseti.github.io/cueai/#how
diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt
index ca6e22b..5998b4e 100644
--- a/cpp/CMakeLists.txt
+++ b/cpp/CMakeLists.txt
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16)
-project(cueai_cpp LANGUAGES CXX)
+project(pocket_cpp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
-add_executable(cueai_demo src/demo.cpp)
-target_include_directories(cueai_demo PRIVATE include)
+add_executable(pocket_demo src/demo.cpp)
+target_include_directories(pocket_demo PRIVATE include)
diff --git a/cpp/include/cueai/physics.hpp b/cpp/include/pocket/physics.hpp
similarity index 92%
rename from cpp/include/cueai/physics.hpp
rename to cpp/include/pocket/physics.hpp
index 66753d9..f1ec72b 100644
--- a/cpp/include/cueai/physics.hpp
+++ b/cpp/include/pocket/physics.hpp
@@ -1,9 +1,9 @@
-// CueAI C++ physics core — cloth sliding / rolling step (NumPy-parity helpers)
+// C++ physics core — cloth sliding / rolling step (NumPy-parity helpers)
#pragma once
#include
#include
-namespace cueai {
+namespace pocket {
constexpr double G = 9.81;
@@ -55,4 +55,4 @@ inline void integrate_rolling(BallState& b, double mu_r, double mu_sp, double dt
b.x += b.vx * dt; b.y += b.vy * dt;
}
-} // namespace cueai
+} // namespace pocket
diff --git a/cpp/src/demo.cpp b/cpp/src/demo.cpp
index 3224c9f..12b7b9b 100644
--- a/cpp/src/demo.cpp
+++ b/cpp/src/demo.cpp
@@ -1,9 +1,9 @@
-#include "cueai/physics.hpp"
+#include "pocket/physics.hpp"
#include
#include
int main() {
- cueai::BallState b;
+ pocket::BallState b;
b.x = 0.5; b.y = 0.5;
b.vx = 2.0; b.vy = 0.5;
b.wy = -2.0 / b.R; // backspin-ish slip
@@ -13,9 +13,9 @@ int main() {
auto u = b.slip();
double um = std::hypot(u[0], u[1]);
if (um > 1e-3)
- cueai::integrate_sliding(b, 0.2, 0.044, dt);
+ pocket::integrate_sliding(b, 0.2, 0.044, dt);
else
- cueai::integrate_rolling(b, 0.01, 0.044, dt);
+ pocket::integrate_rolling(b, 0.01, 0.044, dt);
if (b.speed() < 1e-4) break;
}
std::cout << std::fixed << std::setprecision(4)
diff --git a/docs/DESIGN.md b/docs/DESIGN.md
index 1a0eef1..a7bacc5 100644
--- a/docs/DESIGN.md
+++ b/docs/DESIGN.md
@@ -1,5 +1,8 @@
# Design
+▶ [Play the game](https://brucemoseti.github.io/cueai/#play) — the fourth tier
+described below, running in the browser.
+
## The problem this solves
Simulating a billiards shot accurately is slow. Sixteen bodies, each in one of
@@ -33,13 +36,13 @@ shot parameters (speed, angle, tip offset, cue position, cloth μ, cushion e)
## Three tiers, on purpose
-**Tier 1: the numerical simulator** (`cueai.physics.simulator`). Explicit Euler
+**Tier 1: the numerical simulator** (`pocket.physics.simulator`). Explicit Euler
integration at 1 ms, four-state cloth dynamics, frictional ball-ball impulses
with spin transfer, cushion rebound, pocket capture. This is the definition of
truth for everything else, and it is validated against closed-form results in
[VALIDATION.md](VALIDATION.md).
-**Tier 2: the closed-form solver** (`cueai.physics.analytic`). No integration at
+**Tier 2: the closed-form solver** (`pocket.physics.analytic`). No integration at
all. The observation that makes this possible: while a ball slides, the slip
velocity `u` decays along a *fixed direction*, so the friction force is constant
and the path over that phase is exactly a parabola of known duration
@@ -51,7 +54,7 @@ that detail right matters: a plain mirror-reflection approximation, which assume
the ball leaves the rail rolling, disagreed with the simulator by about a metre,
while this solver lands within 114 mm on direct shots and 225 mm across one rail.
-**Tier 3: the learned residual** (`cueai.ml`). A small MLP predicts the vector
+**Tier 3: the learned residual** (`pocket.ml`). A small MLP predicts the vector
from the closed-form endpoint to the simulated endpoint. Its head is initialised
to zero, so training starts from "trust the physics exactly" and moves away only
where the data insists.
diff --git a/docs/assets/web_bot.png b/docs/assets/web_bot.png
index cb4a04a..86ec154 100644
Binary files a/docs/assets/web_bot.png and b/docs/assets/web_bot.png differ
diff --git a/docs/assets/web_demo.gif b/docs/assets/web_demo.gif
index 1502fe0..f39eb05 100644
Binary files a/docs/assets/web_demo.gif and b/docs/assets/web_demo.gif differ
diff --git a/docs/assets/web_demo.mp4 b/docs/assets/web_demo.mp4
index 68a01f1..cc51a10 100644
Binary files a/docs/assets/web_demo.mp4 and b/docs/assets/web_demo.mp4 differ
diff --git a/docs/assets/web_game.png b/docs/assets/web_game.png
index 1a89014..f9e3ff7 100644
Binary files a/docs/assets/web_game.png and b/docs/assets/web_game.png differ
diff --git a/docs/assets/web_inspector.png b/docs/assets/web_inspector.png
index 3e94777..57ddfa3 100644
Binary files a/docs/assets/web_inspector.png and b/docs/assets/web_inspector.png differ
diff --git a/models/.gitkeep b/models/.gitkeep
index c74b2d2..2c6d3cc 100644
--- a/models/.gitkeep
+++ b/models/.gitkeep
@@ -1,3 +1,3 @@
{
- "note": "Run: python -m cueai.ml.train --n-samples 3000 --epochs 40"
+ "note": "Run: python -m pocket.ml.train --n-samples 3000 --epochs 40"
}
diff --git a/pyproject.toml b/pyproject.toml
index 3a2ad25..e05c740 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,7 +3,7 @@ requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
-name = "cueai"
+name = "pocket-physics"
version = "0.3.0"
description = "Physics-informed billiards simulation with a learned fast surrogate"
readme = "README.md"
@@ -36,9 +36,9 @@ dev = [
]
[project.scripts]
-cueai-ui = "cueai.ui.app:main"
-cueai-train = "cueai.ml.train:main"
-cueai-api = "cueai.api.main:run"
+pocket-ui = "pocket.ui.app:main"
+pocket-train = "pocket.ml.train:main"
+pocket-api = "pocket.api.main:run"
[tool.setuptools.packages.find]
where = ["src"]
@@ -70,7 +70,7 @@ ignore = [
# No python_version pin: mypy targets whichever interpreter runs it, so each entry
# in the CI matrix checks its own version against the numpy stubs built for it.
# Ruff's py310 target is what holds the floor for our own syntax.
-files = ["src/cueai"]
+files = ["src/pocket"]
ignore_missing_imports = true
warn_unused_ignores = true
warn_redundant_casts = true
diff --git a/scripts/benchmark.py b/scripts/benchmark.py
index cf6d4e9..b0ebcbb 100644
--- a/scripts/benchmark.py
+++ b/scripts/benchmark.py
@@ -3,7 +3,7 @@
Measure prediction latency and accuracy, and write docs/BENCHMARKS.md.
Everything the README claims about speed comes from this script. Run it after
-``cueai-train`` so the accuracy section can read models/metrics.json:
+``pocket-train`` so the accuracy section can read models/metrics.json:
python scripts/benchmark.py
"""
@@ -23,10 +23,10 @@
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
-from cueai.ml.infer import TrajectoryPredictor # noqa: E402
-from cueai.physics.analytic import predict_endpoint # noqa: E402
-from cueai.physics.constants import ShotParams, TableParams # noqa: E402
-from cueai.physics.simulator import Simulator # noqa: E402
+from pocket.ml.infer import TrajectoryPredictor # noqa: E402
+from pocket.physics.analytic import predict_endpoint # noqa: E402
+from pocket.physics.constants import ShotParams, TableParams # noqa: E402
+from pocket.physics.simulator import Simulator # noqa: E402
def time_calls(fn, repeats: int) -> dict[str, float]:
@@ -69,7 +69,7 @@ def measure_latency(repeats: int) -> dict[str, dict[str, float]]:
if gbm_path.exists():
import joblib
- from cueai.ml.features import build_features
+ from pocket.ml.features import build_features
bundle = joblib.load(gbm_path)
row = build_features(shot, cue, (1.4, 0.7), table)[None, :]
diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh
index 81358f7..e40c959 100644
--- a/scripts/bootstrap.sh
+++ b/scripts/bootstrap.sh
@@ -6,8 +6,8 @@ python3 -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -e ".[dev]" 2>/dev/null || pip install -r requirements.txt && pip install -e .
-python -m cueai.ml.train --n-samples 800 --epochs 15
+python -m pocket.ml.train --n-samples 800 --epochs 15
python -m pytest tests/ -q
-echo "CueAI setup complete."
-echo " API: uvicorn cueai.api.main:app --reload --port 8000"
-echo " UI: python -m cueai.ui.app"
+echo "Pocket Physics setup complete."
+echo " API: uvicorn pocket.api.main:app --reload --port 8000"
+echo " UI: python -m pocket.ui.app"
diff --git a/scripts/export_parity_cases.py b/scripts/export_parity_cases.py
index 6a738f6..cfcdce5 100644
--- a/scripts/export_parity_cases.py
+++ b/scripts/export_parity_cases.py
@@ -2,7 +2,7 @@
"""
Export reference shots so the browser physics can be checked against Python.
-The Python package under `src/cueai/physics/` is the definition of correct: it
+The Python package under `src/pocket/physics/` is the definition of correct: it
is what `tests/test_validation.py` pins to closed-form mechanics. The browser
runs a hand port of it, and a port is only worth anything if someone measures
the difference. This writes the initial conditions and the reference outcome
@@ -25,10 +25,10 @@
import numpy as np
-from cueai.physics.ball import Ball
-from cueai.physics.constants import BallParams, ShotParams, TableParams
-from cueai.physics.rack import identity_for, make_full_rack
-from cueai.physics.simulator import Simulator
+from pocket.physics.ball import Ball
+from pocket.physics.constants import BallParams, ShotParams, TableParams
+from pocket.physics.rack import identity_for, make_full_rack
+from pocket.physics.simulator import Simulator
PERTURBATION_M = 1e-12
diff --git a/scripts/make_figures.py b/scripts/make_figures.py
index 9f2edcb..e1c5fa7 100644
--- a/scripts/make_figures.py
+++ b/scripts/make_figures.py
@@ -25,10 +25,10 @@
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
-from cueai.physics import analytic # noqa: E402
-from cueai.physics.ball import Ball, MotionState, integrate_ball # noqa: E402
-from cueai.physics.constants import BallParams, ShotParams, TableParams # noqa: E402
-from cueai.physics.simulator import Simulator # noqa: E402
+from pocket.physics import analytic # noqa: E402
+from pocket.physics.ball import Ball, MotionState, integrate_ball # noqa: E402
+from pocket.physics.constants import BallParams, ShotParams, TableParams # noqa: E402
+from pocket.physics.simulator import Simulator # noqa: E402
ASSETS = ROOT / "docs" / "assets"
CLOTH = "#12764a"
diff --git a/src/cueai/__init__.py b/src/cueai/__init__.py
deleted file mode 100644
index 89621ad..0000000
--- a/src/cueai/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-"""CueAI — physics-informed billiards simulation + ML trajectory prediction."""
-
-__version__ = "0.1.0"
diff --git a/src/cueai/physics/__init__.py b/src/cueai/physics/__init__.py
deleted file mode 100644
index 2807080..0000000
--- a/src/cueai/physics/__init__.py
+++ /dev/null
@@ -1,18 +0,0 @@
-"""Physics package."""
-
-from cueai.physics.analytic import predict_endpoint, straight_shot
-from cueai.physics.constants import BallParams, ShotParams, TableParams
-from cueai.physics.rack import make_full_rack
-from cueai.physics.simulator import SimResult, Simulator, shot_feature_vector
-
-__all__ = [
- "BallParams",
- "ShotParams",
- "SimResult",
- "Simulator",
- "TableParams",
- "make_full_rack",
- "predict_endpoint",
- "shot_feature_vector",
- "straight_shot",
-]
diff --git a/src/pocket/__init__.py b/src/pocket/__init__.py
new file mode 100644
index 0000000..d610600
--- /dev/null
+++ b/src/pocket/__init__.py
@@ -0,0 +1,8 @@
+"""Pocket Physics — physics-informed billiards simulation + ML trajectory prediction."""
+
+from importlib.metadata import PackageNotFoundError, version
+
+try:
+ __version__ = version("pocket-physics")
+except PackageNotFoundError: # a source tree that was never installed
+ __version__ = "0.0.0+unknown"
diff --git a/src/cueai/api/__init__.py b/src/pocket/api/__init__.py
similarity index 100%
rename from src/cueai/api/__init__.py
rename to src/pocket/api/__init__.py
diff --git a/src/cueai/api/main.py b/src/pocket/api/main.py
similarity index 91%
rename from src/cueai/api/main.py
rename to src/pocket/api/main.py
index 999bc1a..0b02810 100644
--- a/src/cueai/api/main.py
+++ b/src/pocket/api/main.py
@@ -1,4 +1,4 @@
-"""FastAPI backend for CueAI full-rack simulation + prediction."""
+"""FastAPI backend for Pocket Physics full-rack simulation + prediction."""
from __future__ import annotations
@@ -10,15 +10,15 @@
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
-from cueai.ml.dataset import MAX_TIP_OFFSET
-from cueai.ml.infer import TrajectoryPredictor
-from cueai.physics.constants import ShotParams, TableParams
-from cueai.physics.rack import make_full_rack
+from pocket.ml.dataset import MAX_TIP_OFFSET
+from pocket.ml.infer import TrajectoryPredictor
+from pocket.physics.constants import ShotParams, TableParams
+from pocket.physics.rack import make_full_rack
API_VERSION = "0.3.0"
app = FastAPI(
- title="CueAI API",
+ title="Pocket Physics API",
description=(
"Physics-informed billiards prediction. /predict runs the full numerical "
"simulation; /predict/fast returns the closed-form plus learned-residual "
@@ -149,7 +149,7 @@ def table_info() -> dict:
def run() -> None:
import uvicorn
- uvicorn.run("cueai.api.main:app", host="0.0.0.0", port=8000, reload=True)
+ uvicorn.run("pocket.api.main:app", host="0.0.0.0", port=8000, reload=True)
if __name__ == "__main__":
diff --git a/src/cueai/ml/__init__.py b/src/pocket/ml/__init__.py
similarity index 52%
rename from src/cueai/ml/__init__.py
rename to src/pocket/ml/__init__.py
index c11bc78..a030104 100644
--- a/src/cueai/ml/__init__.py
+++ b/src/pocket/ml/__init__.py
@@ -1,5 +1,5 @@
"""ML package."""
-from cueai.ml.infer import TrajectoryPredictor
+from pocket.ml.infer import TrajectoryPredictor
__all__ = ["TrajectoryPredictor"]
diff --git a/src/cueai/ml/dataset.py b/src/pocket/ml/dataset.py
similarity index 96%
rename from src/cueai/ml/dataset.py
rename to src/pocket/ml/dataset.py
index 630b85e..07ab327 100644
--- a/src/cueai/ml/dataset.py
+++ b/src/pocket/ml/dataset.py
@@ -25,9 +25,9 @@
import pandas as pd
from tqdm import tqdm
-from cueai.physics.analytic import predict_endpoint
-from cueai.physics.constants import ShotParams, TableParams
-from cueai.physics.simulator import FEATURE_NAMES, Simulator, shot_feature_vector
+from pocket.physics.analytic import predict_endpoint
+from pocket.physics.constants import ShotParams, TableParams
+from pocket.physics.simulator import FEATURE_NAMES, Simulator, shot_feature_vector
TARGET_NAMES = [
"cue_end_x",
diff --git a/src/cueai/ml/features.py b/src/pocket/ml/features.py
similarity index 94%
rename from src/cueai/ml/features.py
rename to src/pocket/ml/features.py
index bae88b2..a1737c4 100644
--- a/src/cueai/ml/features.py
+++ b/src/pocket/ml/features.py
@@ -22,9 +22,9 @@
import numpy as np
import pandas as pd
-from cueai.physics.analytic import solve_free_ball
-from cueai.physics.constants import BallParams, ShotParams, TableParams
-from cueai.physics.simulator import FEATURE_NAMES as SHOT_FEATURE_NAMES
+from pocket.physics.analytic import solve_free_ball
+from pocket.physics.constants import BallParams, ShotParams, TableParams
+from pocket.physics.simulator import FEATURE_NAMES as SHOT_FEATURE_NAMES
BASELINE_FEATURE_NAMES = [
"base_cue_x", # closed-form resting position of the cue ball
@@ -54,7 +54,7 @@ def build_features(
radius: float = 0.028575,
) -> np.ndarray:
"""Model input for a single shot. Order matches :data:`FEATURE_NAMES`."""
- from cueai.physics.simulator import shot_feature_vector
+ from pocket.physics.simulator import shot_feature_vector
cue = np.asarray(cue_pos, dtype=np.float64)
obj = np.asarray(obj_pos, dtype=np.float64) if obj_pos is not None else None
diff --git a/src/cueai/ml/infer.py b/src/pocket/ml/infer.py
similarity index 95%
rename from src/cueai/ml/infer.py
rename to src/pocket/ml/infer.py
index 59e000b..d3c9291 100644
--- a/src/cueai/ml/infer.py
+++ b/src/pocket/ml/infer.py
@@ -20,12 +20,12 @@
import numpy as np
import torch
-from cueai.ml.features import FEATURE_NAMES, build_features
-from cueai.ml.model import CueNet
-from cueai.physics.analytic import predict_endpoint
-from cueai.physics.ball import Ball
-from cueai.physics.constants import ShotParams, TableParams
-from cueai.physics.simulator import Simulator
+from pocket.ml.features import FEATURE_NAMES, build_features
+from pocket.ml.model import CueNet
+from pocket.physics.analytic import predict_endpoint
+from pocket.physics.ball import Ball
+from pocket.physics.constants import ShotParams, TableParams
+from pocket.physics.simulator import Simulator
class TrajectoryPredictor:
@@ -82,7 +82,7 @@ def feature_vector(
obj_pos: tuple[float, float] | np.ndarray | None,
table: TableParams,
) -> np.ndarray:
- """Identical construction to training; see :mod:`cueai.ml.features`."""
+ """Identical construction to training; see :mod:`pocket.ml.features`."""
return build_features(
shot, cue_pos, obj_pos, table, radius=self.sim.ball_params.radius
)
diff --git a/src/cueai/ml/model.py b/src/pocket/ml/model.py
similarity index 100%
rename from src/cueai/ml/model.py
rename to src/pocket/ml/model.py
diff --git a/src/cueai/ml/train.py b/src/pocket/ml/train.py
similarity index 98%
rename from src/cueai/ml/train.py
rename to src/pocket/ml/train.py
index 7548277..4bc489e 100644
--- a/src/cueai/ml/train.py
+++ b/src/pocket/ml/train.py
@@ -30,9 +30,9 @@
from sklearn.preprocessing import StandardScaler
from torch.utils.data import DataLoader, TensorDataset
-from cueai.ml.dataset import BASELINE_NAMES, TARGET_NAMES, generate_dataset
-from cueai.ml.features import BASELINE_FEATURE_NAMES, FEATURE_NAMES, build_feature_frame
-from cueai.ml.model import CueNet
+from pocket.ml.dataset import BASELINE_NAMES, TARGET_NAMES, generate_dataset
+from pocket.ml.features import BASELINE_FEATURE_NAMES, FEATURE_NAMES, build_feature_frame
+from pocket.ml.model import CueNet
# Endpoint targets, in metres: (cue_x, cue_y, obj_x, obj_y)
ENDPOINT_TARGETS = TARGET_NAMES[:4]
@@ -291,7 +291,7 @@ def _export_onnx(net: CueNet, in_dim: int, path: Path) -> bool:
def main(argv: list[str] | None = None) -> dict:
- parser = argparse.ArgumentParser(description="Train CueAI models")
+ parser = argparse.ArgumentParser(description="Train Pocket Physics models")
parser.add_argument("--n-samples", type=int, default=4000)
parser.add_argument("--epochs", type=int, default=120)
parser.add_argument("--batch-size", type=int, default=128)
diff --git a/src/pocket/physics/__init__.py b/src/pocket/physics/__init__.py
new file mode 100644
index 0000000..e180494
--- /dev/null
+++ b/src/pocket/physics/__init__.py
@@ -0,0 +1,18 @@
+"""Physics package."""
+
+from pocket.physics.analytic import predict_endpoint, straight_shot
+from pocket.physics.constants import BallParams, ShotParams, TableParams
+from pocket.physics.rack import make_full_rack
+from pocket.physics.simulator import SimResult, Simulator, shot_feature_vector
+
+__all__ = [
+ "BallParams",
+ "ShotParams",
+ "SimResult",
+ "Simulator",
+ "TableParams",
+ "make_full_rack",
+ "predict_endpoint",
+ "shot_feature_vector",
+ "straight_shot",
+]
diff --git a/src/cueai/physics/analytic.py b/src/pocket/physics/analytic.py
similarity index 98%
rename from src/cueai/physics/analytic.py
rename to src/pocket/physics/analytic.py
index c21b99d..6c92c94 100644
--- a/src/cueai/physics/analytic.py
+++ b/src/pocket/physics/analytic.py
@@ -1,12 +1,12 @@
"""
Closed-form solutions for the straight-shot limit of the cloth model.
-These are the textbook results the numerical integrator in :mod:`cueai.physics.ball`
+These are the textbook results the numerical integrator in :mod:`pocket.physics.ball`
must reproduce, so they serve two purposes:
1. Ground truth for the validation suite (``tests/test_validation.py``).
2. A microsecond-cost baseline predictor that the learned residual model
- corrects (:mod:`cueai.ml`), which is what makes the "physics-informed"
+ corrects (:mod:`pocket.ml`), which is what makes the "physics-informed"
framing measurable rather than decorative.
Derivation (ball of mass m, radius R, I = 2/5 mR², struck along +x)
@@ -34,7 +34,7 @@
import numpy as np
-from cueai.physics.constants import BallParams, G, ShotParams, TableParams
+from pocket.physics.constants import BallParams, G, ShotParams, TableParams
# Slip decays 3.5x faster than the centre of mass because the friction torque
# also spins the ball up: 1 + mR²/I = 1 + 5/2.
diff --git a/src/cueai/physics/ball.py b/src/pocket/physics/ball.py
similarity index 98%
rename from src/cueai/physics/ball.py
rename to src/pocket/physics/ball.py
index 5567de1..b595a1d 100644
--- a/src/cueai/physics/ball.py
+++ b/src/pocket/physics/ball.py
@@ -9,10 +9,10 @@
import numpy as np
-from cueai.physics.constants import BallParams, G, TableParams
+from pocket.physics.constants import BallParams, G, TableParams
if TYPE_CHECKING:
- from cueai.physics.rack import BallIdentity
+ from pocket.physics.rack import BallIdentity
class MotionState(Enum):
diff --git a/src/cueai/physics/collisions.py b/src/pocket/physics/collisions.py
similarity index 99%
rename from src/cueai/physics/collisions.py
rename to src/pocket/physics/collisions.py
index 4fce6c5..9cf959b 100644
--- a/src/cueai/physics/collisions.py
+++ b/src/pocket/physics/collisions.py
@@ -4,8 +4,8 @@
import numpy as np
-from cueai.physics.ball import Ball
-from cueai.physics.constants import TableParams
+from pocket.physics.ball import Ball
+from pocket.physics.constants import TableParams
# Two balls count as touching once their surfaces are inside this band.
#
diff --git a/src/cueai/physics/constants.py b/src/pocket/physics/constants.py
similarity index 100%
rename from src/cueai/physics/constants.py
rename to src/pocket/physics/constants.py
diff --git a/src/cueai/physics/rack.py b/src/pocket/physics/rack.py
similarity index 97%
rename from src/cueai/physics/rack.py
rename to src/pocket/physics/rack.py
index 901d8d1..2696724 100644
--- a/src/cueai/physics/rack.py
+++ b/src/pocket/physics/rack.py
@@ -6,8 +6,8 @@
import numpy as np
-from cueai.physics.ball import Ball
-from cueai.physics.constants import BallParams, TableParams
+from pocket.physics.ball import Ball
+from pocket.physics.constants import BallParams, TableParams
# Standard pool ball colors (RGB 0-255)
BALL_COLORS: dict[int, tuple[int, int, int]] = {
diff --git a/src/cueai/physics/simulator.py b/src/pocket/physics/simulator.py
similarity index 97%
rename from src/cueai/physics/simulator.py
rename to src/pocket/physics/simulator.py
index fb7344a..40a4167 100644
--- a/src/cueai/physics/simulator.py
+++ b/src/pocket/physics/simulator.py
@@ -6,14 +6,14 @@
import numpy as np
-from cueai.physics.ball import Ball, integrate_ball
-from cueai.physics.collisions import (
+from pocket.physics.ball import Ball, integrate_ball
+from pocket.physics.collisions import (
check_pocket,
resolve_all_ball_collisions,
resolve_cushion,
)
-from cueai.physics.constants import BallParams, ShotParams, TableParams
-from cueai.physics.rack import make_full_rack
+from pocket.physics.constants import BallParams, ShotParams, TableParams
+from pocket.physics.rack import make_full_rack
@dataclass
@@ -56,7 +56,7 @@ def rack_cue_and_object(
obj_pos: tuple[float, float] | None = None,
) -> list[Ball]:
"""Legacy 2-ball setup (kept for ML dataset compatibility)."""
- from cueai.physics.rack import identity_for
+ from pocket.physics.rack import identity_for
L, W = self.table.length, self.table.width
if cue_pos is None:
diff --git a/src/cueai/ui/__init__.py b/src/pocket/ui/__init__.py
similarity index 100%
rename from src/cueai/ui/__init__.py
rename to src/pocket/ui/__init__.py
diff --git a/src/cueai/ui/app.py b/src/pocket/ui/app.py
similarity index 97%
rename from src/cueai/ui/app.py
rename to src/pocket/ui/app.py
index 2280bcb..a6f9095 100644
--- a/src/cueai/ui/app.py
+++ b/src/pocket/ui/app.py
@@ -1,4 +1,4 @@
-"""PyQt6 interactive CueAI — full 16-ball table."""
+"""PyQt6 interactive Pocket Physics — full 16-ball table."""
from __future__ import annotations
@@ -23,10 +23,10 @@
QWidget,
)
-from cueai.ml.infer import TrajectoryPredictor
-from cueai.physics.ball import Ball
-from cueai.physics.constants import ShotParams, TableParams
-from cueai.physics.rack import make_full_rack
+from pocket.ml.infer import TrajectoryPredictor
+from pocket.physics.ball import Ball
+from pocket.physics.constants import ShotParams, TableParams
+from pocket.physics.rack import make_full_rack
class TableCanvas(QWidget):
@@ -211,7 +211,7 @@ def paintEvent(self, _event) -> None:
p.setPen(QColor(210, 230, 210))
p.setFont(QFont("Avenir Next", 12, QFont.Weight.DemiBold))
- p.drawText(18, 24, "CueAI — full rack · spin · throw · multi-collision")
+ p.drawText(18, 24, "Pocket Physics — full rack · spin · throw · multi-collision")
def _draw_ball(self, p: QPainter, b: Ball, pos: np.ndarray, br_px: float) -> None:
c = self.world_to_screen(*pos)
@@ -303,7 +303,7 @@ def mouseReleaseEvent(self, _event) -> None:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
- self.setWindowTitle("CueAI — Full Rack Billiards")
+ self.setWindowTitle("Pocket Physics — Full Rack Billiards")
self.resize(1280, 700)
root = Path(__file__).resolve().parents[3]
self.predictor = TrajectoryPredictor(model_dir=root / "models")
diff --git a/src/cueai/vision/__init__.py b/src/pocket/vision/__init__.py
similarity index 100%
rename from src/cueai/vision/__init__.py
rename to src/pocket/vision/__init__.py
diff --git a/src/cueai/vision/overlay.py b/src/pocket/vision/overlay.py
similarity index 96%
rename from src/cueai/vision/overlay.py
rename to src/pocket/vision/overlay.py
index 9dbfcc9..42d223e 100644
--- a/src/cueai/vision/overlay.py
+++ b/src/pocket/vision/overlay.py
@@ -9,7 +9,7 @@
except ImportError: # pragma: no cover
cv2 = None
-from cueai.physics.constants import TableParams
+from pocket.physics.constants import TableParams
def render_trajectory_frame(
diff --git a/tests/test_api.py b/tests/test_api.py
index 30d324b..5af85ad 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -6,7 +6,7 @@
import pytest
from fastapi.testclient import TestClient
-from cueai.api.main import app
+from pocket.api.main import app
SOFT_SHOT = {"speed": 1.5, "angle_deg": 5.0, "full_rack": False, "cue_x": 0.6, "cue_y": 0.6}
diff --git a/tests/test_features.py b/tests/test_features.py
index df808a4..218f00d 100644
--- a/tests/test_features.py
+++ b/tests/test_features.py
@@ -12,9 +12,9 @@
import pandas as pd
import pytest
-from cueai.ml import features as feat
-from cueai.ml.dataset import simulate_sample
-from cueai.physics.constants import ShotParams, TableParams
+from pocket.ml import features as feat
+from pocket.ml.dataset import simulate_sample
+from pocket.physics.constants import ShotParams, TableParams
def test_feature_names_match_vector_length() -> None:
diff --git a/tests/test_metrics.py b/tests/test_metrics.py
index a216de2..e2430de 100644
--- a/tests/test_metrics.py
+++ b/tests/test_metrics.py
@@ -10,7 +10,7 @@
import numpy as np
-from cueai.ml.train import endpoint_errors, risk_coverage
+from pocket.ml.train import endpoint_errors, risk_coverage
def test_endpoint_errors_measures_distance_not_axes() -> None:
diff --git a/tests/test_physics.py b/tests/test_physics.py
index 58c95a9..455c3fc 100644
--- a/tests/test_physics.py
+++ b/tests/test_physics.py
@@ -1,13 +1,13 @@
-"""Unit tests for CueAI physics + full rack."""
+"""Unit tests for the physics core + full rack."""
from __future__ import annotations
import numpy as np
-from cueai.physics.ball import Ball, MotionState, integrate_ball
-from cueai.physics.constants import BallParams, ShotParams, TableParams
-from cueai.physics.rack import make_full_rack
-from cueai.physics.simulator import Simulator, shot_feature_vector
+from pocket.physics.ball import Ball, MotionState, integrate_ball
+from pocket.physics.constants import BallParams, ShotParams, TableParams
+from pocket.physics.rack import make_full_rack
+from pocket.physics.simulator import Simulator, shot_feature_vector
def test_ball_comes_to_rest():
diff --git a/tests/test_validation.py b/tests/test_validation.py
index 481ccb6..2957100 100644
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -12,17 +12,17 @@
import numpy as np
import pytest
-from cueai.physics import analytic
-from cueai.physics.ball import Ball, MotionState, integrate_ball
-from cueai.physics.collisions import (
+from pocket.physics import analytic
+from pocket.physics.ball import Ball, MotionState, integrate_ball
+from pocket.physics.collisions import (
CONTACT_BAND,
resolve_all_ball_collisions,
resolve_ball_ball,
resolve_cushion,
)
-from cueai.physics.constants import BallParams, G, ShotParams, TableParams
-from cueai.physics.rack import make_full_rack
-from cueai.physics.simulator import Simulator
+from pocket.physics.constants import BallParams, G, ShotParams, TableParams
+from pocket.physics.rack import make_full_rack
+from pocket.physics.simulator import Simulator
DT = 1e-4
SMOOTH_CLOTH = {"friction_noise_amp": 0.0}
diff --git a/web/css/style.css b/web/css/style.css
index 2e98a4e..bd42a1c 100644
--- a/web/css/style.css
+++ b/web/css/style.css
@@ -84,6 +84,8 @@ header.masthead {
grid-template-columns: minmax(0, 1fr) 330px;
gap: 20px;
align-items: start;
+ /* Landing on #play should not put the table flush against the viewport. */
+ scroll-margin-top: 14px;
}
@media (max-width: 1080px) {
diff --git a/web/index.html b/web/index.html
index 43389f6..bf7c225 100644
--- a/web/index.html
+++ b/web/index.html
@@ -3,10 +3,10 @@
- CueAI — play the simulator
+ Pocket Physics — play the simulator
-
CueAI
+
Pocket Physics
Eight-ball against a search-based opponent, on the same rigid-body physics the
Python reference is validated against.
-
+
@@ -236,7 +237,7 @@
The bug the tests could not see
The browser runs the reference, not a lookalike
web/js/physics.js is a hand port of
- src/cueai/physics/, and a port nobody measures is a rumour.
+ src/pocket/physics/, and a port nobody measures is a rumour.
scripts/export_parity_cases.py runs
35 shots through the Python simulator —
draw, follow, english off two rails, thin cuts, clusters, and full sixteen-ball
@@ -338,8 +339,9 @@
Reproducing all of it
diff --git a/web/js/main.js b/web/js/main.js
index 6837fa3..bbd6595 100644
--- a/web/js/main.js
+++ b/web/js/main.js
@@ -736,7 +736,7 @@ function syncUI() {
// Test seam. `web/test/browser.mjs` drives a real Chrome through this rather
// than through synthetic mouse maths, so the end-to-end test exercises the
// same functions the buttons call.
-window.cueai = {
+window.pocket = {
get state() {
return state;
},
diff --git a/web/js/physics.js b/web/js/physics.js
index 75cb4e3..c3e6f18 100644
--- a/web/js/physics.js
+++ b/web/js/physics.js
@@ -1,5 +1,5 @@
/**
- * Billiards physics, ported line for line from `src/cueai/physics/`.
+ * Billiards physics, ported line for line from `src/pocket/physics/`.
*
* This is deliberately a transcription rather than a reimplementation: the
* Python package is the reference that `tests/test_validation.py` checks
@@ -15,7 +15,7 @@ export const G = 9.81;
/**
* Two balls count as touching once their surfaces are inside this band.
*
- * Mirrors `CONTACT_BAND` in `cueai/physics/collisions.py`, where the reasoning
+ * Mirrors `CONTACT_BAND` in `pocket/physics/collisions.py`, where the reasoning
* is written out: the value has to sit far above the floating-point noise in a
* position and far below anything physical, and it must not coincide with the
* gap balls are racked at, or which of a rack's thirty contacts exist becomes
diff --git a/web/js/rack.js b/web/js/rack.js
index ebaff66..3194f03 100644
--- a/web/js/rack.js
+++ b/web/js/rack.js
@@ -1,4 +1,4 @@
-/** Ball identities, colours, and the opening rack. Mirrors `cueai/physics/rack.py`. */
+/** Ball identities, colours, and the opening rack. Mirrors `pocket/physics/rack.py`. */
import { BALL, makeBall } from "./physics.js";
@@ -70,7 +70,7 @@ export function rackOrder(seed = 7) {
}
export function trianglePositions(apex) {
- // Racked balls touch; see `triangle_positions` in `cueai/physics/rack.py`.
+ // Racked balls touch; see `triangle_positions` in `pocket/physics/rack.py`.
const gap = 2 * BALL.radius;
const spots = [];
for (let row = 0; row < 5; row++) {
diff --git a/web/test/browser.mjs b/web/test/browser.mjs
index 7046ab2..b8e7730 100644
--- a/web/test/browser.mjs
+++ b/web/test/browser.mjs
@@ -81,7 +81,7 @@ async function main() {
throw new Error(`page returned ${response ? response.status() : "no response"}`);
}
- await page.waitForFunction(() => window.cueai !== undefined, { timeout: 10000 });
+ await page.waitForFunction(() => window.pocket !== undefined, { timeout: 10000 });
// The canvas must actually have been painted, not merely created.
const painted = await page.evaluate(() => {
@@ -102,8 +102,25 @@ async function main() {
}
console.log(`page loaded and painted (${painted.width}x${painted.height} device pixels)`);
+
+ // The README and the page nav send people to #play expecting to land on the
+ // table. That is one attribute away from silently scrolling nowhere.
+ const anchors = await page.evaluate(() => {
+ const local = [...document.querySelectorAll("a[href^='#']")].map((a) => a.getAttribute("href"));
+ return {
+ broken: local.filter((href) => !document.querySelector(href)),
+ playIsTheTable: document.getElementById("play")?.contains(document.getElementById("table")),
+ };
+ });
+ if (anchors.broken.length) {
+ throw new Error(`links to nothing: ${anchors.broken.join(", ")}`);
+ }
+ if (!anchors.playIsTheTable) {
+ throw new Error("#play does not contain the table, so the play link lands somewhere else");
+ }
+ console.log("every in-page link resolves, and #play is the table");
for (let game = 0; game < games; game++) {
- if (game > 0) await page.evaluate(() => window.cueai.newGame());
+ if (game > 0) await page.evaluate(() => window.pocket.newGame());
const result = await playGame(page, shotLimit, verbose);
console.log(
`game ${game + 1}: ${result.shots} shots (${result.youShots} yours, ` +
@@ -135,13 +152,13 @@ async function playGame(page, shotLimit, verbose = false) {
let turns = 0;
for (; turns < shotLimit; turns++) {
- const mode = await page.evaluate(() => window.cueai.mode);
+ const mode = await page.evaluate(() => window.pocket.mode);
if (mode === "over") break;
if (mode === "placing") {
await page.evaluate(() => {
- const s = window.cueai.state;
- window.cueai.place(s.table.length * (s.behindHeadString ? 0.18 : 0.5), s.table.width * 0.5);
+ const s = window.pocket.state;
+ window.pocket.place(s.table.length * (s.behindHeadString ? 0.18 : 0.5), s.table.width * 0.5);
});
continue;
}
@@ -151,7 +168,7 @@ async function playGame(page, shotLimit, verbose = false) {
// eight is only chosen once the group is genuinely cleared, so a random
// walk does not end every game by potting it early.
await page.evaluate(() => {
- const s = window.cueai.state;
+ const s = window.pocket.state;
const cue = s.balls.find((b) => b.number === 0);
const suit = (n) => (n === 8 ? "eight" : n <= 7 ? "solid" : "stripe");
const mine = s.groups.you;
@@ -162,9 +179,9 @@ async function playGame(page, shotLimit, verbose = false) {
if (targets.length === 0) targets = onTable.filter((b) => b.number === 8);
if (targets.length === 0) targets = onTable;
const t = targets[Math.floor(Math.random() * targets.length)];
- window.cueai.aim(Math.atan2(t.y - cue.y, t.x - cue.x));
- window.cueai.setPower(0.3 + Math.random() * 0.35);
- window.cueai.shoot();
+ window.pocket.aim(Math.atan2(t.y - cue.y, t.x - cue.x));
+ window.pocket.setPower(0.3 + Math.random() * 0.35);
+ window.pocket.shoot();
});
await settle(page);
continue;
@@ -181,9 +198,9 @@ async function playGame(page, shotLimit, verbose = false) {
// The page records every resolved shot, which is the only reliable way to
// see the bot's turns: they begin and end inside a single wait.
const final = await page.evaluate(() => ({
- phase: window.cueai.state.phase,
- winner: window.cueai.state.winner,
- history: window.cueai.history,
+ phase: window.pocket.state.phase,
+ winner: window.pocket.state.winner,
+ history: window.pocket.history,
}));
const history = final.history;
@@ -213,19 +230,19 @@ async function playGame(page, shotLimit, verbose = false) {
/** Wait until the table is at rest and the page is ready for input again. */
async function settle(page) {
try {
- await page.waitForFunction(() => ["aim", "placing", "over"].includes(window.cueai.mode), {
+ await page.waitForFunction(() => ["aim", "placing", "over"].includes(window.pocket.mode), {
timeout: 60000,
polling: 120,
});
} catch {
// A stuck turn is the failure this test exists to catch, so say what stuck.
const snapshot = await page.evaluate(() => {
- const s = window.cueai.state;
+ const s = window.pocket.state;
const moving = s.balls
.filter((b) => !b.pocketed && Math.hypot(b.vx, b.vy) > 1e-4)
.map((b) => `${b.number}@${Math.hypot(b.vx, b.vy).toFixed(4)}m/s`);
return {
- mode: window.cueai.mode,
+ mode: window.pocket.mode,
turn: s.turn,
phase: s.phase,
ballInHand: s.ballInHand,
diff --git a/web/test/capture.mjs b/web/test/capture.mjs
index 6d98b97..d1c42ed 100644
--- a/web/test/capture.mjs
+++ b/web/test/capture.mjs
@@ -53,7 +53,7 @@ function skip(reason) {
/** Aim at the ghost ball for the straightest clear pot, the way a player would. */
const AIM_AT_BEST_POT = () => {
- const s = window.cueai.state;
+ const s = window.pocket.state;
const cue = s.balls.find((b) => b.number === 0);
const R = 0.028575;
const suit = (n) => (n === 8 ? "eight" : n <= 7 ? "solid" : "stripe");
@@ -96,8 +96,8 @@ const AIM_AT_BEST_POT = () => {
}
}
if (!best) return false;
- window.cueai.aim(best.angle);
- window.cueai.setPower(0.34);
+ window.pocket.aim(best.angle);
+ window.pocket.setPower(0.34);
return true;
};
@@ -108,7 +108,7 @@ const AIM_AT_BEST_POT = () => {
* both common and early, and the clip becomes a break and one shot.
*/
const AIM_AT_ANY_LEGAL = () => {
- const s = window.cueai.state;
+ const s = window.pocket.state;
const cue = s.balls.find((b) => b.number === 0);
const suit = (n) => (n === 8 ? "eight" : n <= 7 ? "solid" : "stripe");
const mine = s.groups.you;
@@ -119,8 +119,8 @@ const AIM_AT_ANY_LEGAL = () => {
const near = targets.reduce((a, b) =>
Math.hypot(b.x - cue.x, b.y - cue.y) < Math.hypot(a.x - cue.x, a.y - cue.y) ? b : a
);
- window.cueai.aim(Math.atan2(near.y - cue.y, near.x - cue.x));
- window.cueai.setPower(0.3);
+ window.pocket.aim(Math.atan2(near.y - cue.y, near.x - cue.x));
+ window.pocket.setPower(0.3);
return true;
};
@@ -133,7 +133,7 @@ const AIM_AT_ANY_LEGAL = () => {
* a shot chosen to be one.
*/
const AIM_INTO_OPEN_SPACE = () => {
- const s = window.cueai.state;
+ const s = window.pocket.state;
const cue = s.balls.find((b) => b.number === 0);
const R = 0.028575;
let best = null;
@@ -170,19 +170,19 @@ const AIM_INTO_OPEN_SPACE = () => {
if (!best || clear > best.clear) best = { clear, angle };
}
if (!best) return 0;
- window.cueai.aim(best.angle);
+ window.pocket.aim(best.angle);
// A ball struck at v slides 12v²/(49 μ g) before it rolls, so the stroke is
// sized to finish that inside the clear line with room to spare. Too hard and
// it reaches a rail mid-slide, which withdraws the prediction just as surely
// as hitting a ball does.
const slideRoom = 0.55 * best.clear;
const v = Math.sqrt((slideRoom * 49 * 0.2 * 9.81) / 12);
- window.cueai.setPower(Math.max(0.12, Math.min(0.6, v / 7.5)));
+ window.pocket.setPower(Math.max(0.12, Math.min(0.6, v / 7.5)));
return best.clear;
};
async function settle(page, timeout = 45000) {
- await page.waitForFunction(() => ["aim", "placing", "over"].includes(window.cueai.mode), {
+ await page.waitForFunction(() => ["aim", "placing", "over"].includes(window.pocket.mode), {
timeout,
polling: 100,
});
@@ -190,7 +190,7 @@ async function settle(page, timeout = 45000) {
/** Wait for the balls to stop, which is earlier than waiting for the turn. */
async function settleShot(page, timeout = 45000) {
- await page.waitForFunction(() => !["rolling", "stroking"].includes(window.cueai.mode), {
+ await page.waitForFunction(() => !["rolling", "stroking"].includes(window.pocket.mode), {
timeout,
polling: 50,
});
@@ -206,27 +206,33 @@ async function settleShot(page, timeout = 45000) {
* taking the picture — so the legend is read back before the shutter, and a
* shot that touched something first buys another turn rather than a caption
* explaining why the interesting line is missing.
+ *
+ * A cue ball that drops after rebounding satisfies the legend and still makes a
+ * poor picture: the trace stops mid-roll and the panel is captioned with a
+ * foul. The heading is chosen to miss the pockets, but only along the line to
+ * the first rail, so the outcome is checked as well as the aim.
*/
async function captureInspector(page, panel, file) {
const showsPrediction = () =>
document.getElementById("trace-legend").textContent.includes("5/7·v₀ =");
+ const cueStillOnTable = () => !window.pocket.state.balls.find((b) => b.number === 0).pocketed;
for (let attempt = 0; attempt < 5; attempt++) {
await settle(page);
- if (await page.evaluate(() => window.cueai.mode === "over")) break;
+ if (await page.evaluate(() => window.pocket.mode === "over")) break;
await page.evaluate(() => {
- if (window.cueai.mode === "placing") {
- const s = window.cueai.state;
- window.cueai.place(s.table.length * 0.45, s.table.width * 0.5);
+ if (window.pocket.mode === "placing") {
+ const s = window.pocket.state;
+ window.pocket.place(s.table.length * 0.45, s.table.width * 0.5);
}
});
// With no clean line available, play a pot instead: it rearranges the
// table, which is what the next attempt needs.
const clear = await page.evaluate(AIM_INTO_OPEN_SPACE);
if (!clear && !(await page.evaluate(AIM_AT_BEST_POT))) break;
- await page.evaluate(() => window.cueai.shoot());
+ await page.evaluate(() => window.pocket.shoot());
await settleShot(page);
- if (await page.evaluate(showsPrediction)) {
+ if (await page.evaluate(showsPrediction) && (await page.evaluate(cueStillOnTable))) {
await panel.screenshot({ path: file });
return true;
}
@@ -246,7 +252,7 @@ async function captureInspector(page, panel, file) {
*/
async function reportBreak(page, before) {
const spread = await page.evaluate((prior) => {
- const live = window.cueai.state.balls.filter((b) => !b.pocketed && b.number !== 0);
+ const live = window.pocket.state.balls.filter((b) => !b.pocketed && b.number !== 0);
const cx = live.reduce((a, b) => a + b.x, 0) / live.length;
const cy = live.reduce((a, b) => a + b.y, 0) / live.length;
const moved = live.filter((b, i) => Math.hypot(b.x - prior[i][0], b.y - prior[i][1]) > 0.05);
@@ -296,7 +302,7 @@ async function main() {
const page = await browser.newPage();
await page.setViewport({ width: 1420, height: 940, deviceScaleFactor: 2 });
await page.goto(url, { waitUntil: "networkidle0", timeout: 30000 });
- await page.waitForFunction(() => window.cueai !== undefined, { timeout: 10000 });
+ await page.waitForFunction(() => window.pocket !== undefined, { timeout: 10000 });
await page.select("#difficulty", "sharp");
// "Quick". The last seconds of a pool shot are balls creeping to a halt,
// which is honest physics and dull footage; the page offers the speed, so
@@ -309,40 +315,40 @@ async function main() {
// leaves the rack standing, which made the clip an advertisement for a
// problem the simulator does not have.
await page.evaluate(() => {
- const s = window.cueai.state;
- window.cueai.place(s.table.length * 0.2, s.table.width * 0.52);
+ const s = window.pocket.state;
+ window.pocket.place(s.table.length * 0.2, s.table.width * 0.52);
const cue = s.balls.find((b) => b.number === 0);
const apex = s.balls
.filter((b) => !b.pocketed && b.number !== 0)
.reduce((a, b) => (b.x < a.x ? b : a));
// The same fraction off square the bot uses: dead centre sends the
// energy back down the table instead of into the corners.
- window.cueai.aim(Math.atan2(apex.y - cue.y, apex.x - cue.x) + 0.004);
- window.cueai.setPower(0.95);
+ window.pocket.aim(Math.atan2(apex.y - cue.y, apex.x - cue.x) + 0.004);
+ window.pocket.setPower(0.95);
});
const before = await page.evaluate(() =>
- window.cueai.state.balls.filter((b) => b.number !== 0).map((b) => [b.x, b.y])
+ window.pocket.state.balls.filter((b) => b.number !== 0).map((b) => [b.x, b.y])
);
const frames = noVideo || !haveFfmpeg ? null : await startScreencast(page);
- await page.evaluate(() => window.cueai.shoot());
+ await page.evaluate(() => window.pocket.shoot());
await settle(page);
await reportBreak(page, before);
// Real shots, so the trace and the bot panel have content.
for (let i = 0; i < 30; i++) {
- const mode = await page.evaluate(() => window.cueai.mode);
+ const mode = await page.evaluate(() => window.pocket.mode);
if (mode === "over") break;
if (mode === "placing") {
await page.evaluate(() => {
- const s = window.cueai.state;
- window.cueai.place(s.table.length * (s.behindHeadString ? 0.2 : 0.45), s.table.width * 0.5);
+ const s = window.pocket.state;
+ window.pocket.place(s.table.length * (s.behindHeadString ? 0.2 : 0.45), s.table.width * 0.5);
});
}
const aimed =
(await page.evaluate(AIM_AT_BEST_POT)) || (await page.evaluate(AIM_AT_ANY_LEGAL));
if (!aimed) break;
- await page.evaluate(() => window.cueai.shoot());
+ await page.evaluate(() => window.pocket.shoot());
await settle(page);
if (frames && frames.elapsed() > seconds) break;
}
@@ -354,9 +360,9 @@ async function main() {
// Line up a shot for the stills, then hold it.
await page.evaluate(() => {
- if (window.cueai.mode === "placing") {
- const s = window.cueai.state;
- window.cueai.place(s.table.length * 0.45, s.table.width * 0.5);
+ if (window.pocket.mode === "placing") {
+ const s = window.pocket.state;
+ window.pocket.place(s.table.length * 0.45, s.table.width * 0.5);
}
});
await page.evaluate(AIM_AT_BEST_POT);
@@ -397,7 +403,7 @@ async function startScreencast(page) {
await page.evaluate(() => {
window.__modeMarks = [];
window.__modeTimer = setInterval(() => {
- window.__modeMarks.push([performance.timeOrigin + performance.now(), window.cueai.mode]);
+ window.__modeMarks.push([performance.timeOrigin + performance.now(), window.pocket.mode]);
}, 60);
});
await client.send("Page.startScreencast", {
diff --git a/web/test/input.mjs b/web/test/input.mjs
index b86c8f4..d301410 100644
--- a/web/test/input.mjs
+++ b/web/test/input.mjs
@@ -1,7 +1,7 @@
/**
* Drive the game with a real cursor and real keys.
*
- * `browser.mjs` plays through `window.cueai`, which proves the modules wire
+ * `browser.mjs` plays through `window.pocket`, which proves the modules wire
* together but skips the layer a person actually touches: pointer capture,
* drag thresholds, which element owns the spacebar. Those are where an
* interactive page goes wrong, and they cannot be tested by calling the
@@ -56,7 +56,7 @@ function check(name, ok, detail = "") {
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
async function settle(page, timeout = 60000) {
- await page.waitForFunction(() => ["aim", "placing", "over"].includes(window.cueai.mode), {
+ await page.waitForFunction(() => ["aim", "placing", "over"].includes(window.pocket.mode), {
timeout,
polling: 100,
});
@@ -64,24 +64,24 @@ async function settle(page, timeout = 60000) {
/** Put the game in a known state: cue ball placed, our turn, ready to aim. */
async function resetToAim(page) {
- await page.evaluate(() => window.cueai.newGame());
+ await page.evaluate(() => window.pocket.newGame());
const spot = await page.evaluate(() => {
- const s = window.cueai.state;
- return window.cueai.toClient(s.table.length * 0.18, s.table.width * 0.5);
+ const s = window.pocket.state;
+ return window.pocket.toClient(s.table.length * 0.18, s.table.width * 0.5);
});
await page.mouse.click(spot.x, spot.y);
- const mode = await page.evaluate(() => window.cueai.mode);
+ const mode = await page.evaluate(() => window.pocket.mode);
if (mode !== "aim") throw new Error(`placing a cue ball by clicking left mode "${mode}"`);
}
/** Viewport pixels for a point in table metres. */
function at(page, x, y) {
- return page.evaluate(([tx, ty]) => window.cueai.toClient(tx, ty), [x, y]);
+ return page.evaluate(([tx, ty]) => window.pocket.toClient(tx, ty), [x, y]);
}
async function cueBall(page) {
return page.evaluate(() => {
- const b = window.cueai.state.balls.find((ball) => ball.number === 0);
+ const b = window.pocket.state.balls.find((ball) => ball.number === 0);
return { x: b.x, y: b.y };
});
}
@@ -116,7 +116,7 @@ async function main() {
page.on("pageerror", (error) => problems.push(`uncaught: ${error.message}`));
await page.goto(url, { waitUntil: "networkidle0", timeout: 30000 });
- await page.waitForFunction(() => window.cueai !== undefined, { timeout: 10000 });
+ await page.waitForFunction(() => window.pocket !== undefined, { timeout: 10000 });
await page.select("#playback", "3"); // the shots here are means, not ends
await placingPutsTheBallWhereYouClick(page);
@@ -147,16 +147,16 @@ async function main() {
// ---------- the checks ----------
async function placingPutsTheBallWhereYouClick(page) {
- await page.evaluate(() => window.cueai.newGame());
- const before = await page.evaluate(() => window.cueai.mode);
+ await page.evaluate(() => window.pocket.newGame());
+ const before = await page.evaluate(() => window.pocket.mode);
const target = await page.evaluate(() => {
- const s = window.cueai.state;
+ const s = window.pocket.state;
return { x: s.table.length * 0.18, y: s.table.width * 0.36 };
});
const spot = await at(page, target.x, target.y);
await page.mouse.click(spot.x, spot.y);
const cue = await cueBall(page);
- const mode = await page.evaluate(() => window.cueai.mode);
+ const mode = await page.evaluate(() => window.pocket.mode);
const off = Math.hypot(cue.x - target.x, cue.y - target.y);
check(
"clicking behind the head string places the cue ball there",
@@ -166,15 +166,15 @@ async function placingPutsTheBallWhereYouClick(page) {
// In front of the head string is illegal on the break, and the page snaps
// back to the nearest legal spot rather than silently ignoring the click.
- await page.evaluate(() => window.cueai.newGame());
+ await page.evaluate(() => window.pocket.newGame());
const illegal = await page.evaluate(() => {
- const s = window.cueai.state;
+ const s = window.pocket.state;
return { x: s.table.length * 0.8, y: s.table.width * 0.5 };
});
const far = await at(page, illegal.x, illegal.y);
await page.mouse.click(far.x, far.y);
const after = await cueBall(page);
- const headString = await page.evaluate(() => window.cueai.state.table.length * 0.25);
+ const headString = await page.evaluate(() => window.pocket.state.table.length * 0.25);
check(
"an illegal placement snaps behind the head string",
after.x <= headString + 1e-6,
@@ -185,7 +185,7 @@ async function placingPutsTheBallWhereYouClick(page) {
async function theCueFollowsTheCursor(page) {
await resetToAim(page);
const cue = await cueBall(page);
- const table = await page.evaluate(() => window.cueai.state.table);
+ const table = await page.evaluate(() => window.pocket.state.table);
const targets = [
[cue.x + 0.5, cue.y],
@@ -196,7 +196,7 @@ async function theCueFollowsTheCursor(page) {
for (const [tx, ty] of targets) {
const p = await at(page, Math.min(tx, table.length - 0.05), Math.max(0.05, ty));
await page.mouse.move(p.x, p.y);
- const angle = await page.evaluate(() => window.cueai.aimAngle);
+ const angle = await page.evaluate(() => window.pocket.aimAngle);
const want = Math.atan2(Math.max(0.05, ty) - cue.y, Math.min(tx, table.length - 0.05) - cue.x);
let delta = Math.abs(angle - want);
if (delta > Math.PI) delta = 2 * Math.PI - delta;
@@ -214,7 +214,7 @@ async function shiftAimsSlowly(page) {
const cue = await cueBall(page);
const start = await at(page, cue.x + 0.5, cue.y);
await page.mouse.move(start.x, start.y);
- const before = await page.evaluate(() => window.cueai.aimAngle);
+ const before = await page.evaluate(() => window.pocket.aimAngle);
// One shift-move a long way round: the aim should ease toward the cursor
// rather than snap to it, which is what makes a quarter-degree cut reachable.
@@ -222,7 +222,7 @@ async function shiftAimsSlowly(page) {
await page.keyboard.down("Shift");
await page.mouse.move(away.x, away.y);
await page.keyboard.up("Shift");
- const after = await page.evaluate(() => window.cueai.aimAngle);
+ const after = await page.evaluate(() => window.pocket.aimAngle);
const demanded = Math.atan2(cue.y + 0.35 - cue.y, cue.x + 0.35 - cue.x) - before;
const moved = after - before;
@@ -238,13 +238,13 @@ async function theArrowKeysMoveByAHair(page) {
await resetToAim(page);
await page.evaluate(() => document.body.focus());
- const a0 = await page.evaluate(() => window.cueai.aimAngle);
+ const a0 = await page.evaluate(() => window.pocket.aimAngle);
await page.keyboard.press("ArrowRight");
- const a1 = await page.evaluate(() => window.cueai.aimAngle);
+ const a1 = await page.evaluate(() => window.pocket.aimAngle);
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowRight");
await page.keyboard.up("Shift");
- const a2 = await page.evaluate(() => window.cueai.aimAngle);
+ const a2 = await page.evaluate(() => window.pocket.aimAngle);
const coarse = a1 - a0;
const fine = a2 - a1;
@@ -254,9 +254,9 @@ async function theArrowKeysMoveByAHair(page) {
`${((coarse * 180) / Math.PI).toFixed(3)}° plain, ${((fine * 180) / Math.PI).toFixed(3)}° with shift`
);
- const p0 = await page.evaluate(() => window.cueai.power);
+ const p0 = await page.evaluate(() => window.pocket.power);
await page.keyboard.press("ArrowUp");
- const p1 = await page.evaluate(() => window.cueai.power);
+ const p1 = await page.evaluate(() => window.pocket.power);
const readout = await page.evaluate(() => document.getElementById("power-readout").textContent);
check(
"the up arrow raises the power, and the readout follows",
@@ -275,8 +275,8 @@ async function aQuickClickDoesNotShoot(page) {
await page.mouse.up();
await wait(120);
- const mode = await page.evaluate(() => window.cueai.mode);
- const shots = await page.evaluate(() => window.cueai.history.length);
+ const mode = await page.evaluate(() => window.pocket.mode);
+ const shots = await page.evaluate(() => window.pocket.history.length);
check(
"a quick click lines the shot up rather than playing it",
mode === "aim" && shots === 0,
@@ -295,7 +295,7 @@ async function holdingStillDoesShoot(page) {
await page.mouse.up();
await wait(120);
- const mode = await page.evaluate(() => window.cueai.mode);
+ const mode = await page.evaluate(() => window.pocket.mode);
check(
"a press held on the spot plays the shot",
["stroking", "rolling"].includes(mode),
@@ -312,13 +312,13 @@ async function pullingTheCueBackSetsThePower(page) {
// drawn, and the further it comes back the harder the shot.
const ahead = await at(page, cue.x + 0.5, cue.y);
await page.mouse.move(ahead.x, ahead.y);
- const angle = await page.evaluate(() => window.cueai.aimAngle);
- const before = await page.evaluate(() => window.cueai.power);
+ const angle = await page.evaluate(() => window.pocket.aimAngle);
+ const before = await page.evaluate(() => window.pocket.power);
await page.mouse.down();
const short = await at(page, cue.x + 0.5 - DRAG_DEADZONE * 0.4, cue.y);
await page.mouse.move(short.x, short.y);
- const nudged = await page.evaluate(() => window.cueai.power);
+ const nudged = await page.evaluate(() => window.pocket.power);
check(
"a twitch inside the dead zone does not change the power",
Math.abs(nudged - before) < 1e-9,
@@ -327,8 +327,8 @@ async function pullingTheCueBackSetsThePower(page) {
const drawn = await at(page, cue.x + 0.5 - 0.25, cue.y);
await page.mouse.move(drawn.x, drawn.y, { steps: 6 });
- const pulled = await page.evaluate(() => window.cueai.power);
- const aimHeld = await page.evaluate(() => window.cueai.aimAngle);
+ const pulled = await page.evaluate(() => window.pocket.power);
+ const aimHeld = await page.evaluate(() => window.pocket.aimAngle);
check(
"drawing the cue back raises the power without disturbing the aim",
pulled > before + 0.1 && Math.abs(aimHeld - angle) < 1e-9,
@@ -337,7 +337,7 @@ async function pullingTheCueBackSetsThePower(page) {
await page.mouse.up();
await wait(120);
- const mode = await page.evaluate(() => window.cueai.mode);
+ const mode = await page.evaluate(() => window.pocket.mode);
check(
"releasing a drawn cue plays the shot",
["stroking", "rolling"].includes(mode),
@@ -354,7 +354,7 @@ async function theControlsOwnTheirOwnKeys(page) {
await page.focus("#difficulty");
await page.keyboard.press("Space");
await wait(120);
- const afterSelect = await page.evaluate(() => window.cueai.mode);
+ const afterSelect = await page.evaluate(() => window.pocket.mode);
check(
"space in a dropdown does not play a shot",
afterSelect === "aim",
@@ -364,7 +364,7 @@ async function theControlsOwnTheirOwnKeys(page) {
await page.evaluate(() => document.activeElement.blur());
await page.keyboard.press("Space");
await wait(120);
- const afterTable = await page.evaluate(() => window.cueai.mode);
+ const afterTable = await page.evaluate(() => window.pocket.mode);
check(
"space with nothing focused plays the shot",
["stroking", "rolling"].includes(afterTable),
@@ -376,15 +376,15 @@ async function theControlsOwnTheirOwnKeys(page) {
async function theSpinWidgetMovesTheTip(page) {
await resetToAim(page);
const box = await (await page.$("#spin")).boundingBox();
- const before = await page.evaluate(() => window.cueai.spin);
+ const before = await page.evaluate(() => window.pocket.spin);
// Bottom of the circle: draw. Then drag well outside it, which must clamp to
// the miscue limit rather than let the tip leave the ball.
await page.mouse.move(box.x + box.width / 2, box.y + box.height * 0.78);
await page.mouse.down();
- const drawn = await page.evaluate(() => window.cueai.spin);
+ const drawn = await page.evaluate(() => window.pocket.spin);
await page.mouse.move(box.x + box.width * 2, box.y + box.height / 2, { steps: 4 });
- const clamped = await page.evaluate(() => window.cueai.spin);
+ const clamped = await page.evaluate(() => window.pocket.spin);
await page.mouse.up();
const magnitude = Math.hypot(clamped.x, clamped.y);
@@ -400,7 +400,7 @@ async function theShootButtonWorks(page) {
const disabledBefore = await page.evaluate(() => document.getElementById("shoot").disabled);
await page.click("#shoot");
await wait(120);
- const mode = await page.evaluate(() => window.cueai.mode);
+ const mode = await page.evaluate(() => window.pocket.mode);
const disabledDuring = await page.evaluate(() => document.getElementById("shoot").disabled);
check(
"the shoot button plays a shot and locks while the balls roll",
diff --git a/web/test/parity.mjs b/web/test/parity.mjs
index 89837f9..33a6e28 100644
--- a/web/test/parity.mjs
+++ b/web/test/parity.mjs
@@ -2,7 +2,7 @@
* Check the browser physics against the Python reference simulator.
*
* `scripts/export_parity_cases.py` runs a spread of shots through
- * `src/cueai/physics/` and records where every ball came to rest. This replays
+ * `src/pocket/physics/` and records where every ball came to rest. This replays
* the same shots through the module the game actually uses and reports the
* worst disagreement. A port that is not measured is a rumour.
*