Skip to content

Add snooker cue#283

Merged
ekiefl merged 6 commits intomainfrom
ek/snooker-cue
Apr 5, 2026
Merged

Add snooker cue#283
ekiefl merged 6 commits intomainfrom
ek/snooker-cue

Conversation

@ekiefl
Copy link
Copy Markdown
Owner

@ekiefl ekiefl commented Mar 24, 2026

This PR adds game-specific cue sticks, starting with a snooker cue.

CueSpecs refactor

  • Removed field-level defaults and static factory methods (CueSpecs.default(), CueSpecs.snooker()) in favor of the PrebuiltCueSpecs enum + CUE_SPECS dict pattern, mirroring PrebuiltBallParams/BALL_PARAMS.
  • Added CueSpecs.default(game_type) and CueSpecs.prebuilt(name) classmethods.
  • Three prebuilts: POOL_GENERIC, SNOOKER_GENERIC, BILLIARD_GENERIC (billiard currently copies pool values).

Cue model support

  • Added model_name: str | None to Cue, following the same pattern as Ball.ballset and Table.model_descr — a rendering identifier on the game object, with path resolution in the renderer.
  • CUE_MODELS dict maps each PrebuiltCueSpecs to a model directory name.
  • Restructured models/cue/ into subdirectories (cue/, cue_snooker/) so each cue model has its own directory.
  • CueRender.init_model() resolves the model from Cue.model_name.

Snooker cue model

  • Added models/cue/cue_snooker/cue.glb (new 3D model).

Game type wiring

  • Cue.from_game_type() now sets both specs and model_name from the prebuilt mapping, so snooker games get the snooker cue automatically.

Acknowledgements

Huge thanks to @alrusdi for creating the beautiful snooker cue model. It worked out of the box and looks great. Thanks!

Update test fixtures to match current CueSpecs schema.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: baa13d96-7147-4c74-a0e9-c7ed1b2c9fa9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ek/snooker-cue

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ekiefl
Copy link
Copy Markdown
Owner Author

ekiefl commented Mar 24, 2026

Fixture patching

Removing field-level defaults from CueSpecs broke deserialization of old msgpack test fixtures. Some were missing shaft_radius_at_tip and shaft_radius_at_butt; the case1-4 fixtures also had a stale butt_radius key and were missing end_mass.

The fixtures were patched with the script below, then round-tripped through pooltool's own System.load/System.save to ensure canonical serialization format.

import msgpack
from pathlib import Path

from pooltool.system.datatypes import System

DEFAULTS = {
    "brand": "Pooltool",
    "M": 0.567,
    "length": 1.4732,
    "tip_radius": 0.0106045,
    "shaft_radius_at_tip": 0.0065,
    "shaft_radius_at_butt": 0.02,
    "end_mass": 0.170097 / 30,
}
EXPECTED_KEYS = set(DEFAULTS.keys())


def patch_specs(specs: dict) -> bool:
    changed = False
    for k in list(specs.keys()):
        if k not in EXPECTED_KEYS:
            del specs[k]
            changed = True
    for k, v in DEFAULTS.items():
        if k not in specs:
            specs[k] = v
            changed = True
    return changed


def patch_fixture(path: Path) -> bool:
    with open(path, "rb") as f:
        data = msgpack.unpackb(f.read(), raw=False)

    changed = patch_specs(data["cue"]["specs"])

    if "events" in data:
        for event in data["events"]:
            if "agents" in event:
                for agent in event["agents"]:
                    if isinstance(agent, dict):
                        for state_key in ("initial", "final"):
                            state = agent.get(state_key)
                            if isinstance(state, dict) and "specs" in state:
                                changed |= patch_specs(state["specs"])

    if changed:
        with open(path, "wb") as f:
            f.write(msgpack.packb(data, use_bin_type=True))

    return changed


tests_dir = Path("tests")
fixtures = sorted(tests_dir.rglob("*.msgpack"))

for p in fixtures:
    patched = patch_fixture(p)
    status = "Patched" if patched else "OK"
    print(f"{status}: {p.relative_to(tests_dir)}")

print("\nRound-tripping through pooltool serializer...")
for p in fixtures:
    s = System.load(p)
    s.save(p)
    print(f"Re-saved: {p.relative_to(tests_dir)}")

@ekiefl ekiefl changed the title Move CueSpecs defaults into classmethods Add snooker cue Mar 24, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 24, 2026

Codecov Report

❌ Patch coverage is 90.90909% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.36%. Comparing base (d77c2ed) to head (4438f7f).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pooltool/ani/animate.py 0.00% 2 Missing ⚠️
pooltool/objects/cue/render.py 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #283      +/-   ##
==========================================
+ Coverage   46.23%   46.36%   +0.13%     
==========================================
  Files         144      144              
  Lines       10287    10315      +28     
==========================================
+ Hits         4756     4783      +27     
- Misses       5531     5532       +1     
Flag Coverage Δ
service 46.36% <90.90%> (+0.13%) ⬆️
service-no-ani 56.52% <95.23%> (+0.18%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

return CueSpecs()
return CueSpecs(
brand="Pooltool",
M=0.567,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My snooker que params as example:

M=0.478
length=1.475
shaft_radius_at_tip=0.0049
shaft_radius_at_butt=0.0124

I'm not sure what end_mass means. How to measure it?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

It's kind of an abstract concept. It's defined in here though: https://drdavepoolinfo.com/technical_proofs/new/TP_A-31.pdf

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://drdavepoolinfo.com/technical_proofs/new/TP_A-31.pdf

From Claude:

The document uses a ball mass of 6 oz (about 170 g) and then works with mass ratios (m_b/m_e) that imply a range of end mass values.

From the plots on page 5, the end mass axis runs from roughly 5 to 15 grams. More specifically, the bounds used are m_b/50 on the low end and m_b/10 on the high end, which works out to about 3.4 g to 17 g.

The example calculations on page 6 give two representative cases:

  • High-squirt cue (like a break cue): mass ratio of 15, meaning an end mass of about 11.3 g
  • Low-squirt cue (a low-deflection playing shaft): mass ratio of 40, meaning an end mass of about 4.25 g

So the practical range for real cues appears to be roughly 4–12 grams — a surprisingly small amount of mass governing how much the cue ball deflects sideways.


Given this, I think it makes sense to replace 0.170097 / 30 (pool ball's mass over 30) with 0.140 / 30 (snooker ball's mass over 30).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a number for the tip radius or should I leave it as 0.0106045?

@ekiefl ekiefl marked this pull request as ready for review April 5, 2026 20:38
@ekiefl
Copy link
Copy Markdown
Owner Author

ekiefl commented Apr 5, 2026

The lint and test CI failures are unrelated to this PR — both failed because the Panda3D buildbot server (buildbot.panda3d.org) timed out during panda3d wheel download. The typecheck job hit the same issue initially but passed on re-run. Likely just flakey server stuff.

@ekiefl ekiefl merged commit d6b1bef into main Apr 5, 2026
10 of 12 checks passed
@ekiefl ekiefl deleted the ek/snooker-cue branch April 5, 2026 20:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants