Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
81197dd
fix(physics): correct slip velocity and friction torque signs
cursoragent Aug 12, 2026
fe693c0
feat(physics): closed-form free-ball solver as a baseline predictor
cursoragent Aug 12, 2026
3869656
chore: package hygiene, lint, types and CI
cursoragent Aug 12, 2026
cace58e
feat(ml): learn the residual against the closed-form solver
cursoragent Aug 12, 2026
2ceba5b
docs: publish measured results, validation and design notes
cursoragent Aug 12, 2026
8c5e2de
feat(ml): report accuracy against coverage using the free closed-form…
cursoragent Aug 12, 2026
763bd57
fix(api): reject tip offsets past the miscue limit
cursoragent Aug 12, 2026
8d7a41c
test(physics): bound label discretisation and pin ghost-ball aim geom…
cursoragent Aug 12, 2026
0b6c716
feat(ml): stratify by ball-ball contact and ablate the physics features
cursoragent Aug 12, 2026
e84b315
docs: report what the averages hide and where the residual loses
cursoragent Aug 12, 2026
815c046
fix(infer): report no object endpoint when no object ball was given
cursoragent Aug 12, 2026
f44d05e
feat(web): playable 8-ball against a search bot on the ported physics
cursoragent Aug 12, 2026
123b35b
fix(physics): stop vertical spin chattering around zero
cursoragent Aug 12, 2026
e1c46e3
fix(physics): make every contact in a rack visible to the solver
cursoragent Aug 12, 2026
e01bf31
feat(web): steady the shot loop and show the cue, the drop and the ha…
cursoragent Aug 12, 2026
0733120
feat(web): quote measured numbers on the page, and fail CI when they …
cursoragent Aug 12, 2026
5f8de49
test(web): play the real page in Chrome, and record the README assets…
cursoragent Aug 12, 2026
97ddb27
style: satisfy ruff on the new scripts, and ignore the node_modules s…
cursoragent Aug 12, 2026
59b5982
ci: deploy the playable table to Pages, but only if it matches the re…
cursoragent Aug 12, 2026
774a42e
docs: republish every number against the corrected physics
cursoragent Aug 12, 2026
23401fb
test(physics): assert that a harder break opens the table further
cursoragent Aug 12, 2026
85cc130
docs: record the README assets on an idle machine and pick the shots …
cursoragent Aug 12, 2026
6a20f73
test(web): check that no two balls ever share space while the shot is…
cursoragent Aug 12, 2026
9e17ddb
docs: quantify the break-spread fix and the restitution choice behind it
cursoragent Aug 12, 2026
5b7a8a8
fix(web): say when the cue ball drops, and keep the trace labels legible
cursoragent Aug 12, 2026
e953ff5
test(web): choose the inspector still deliberately and verify it befo…
cursoragent Aug 12, 2026
fc3c686
docs: re-record the README stills and clip from the running page
cursoragent Aug 12, 2026
903d8e9
feat(web): keep the referee's reasoning on screen in a shot log
cursoragent Aug 12, 2026
02ede60
docs: re-record the stills with the shot log in frame
cursoragent Aug 12, 2026
721344e
docs: mention the shot log in the tour of the page
cursoragent Aug 12, 2026
a26c121
docs: stop committing stills nothing links to
cursoragent Aug 12, 2026
9d3424b
docs: re-record the stills after trimming the capture set
cursoragent Aug 12, 2026
fa06071
docs: describe the browser port and the search that runs on it
cursoragent Aug 12, 2026
81bb447
test(web): play the page with a real cursor and keyboard
cursoragent Aug 12, 2026
d7eb9bb
fix(capture): aim the demo break at the apex, and keep the clip moving
cursoragent Aug 12, 2026
00f4cb8
test(web): assert that the break opens the rack
cursoragent Aug 12, 2026
baf99d2
fix(ci): let mypy target the interpreter it runs on
cursoragent Aug 12, 2026
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
122 changes: 122 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
check:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip

- name: Install
run: |
python -m pip install --upgrade pip
pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install -e ".[dev]"

- name: Lint
run: ruff check src tests scripts

- name: Type check
run: mypy

- name: Test
run: pytest tests/ -q

# The playable page quotes measured figures. This fails if any of them
# has drifted from what the artefacts in the repository actually say.
- name: Fail if the numbers on the playable page are stale
if: matrix.python-version == '3.12'
run: python scripts/site_facts.py --check

browser-physics:
# The browser runs a hand port of src/cueai/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
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip

- uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install
run: |
python -m pip install --upgrade pip
pip install -e .

- name: Fail if the committed reference shots no longer match the simulator
run: python scripts/export_parity_cases.py --check

- name: Check the browser physics against the reference
run: node web/test/parity.mjs --verbose

- name: Play the bot against itself
run: node web/test/selfplay.mjs --games 6 --difficulty club

- name: Load the page in Chrome and play a game
run: |
npm install --no-save puppeteer-core
python -m http.server 8123 --directory web &
npx --yes wait-on http://localhost:8123/index.html
node web/test/browser.mjs --url http://localhost:8123/index.html --games 1

# The layer above: pointer capture, the drag threshold, and which element
# owns the spacebar. Driven with a real cursor, because calling the
# handlers directly is exactly what would miss a regression here.
- name: Drive the page with a real cursor and keyboard
run: node web/test/input.mjs --url http://localhost:8123/index.html

pipeline:
# Proves the data -> train -> export -> serve path still works end to end,
# on a dataset small enough to finish in a couple of minutes.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip

- name: Install
run: |
python -m pip install --upgrade pip
pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install -e ".[dev]"

- name: Train on a small dataset
run: python -m cueai.ml.train --n-samples 400 --epochs 10

- name: Benchmark
run: python scripts/benchmark.py --repeats 50

- name: Render figures
run: python scripts/make_figures.py

- uses: actions/upload-artifact@v4
with:
name: cueai-pipeline-output
path: |
models/metrics.json
models/latency.json
docs/BENCHMARKS.md
docs/assets/*.png
66 changes: 66 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Deploy the playable table

# The game is dependency-free ES modules, so "building" it is copying web/.
# It still has to pass the physics parity check before it goes live: a page
# that plays differently from the reference simulator would undercut the point
# of publishing it.

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip

- uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install the reference simulator
run: |
python -m pip install --upgrade pip
pip install -e .

- name: Check the browser physics against the reference
run: |
python scripts/export_parity_cases.py --check
node web/test/parity.mjs

- name: Play the bot against itself
run: node web/test/selfplay.mjs --games 4 --difficulty club

deploy:
needs: verify
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4

- uses: actions/configure-pages@v5

- uses: actions/upload-pages-artifact@v3
with:
path: web

- id: deployment
uses: actions/deploy-pages@v4
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ data/processed/
*.log
.DS_Store
.cppbuild/

# Puppeteer, installed on demand by web/test/browser.mjs
node_modules
package-lock.json
docs/assets/.frames/
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 CueAI 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
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
77 changes: 77 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
.PHONY: help setup test lint typecheck check train bench figures facts api ui play \
parity parity-check selfplay browser input capture web clean all
.DEFAULT_GOAL := help

PY ?= python3
NODE ?= node
# Defaults reproduce the published numbers in models/metrics.json.
SAMPLES ?= 20000
EPOCHS ?= 300
GAMES ?= 20
PORT ?= 8123

help: ## Show available targets
@grep -E '^[a-z-]+:.*?## ' $(MAKEFILE_LIST) | awk -F':.*## ' '{printf " \033[36m%-10s\033[0m %s\n", $$1, $$2}'

setup: ## Install the package with dev extras
$(PY) -m pip install -e ".[dev]"

test: ## Run the test suite
$(PY) -m pytest tests/ -q

lint: ## Check style
$(PY) -m ruff check src tests scripts

typecheck: ## Run static type checks
$(PY) -m mypy

check: lint typecheck test ## Everything CI runs on the Python side

parity: ## Re-export reference shots and check the browser physics against them
$(PY) scripts/export_parity_cases.py
$(NODE) web/test/parity.mjs --verbose
$(PY) scripts/site_facts.py

parity-check: ## Verify the committed reference shots without rewriting them
$(PY) scripts/export_parity_cases.py --check
$(NODE) web/test/parity.mjs

selfplay: ## Play the bot against itself headlessly, exercising every rule
$(NODE) web/test/selfplay.mjs --games $(GAMES) --difficulty sharp --vs relaxed

browser: ## Load the page in Chrome and play a game (needs puppeteer-core)
$(NODE) web/test/browser.mjs --url http://localhost:$(PORT)/index.html --games 2
$(NODE) web/test/input.mjs --url http://localhost:$(PORT)/index.html

capture: ## Re-record the screenshots and the clip in the README
$(NODE) web/test/capture.mjs --url http://localhost:$(PORT)/index.html

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)"
@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)

bench: ## Measure latency and rewrite docs/BENCHMARKS.md
$(PY) scripts/benchmark.py

figures: ## Render the README figures into docs/assets
$(PY) scripts/make_figures.py

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

ui: ## Launch the desktop table (needs the ui extra)
$(PY) -m cueai.ui.app

all: train bench figures facts ## Reproduce every published number and figure

clean: ## Remove generated artefacts
rm -rf .pytest_cache .mypy_cache .ruff_cache **/__pycache__
rm -f models/*.pt models/*.onnx models/*.joblib
Loading
Loading