From dda07fb3d81f93dbbf6decd367be2607ebf3672d Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Mon, 23 Mar 2026 23:27:53 -0700 Subject: [PATCH 1/5] Move CueSpecs defaults from field definitions into classmethods Update test fixtures to match current CueSpecs schema. --- pooltool/objects/cue/datatypes.py | 40 ++++++++++++------ tests/events/example_system.msgpack | Bin 20309 -> 20401 bytes .../event_based/test_data/case1.msgpack | Bin 15760 -> 16314 bytes .../event_based/test_data/case2.msgpack | Bin 9065 -> 9479 bytes .../event_based/test_data/case3.msgpack | Bin 4939 -> 5361 bytes .../event_based/test_data/case4.msgpack | Bin 4947 -> 5361 bytes .../test_shots/01_test_shot_no_point.msgpack | Bin 39079 -> 39171 bytes .../test_shots/01a_test_shot_no_point.msgpack | Bin 39123 -> 39215 bytes .../test_shots/02_test_shot_ispoint.msgpack | Bin 37461 -> 37569 bytes .../test_shots/02a_test_shot_ispoint.msgpack | Bin 37487 -> 37595 bytes .../test_shots/03_test_shot_ispoint.msgpack | Bin 34807 -> 34915 bytes .../test_shots/03a_test_shot_ispoint.msgpack | Bin 34845 -> 34953 bytes .../test_shots/04_test_shot_no_point.msgpack | Bin 34827 -> 34919 bytes .../test_shots/04a_test_shot_no_point.msgpack | Bin 34853 -> 34945 bytes .../test_shots/05_test_shot_ispoint.msgpack | Bin 39121 -> 39213 bytes .../test_shots/05a_test_shot_ispoint.msgpack | Bin 39147 -> 39239 bytes 16 files changed, 27 insertions(+), 13 deletions(-) diff --git a/pooltool/objects/cue/datatypes.py b/pooltool/objects/cue/datatypes.py index 00aa6808..64c4bf5f 100755 --- a/pooltool/objects/cue/datatypes.py +++ b/pooltool/objects/cue/datatypes.py @@ -27,28 +27,42 @@ class CueSpecs: end_mass: The mass of the of the cue's end. This controls the amount of deflection (squirt) that occurs when using sidespin. Lower means less deflection. It is - defined here: - https://billiards.colostate.edu/technical_proofs/new/TP_A-31.pdf. + defined here: https://drdavepoolinfo.com/technical_proofs/new/TP_A-31.pdf. """ - brand: str = field(default="Predator") - M: float = field(default=0.567) - length: float = field(default=1.4732) - tip_radius: float = field(default=0.0106045) # nickel radius - shaft_radius_at_tip: float = field( - default=0.0065 - ) # 13 mm shaft diameter at the tip - shaft_radius_at_butt: float = field(default=0.02) - end_mass: float = field(default=0.170097 / 30) + brand: str = field() + M: float = field() + length: float = field() + tip_radius: float = field() + shaft_radius_at_tip: float = field() + shaft_radius_at_butt: float = field() + end_mass: float = field() @staticmethod def default() -> CueSpecs: """Construct a default cue spec""" - return CueSpecs() + return CueSpecs( + 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, + ) @staticmethod def snooker() -> CueSpecs: - raise NotImplementedError() + # FIXME: this is just a copy paste of the pool cue specs + return CueSpecs( + 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, + ) @define diff --git a/tests/events/example_system.msgpack b/tests/events/example_system.msgpack index 36ad1082cc4ebe2149156462daceaee0283c8cdb..8856fd95169df57fd08881696901503a956ed2a6 100644 GIT binary patch delta 160 zcmcaQk8$IC#tG?+?GrQXj5Zf%B&LV(D`4yLwYuIK+rJKxflQ!Q{t=0qpk8wVy delta 65 zcmdlupYiHE#tG?+Z4)!>cvdHsmXyR7C8lJS7H_-}t1#JCaXF^U#2Yf33zhaVZ#Gk_ G)&u~Acb diff --git a/tests/evolution/event_based/test_data/case1.msgpack b/tests/evolution/event_based/test_data/case1.msgpack index b493d40a53996bd39c82e47f7b58704ed83e60a8..ef8f1e28727a79bf3b1597d95fc5e1449e066f7a 100644 GIT binary patch delta 1012 zcmbPGy{mpgI%E693_IV=#TkidCGkaxDVe3k@rfnzC7A`M?W^YRD?72patpd-QfW!a zY5OTv58c;^RIfsA~Od<8EeQ#`ekkg{L!iP1cw8mn@A>I&Hsl zCJ4-0oC@N+O9O++f)c8n%aamwa*9(+4ou!?KTM>87}E4waV#c_d$vbdPv!KBNQ=h1)8mYj)qP zs|E`NbFljxHOO)QOih?67GP7}YLjk?$7VKNut((e$?-^`fix%x1+3a8OG^ur9Fs+x zc}(1yI8HM_z~o>vSFmX|lQS*6Cd-?XZej!b(zMLH#2k>DK&Fr$B5jioSo(uqF_UEf LAlpt}h};JNFBqQE delta 468 zcmdm0KcRX;I%Dg^3_H=)Nu?zv@kNO#nWe?2?Wa^dbYCY@z40@%Fk9QA%#?+OlkF@_ zCmV{yGImV9XyHFONK0jMqO!;4Tu})ohzcXHiUZ=PD%>~!mXL(1Fb1pemqu0LvAJ6Y zW`+q^#XUJx74DlQ6<}tVf>q=yA*tA$sGI<`!3-?>M-@pH=t}p^=IWA6jBS$xHK(IE z-+l8jEwID`9aIUA&0%^HARYMzs1oj*pBPF5B_^57L@~)@@_I|H$2WBcTb!qXYMCNGromn@A>I&Hsl zCJ4-0oC@N+O9O++iIS?E%aamwa*9(+4oo(XbDyj%Y7NvIFG`Nyjbb3ZZ^cO0>oM6u z!V0LjT7n$C=OnGz+81S}EHs_mAYnS$St^!fk9$mxk%k~%lPH0g%8Pkt$54RMJ% z*pPTx(hX^X7+?W5;H@0#26#+nQvijfqXOw+={b3-A}9_oD$XP$2-TFW1Wz+S!Lrn{ J)Vz}72>=ZMu zlbuCk89OE~6!D)tNl9h$ekJ$Gb)wb~1x8>6UqsOr=!-)Y7=slo6-QOzG5Mi{6=U0E z8OiA=CVNbtBn1*bD}^rZECUj+lR*`CpZrBs<_AGFh!6# l6BSX#TR1@8U7TE+3U+RTGP-m3s913_EK4m*%_}LM000oHdmsP+ diff --git a/tests/evolution/event_based/test_data/case3.msgpack b/tests/evolution/event_based/test_data/case3.msgpack index 0a0c61f51162d3d3e7a311938d43933b1e4503a4..806ee0f30d3eda84ea119ea337e6e48391f238f3 100644 GIT binary patch delta 772 zcmX@D_EB?!80TpR1qLvfDDJ`7J~6@0cXM$@Vp>UjQDRDFX>ojFNqk9W!D;)d`TNRF zY_Z&eE}2wXQgYgUO4UR6bt2U(Qu9*ca}$e;PurJU1{tba|Ju0uKNDN~qRfS&+wHvNS&FwEfDNATVojDv0wg4Gbn<TQR^uFdKN3R{X6(rEiz=5`z zo18#9!vi(M9BhaqFR6xrLNAIBYKR5ckn?<`8nS@D3E~w?umQ0Gq#6KnMVX)t2PhFO ePA*La8}nX}bYq$XPeW2V1H-b^vedki;t2r7!9=|P delta 360 zcmeyUd0K6P7$e6-c@M_ci5YgHtCLDgO5%$WQ!-17Puowadg#7Rqw-eldA=%qj=qY@&_SXptzp!Of>POBG#M?%Tmiy^Gb>*008^JeHH)! diff --git a/tests/evolution/event_based/test_data/case4.msgpack b/tests/evolution/event_based/test_data/case4.msgpack index 43c4eed94e551692aa78159a3887022b003024a7..285568bcea4948e65aeec6dcdc79ac54d5f71942 100644 GIT binary patch delta 743 zcmcbt_EB>}I%E693_IV=#TkidCGkaxDVe3k@rfnzC7A`M?W^YRD?72patpd-QfW!a zY5OTv58c;^RIfsA~Od`VjO~+mGf!vin*5N}U$Qhl>9qaI znIJH0aVm)OE)5JOFJw{WT%MGelT(~pa$s@+r^jSIRx7slMVToJjV2#pHl5te8cVVv z?vrP;Swjpl1{)&CPL3g(98g0{z=q7{Al;BAh$~FN2Iz8;Zh-q_S8i)yST=H#6P8za ztbls?dCAe6#0SzlmyaC1ANWCfwFOAm>pr64OfJixN{ZON-+ZOX5p13r^ct&EHpcVvFS#bjhUB tl9I^_G-NhjxTD5#ngIePuTft~u3?)OXl!NXgqg%JxgfQ5^KG3o*#K;xLvR29 delta 65 zcmZqP#I$@P(}YaMwuxDGJgbvROG@I45>qlui#Oi-p*H!J`btchiMM1n3uw+{-n>yK GCV(D*Q&1~*PzXdGn+a diff --git a/tests/ruleset/test_shots/02_test_shot_ispoint.msgpack b/tests/ruleset/test_shots/02_test_shot_ispoint.msgpack index 0e7ac2381b255fc2ce219078a98a69bd43c28e74..ddac4a41f0385bb0eafeff78845efe423b39ed6e 100644 GIT binary patch delta 200 zcmcb*gz4Z?rU}ZNryaZ-7(if>x-Db-WIJ^`qs_$`iD@PAMTseyrN!}yCGjPh1*h$+ z=I<*zvBh!=x@1ymNy)}FA5=L`gNz>% diff --git a/tests/ruleset/test_shots/02a_test_shot_ispoint.msgpack b/tests/ruleset/test_shots/02a_test_shot_ispoint.msgpack index 7f5c3cef1f19862570c047df73f2b27efdb60d8b..50561aeee2cdec6fe08c2d8fcda59414934e3154 100644 GIT binary patch delta 194 zcmaFAgz5HDrU}ZNryaZ-7(ie$qq-eq`(y`oJEP6T8Hs5n@kNO#nWe??i6!wRnFXiq ztLE=3JF&%b3%X=dX-Ub(wI5VDPJ_(>nlx7}nM&qPTr0DAnfe@NPMFyYlN*XOHfL+I GqyqqTut!P& delta 80 zcmcb;l=FRK0b<+U~7$8&t diff --git a/tests/ruleset/test_shots/03_test_shot_ispoint.msgpack b/tests/ruleset/test_shots/03_test_shot_ispoint.msgpack index 2b00513592ad79e4bb11a65c189ea9c98c3b841e..c67f5b90157eec0b0b01f67124b7c0841d576dd2 100644 GIT binary patch delta 189 zcmey~&-8c#(*$MC(+=JZ3?MN1gNiL<`(!;;JEP6T8Hs5n@kNO#nWe??i6!wRnFXiq ztLE=3JF&%b3%X=dX-Ub(H4l_IPJ_(>n$)9`OeJ$Cu94Y1MRg`K-0aP|ny2Cb#z#kQ delta 86 zcmaDnf$4ic(*$Kk^T~Ruwv26)b5!kkRwtE~l*AV$reu~DPrjonv+>&z<;jOsl5r~9 Ud`ERFGb6*~4~DIq<+WVm0I4k@Q2+n{ diff --git a/tests/ruleset/test_shots/03a_test_shot_ispoint.msgpack b/tests/ruleset/test_shots/03a_test_shot_ispoint.msgpack index b1cbec3ee983eb56b804a13a6efcff8eae297216..984fd9b04c1e723bb8f69a193094c456b27f7473 100644 GIT binary patch delta 194 zcmbO`fvIyM(*$MC(+=JZ3?MN1qlz74`(y)EJEP6T8Hs5n@kNO#nWe??i6!wRnFXiq ztLE=3JF&%b3%X=dX-Ub(wGWgzPJ_(>n$)Y3OeJ$Cu9ew5O?3`4C(LZ1Gh^E~@6t?; F0|2w^Nn8K` delta 94 zcmeC2$TW8X(*$Kk^U3KQx|Nxc OVX{DF>t=Z^k2nC0K^`ms diff --git a/tests/ruleset/test_shots/04a_test_shot_no_point.msgpack b/tests/ruleset/test_shots/04a_test_shot_no_point.msgpack index 74475770d782a7a1ce4d3acd257dd367f9f04faa..c344bc847835683f91e355755282d346f6ce5874 100644 GIT binary patch delta 167 zcmZ2FfvIsK(}XO>_Q?*ac1D|vGZNEE;)@bfGE0l&6HDStG7C=ISIys7c4CX=7Iev^ u(vp(NU8*u0FCJ0mIL!b7lc%bzBHOghU8*~nIblXIOpbSG-+W3lIt~CvMM3=l delta 82 zcmZpi$h33<(}XO>w#j*_c08+-N=r)OixN{ZON%GpQKUx`UaK XVe*DZjmhUt;wL}Q_S#&aB^U<)h3X;- diff --git a/tests/ruleset/test_shots/05_test_shot_ispoint.msgpack b/tests/ruleset/test_shots/05_test_shot_ispoint.msgpack index ac886a693fe6774f5c353503050de68a0ef812fb..d3267dd1f1ec6b2d191dd7cd9ca8ffbb78f193e2 100644 GIT binary patch delta 165 zcmcb(k!kHFrU{vh?Gv-?j5Zf%B&LV(D*Q&1~*PzXdG Date: Sun, 5 Apr 2026 11:42:13 -0700 Subject: [PATCH 2/5] Update snooker cue values --- pooltool/objects/cue/datatypes.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pooltool/objects/cue/datatypes.py b/pooltool/objects/cue/datatypes.py index 64c4bf5f..b025560e 100755 --- a/pooltool/objects/cue/datatypes.py +++ b/pooltool/objects/cue/datatypes.py @@ -48,20 +48,19 @@ def default() -> CueSpecs: tip_radius=0.0106045, shaft_radius_at_tip=0.0065, shaft_radius_at_butt=0.02, - end_mass=0.170097 / 30, + end_mass=0.170097 / 30, # pool ball mass over 30 ) @staticmethod def snooker() -> CueSpecs: - # FIXME: this is just a copy paste of the pool cue specs return CueSpecs( brand="Pooltool", - M=0.567, - length=1.4732, + M=0.478, + length=1.475, tip_radius=0.0106045, - shaft_radius_at_tip=0.0065, - shaft_radius_at_butt=0.02, - end_mass=0.170097 / 30, + shaft_radius_at_tip=0.0049, + shaft_radius_at_butt=0.0124, + end_mass=0.140 / 30, # snooker ball mass over 30 ) From d0e17c33aad71981d9a26eee75e4008f21f90fcb Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Sun, 5 Apr 2026 12:19:16 -0700 Subject: [PATCH 3/5] Add game type to cue stick mapping --- pooltool/objects/cue/datatypes.py | 28 ++++++++++++++++++++++++++++ tests/objects/cue/test_datatypes.py | 27 ++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/pooltool/objects/cue/datatypes.py b/pooltool/objects/cue/datatypes.py index b025560e..70ebe079 100755 --- a/pooltool/objects/cue/datatypes.py +++ b/pooltool/objects/cue/datatypes.py @@ -2,8 +2,12 @@ from __future__ import annotations +from collections.abc import Callable + from attrs import define, evolve, field, fields_dict +from pooltool.game.datatypes import GameType + @define(frozen=True) class CueSpecs: @@ -64,6 +68,15 @@ def snooker() -> CueSpecs: ) +default_cuespecs_map: dict[GameType, Callable[[], CueSpecs]] = { + GameType.EIGHTBALL: CueSpecs.default, + GameType.NINEBALL: CueSpecs.default, + GameType.THREECUSHION: CueSpecs.default, + GameType.SNOOKER: CueSpecs.snooker, + GameType.SUMTOTHREE: CueSpecs.default, +} + + @define class Cue: """A cue stick. @@ -193,6 +206,21 @@ def set_state( if cue_ball_id is not None: self.cue_ball_id = cue_ball_id + @classmethod + def from_game_type(cls, game_type: GameType, id: str | None = None) -> Cue: + if game_type not in default_cuespecs_map: + raise NotImplementedError( + f"There is no cue stick associated with '{game_type}'" + ) + + if id is None: + id = fields_dict(cls)["id"].default + assert id is not None + + cue = cls(id=id) + cue.specs = default_cuespecs_map[game_type]() + return cue + @classmethod def default(cls) -> Cue: """Construct a cue with defaults""" diff --git a/tests/objects/cue/test_datatypes.py b/tests/objects/cue/test_datatypes.py index a52eebe4..163f13b5 100644 --- a/tests/objects/cue/test_datatypes.py +++ b/tests/objects/cue/test_datatypes.py @@ -1,7 +1,8 @@ import pytest from attrs.exceptions import FrozenInstanceError -from pooltool.objects.cue.datatypes import Cue +from pooltool.game.datatypes import GameType +from pooltool.objects.cue.datatypes import Cue, CueSpecs def test_cue_copy(): @@ -20,3 +21,27 @@ def test_cue_copy(): cue.phi += 1 assert cue != copy assert cue.phi != copy.phi + + +def test_cue_specs_construction(): + # Can't instantiate without setting parameters + with pytest.raises(TypeError): + CueSpecs() # type: ignore + + # All default methods construct properly + CueSpecs.default() + CueSpecs.snooker() + + +def test_cue_from_game_type(): + assert Cue.from_game_type(GameType.EIGHTBALL).specs == CueSpecs.default() + assert Cue.from_game_type(GameType.SNOOKER).specs == CueSpecs.snooker() + + # ID is passed through + assert Cue.from_game_type(GameType.SNOOKER).id == "cue_stick" + assert Cue.from_game_type(GameType.SNOOKER, id="other").id == "other" + + +def test_cue_from_game_type_unknown(): + with pytest.raises(NotImplementedError): + Cue.from_game_type("unknown_game") # type: ignore From 75befadaeec0062df687c77e84891bb3d852feb9 Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Sun, 5 Apr 2026 12:54:42 -0700 Subject: [PATCH 4/5] Mirror BallParams prebuilts --- pooltool/objects/__init__.py | 3 +- pooltool/objects/cue/datatypes.py | 122 ++++++++++++++++++++-------- tests/objects/cue/test_datatypes.py | 11 ++- 3 files changed, 95 insertions(+), 41 deletions(-) diff --git a/pooltool/objects/__init__.py b/pooltool/objects/__init__.py index 5fcff7f0..8526ddae 100644 --- a/pooltool/objects/__init__.py +++ b/pooltool/objects/__init__.py @@ -15,7 +15,7 @@ ) from pooltool.objects.ball.params import BallParams, PrebuiltBallParams from pooltool.objects.ball.sets import BallSet, get_ballset, get_ballset_names -from pooltool.objects.cue.datatypes import Cue, CueSpecs +from pooltool.objects.cue.datatypes import Cue, CueSpecs, PrebuiltCueSpecs from pooltool.objects.table.collection import TableName from pooltool.objects.table.components import ( CircularCushionSegment, @@ -42,6 +42,7 @@ "BallHistory", "BallOrientation", "CueSpecs", + "PrebuiltCueSpecs", "Cue", "Pocket", "LinearCushionSegment", diff --git a/pooltool/objects/cue/datatypes.py b/pooltool/objects/cue/datatypes.py index 70ebe079..9fba903b 100755 --- a/pooltool/objects/cue/datatypes.py +++ b/pooltool/objects/cue/datatypes.py @@ -2,11 +2,10 @@ from __future__ import annotations -from collections.abc import Callable - from attrs import define, evolve, field, fields_dict from pooltool.game.datatypes import GameType +from pooltool.utils.strenum import StrEnum, auto @define(frozen=True) @@ -42,40 +41,91 @@ class CueSpecs: shaft_radius_at_butt: float = field() end_mass: float = field() - @staticmethod - def default() -> CueSpecs: - """Construct a default cue spec""" - return CueSpecs( - 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, # pool ball mass over 30 - ) - - @staticmethod - def snooker() -> CueSpecs: - return CueSpecs( - brand="Pooltool", - M=0.478, - length=1.475, - tip_radius=0.0106045, - shaft_radius_at_tip=0.0049, - shaft_radius_at_butt=0.0124, - end_mass=0.140 / 30, # snooker ball mass over 30 - ) - - -default_cuespecs_map: dict[GameType, Callable[[], CueSpecs]] = { - GameType.EIGHTBALL: CueSpecs.default, - GameType.NINEBALL: CueSpecs.default, - GameType.THREECUSHION: CueSpecs.default, - GameType.SNOOKER: CueSpecs.snooker, - GameType.SUMTOTHREE: CueSpecs.default, + @classmethod + def default(cls, game_type: GameType = GameType.EIGHTBALL) -> CueSpecs: + """Return prebuilt cue specs based on game type. + + Args: + game_type: + What type of game is being played? + + Returns: + The prebuilt cue specs associated with the passed game type. + """ + return _get_default_cue_specs(game_type) + + @classmethod + def prebuilt(cls, name: PrebuiltCueSpecs) -> CueSpecs: + """Return prebuilt cue specs based on name. + + Args: + name: + A :class:`PrebuiltCueSpecs` member. + """ + return _prebuilt_cue_specs(name) + + +class PrebuiltCueSpecs(StrEnum): + """An Enum specifying prebuilt cue specs. + + Attributes: + POOL_GENERIC: + SNOOKER_GENERIC: + BILLIARD_GENERIC: + """ + + POOL_GENERIC = auto() + SNOOKER_GENERIC = auto() + BILLIARD_GENERIC = auto() + + +CUE_SPECS: dict[PrebuiltCueSpecs, CueSpecs] = { + PrebuiltCueSpecs.POOL_GENERIC: CueSpecs( + 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, + ), + PrebuiltCueSpecs.SNOOKER_GENERIC: CueSpecs( + brand="Pooltool", + M=0.478, + length=1.475, + tip_radius=0.0106045, + shaft_radius_at_tip=0.0049, + shaft_radius_at_butt=0.0124, + end_mass=0.140 / 30, + ), + # TODO: These are just copied from the pool cue specs + PrebuiltCueSpecs.BILLIARD_GENERIC: CueSpecs( + 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.210 / 30, + ), } +_default_map: dict[GameType, PrebuiltCueSpecs] = { + GameType.EIGHTBALL: PrebuiltCueSpecs.POOL_GENERIC, + GameType.NINEBALL: PrebuiltCueSpecs.POOL_GENERIC, + GameType.THREECUSHION: PrebuiltCueSpecs.BILLIARD_GENERIC, + GameType.SNOOKER: PrebuiltCueSpecs.SNOOKER_GENERIC, + GameType.SUMTOTHREE: PrebuiltCueSpecs.BILLIARD_GENERIC, +} + + +def _get_default_cue_specs(game_type: GameType) -> CueSpecs: + return _prebuilt_cue_specs(_default_map[game_type]) + + +def _prebuilt_cue_specs(name: PrebuiltCueSpecs) -> CueSpecs: + return CUE_SPECS[name] + @define class Cue: @@ -208,7 +258,7 @@ def set_state( @classmethod def from_game_type(cls, game_type: GameType, id: str | None = None) -> Cue: - if game_type not in default_cuespecs_map: + if game_type not in _default_map: raise NotImplementedError( f"There is no cue stick associated with '{game_type}'" ) @@ -218,7 +268,7 @@ def from_game_type(cls, game_type: GameType, id: str | None = None) -> Cue: assert id is not None cue = cls(id=id) - cue.specs = default_cuespecs_map[game_type]() + cue.specs = CueSpecs.default(game_type) return cue @classmethod diff --git a/tests/objects/cue/test_datatypes.py b/tests/objects/cue/test_datatypes.py index 163f13b5..28b0cfea 100644 --- a/tests/objects/cue/test_datatypes.py +++ b/tests/objects/cue/test_datatypes.py @@ -2,7 +2,7 @@ from attrs.exceptions import FrozenInstanceError from pooltool.game.datatypes import GameType -from pooltool.objects.cue.datatypes import Cue, CueSpecs +from pooltool.objects.cue.datatypes import Cue, CueSpecs, PrebuiltCueSpecs def test_cue_copy(): @@ -28,14 +28,17 @@ def test_cue_specs_construction(): with pytest.raises(TypeError): CueSpecs() # type: ignore - # All default methods construct properly + # All prebuilt/default methods construct properly CueSpecs.default() - CueSpecs.snooker() + CueSpecs.default(GameType.SNOOKER) + CueSpecs.prebuilt(PrebuiltCueSpecs.POOL_GENERIC) def test_cue_from_game_type(): assert Cue.from_game_type(GameType.EIGHTBALL).specs == CueSpecs.default() - assert Cue.from_game_type(GameType.SNOOKER).specs == CueSpecs.snooker() + assert Cue.from_game_type(GameType.SNOOKER).specs == CueSpecs.default( + GameType.SNOOKER + ) # ID is passed through assert Cue.from_game_type(GameType.SNOOKER).id == "cue_stick" From cccb0aeb77d4b7eae08bfe01e022e4a08fe3af69 Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Sun, 5 Apr 2026 12:58:39 -0700 Subject: [PATCH 5/5] Cue specs match game type selected in-game --- pooltool/ani/animate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pooltool/ani/animate.py b/pooltool/ani/animate.py index c64b7297..dd973291 100755 --- a/pooltool/ani/animate.py +++ b/pooltool/ani/animate.py @@ -522,7 +522,8 @@ def _create_system(self): ballset=None, spacing_factor=1e-3, ) - cue = Cue(cue_ball_id=game.shot_constraints.cueball(balls)) + cue = Cue.from_game_type(game_type) + cue.cue_ball_id = game.shot_constraints.cueball(balls) shot = System(table=table, balls=balls, cue=cue) self.attach_system(shot)