Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
# CueAI
# Pocket Physics

### ▶ [Play eight-ball against the bot][play] — in the browser, nothing to install

[![Play](https://img.shields.io/badge/play-eight--ball%20in%20the%20browser-12594a)][play]
[![CI](https://github.com/BruceMoseti/cueai/actions/workflows/ci.yml/badge.svg)](https://github.com/BruceMoseti/cueai/actions/workflows/ci.yml)
![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue)
![License](https://img.shields.io/badge/license-MIT-green)

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.
Expand All @@ -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]**

![Eight-ball against the search bot, in the browser](docs/assets/web_demo.gif)

Expand All @@ -40,7 +46,7 @@ found afterwards, and which of those facts made a shot a foul.
![The whole interface: table, live cue-ball trace, the bot's report and the shot log](docs/assets/web_game.png)

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
Expand Down Expand Up @@ -106,7 +112,7 @@ else is a ball rolling.
<br clear="right" />

**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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -491,3 +497,8 @@ published ranges in those sources.
## License

MIT — see [LICENSE](LICENSE).

<!-- The published game. Renaming the repository moves this URL with it. -->

[play]: https://brucemoseti.github.io/cueai/#play
[explainer]: https://brucemoseti.github.io/cueai/#how
6 changes: 3 additions & 3 deletions cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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 <cmath>
#include <array>

namespace cueai {
namespace pocket {

constexpr double G = 9.81;

Expand Down Expand Up @@ -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
8 changes: 4 additions & 4 deletions cpp/src/demo.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#include "cueai/physics.hpp"
#include "pocket/physics.hpp"
#include <iostream>
#include <iomanip>

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
Expand All @@ -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)
Expand Down
9 changes: 6 additions & 3 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
Binary file modified docs/assets/web_bot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/web_demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/web_demo.mp4
Binary file not shown.
Binary file modified docs/assets/web_game.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/web_inspector.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion models/.gitkeep
Original file line number Diff line number Diff line change
@@ -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"
}
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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
12 changes: 6 additions & 6 deletions scripts/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand All @@ -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]:
Expand Down Expand Up @@ -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, :]
Expand Down
8 changes: 4 additions & 4 deletions scripts/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
10 changes: 5 additions & 5 deletions scripts/export_parity_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
8 changes: 4 additions & 4 deletions scripts/make_figures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 0 additions & 3 deletions src/cueai/__init__.py

This file was deleted.

18 changes: 0 additions & 18 deletions src/cueai/physics/__init__.py

This file was deleted.

8 changes: 8 additions & 0 deletions src/pocket/__init__.py
Original file line number Diff line number Diff line change
@@ -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"
File renamed without changes.
Loading
Loading