From 3808c527c35443083ee371ef29741666595b7e65 Mon Sep 17 00:00:00 2001 From: Sebastiano Giordano <46520354+Krahos@users.noreply.github.com> Date: Thu, 19 Jun 2025 15:20:14 +0200 Subject: [PATCH 1/7] chore: updating flake --- flake.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/flake.lock b/flake.lock index 5e7acbe..22b7c0b 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1739866667, - "narHash": "sha256-EO1ygNKZlsAC9avfcwHkKGMsmipUk1Uc0TbrEZpkn64=", + "lastModified": 1750134718, + "narHash": "sha256-v263g4GbxXv87hMXMCpjkIxd/viIF7p3JpJrwgKdNiI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "73cf49b8ad837ade2de76f87eb53fc85ed5d4680", + "rev": "9e83b64f727c88a7711a2c463a7b16eedb69a84c", "type": "github" }, "original": { @@ -36,11 +36,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1736320768, - "narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=", + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4bc9c909d9ac828a039f288cf872d16d38185db8", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", "type": "github" }, "original": { @@ -62,11 +62,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1740191166, - "narHash": "sha256-WqRxO1Afx8jPYG4CKwkvDFWFvDLCwCd4mxb97hFGYPg=", + "lastModified": 1750300711, + "narHash": "sha256-4XHPocwP+66PhxyyObPXfI+Rql4PoGe/xBK791N8I78=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "74a3fb71b0cc67376ab9e7c31abcd68c813fc226", + "rev": "4178888556c15e0a1c57850d2f103ac300a6e9e2", "type": "github" }, "original": { From b0bf385209acfdad470c794828a9eb0bca5ea9ed Mon Sep 17 00:00:00 2001 From: Sebastiano Giordano <46520354+Krahos@users.noreply.github.com> Date: Sun, 14 Dec 2025 15:24:47 +0100 Subject: [PATCH 2/7] refactor!: heavy refactoring --- Cargo.lock | 389 ++++++++++---- Cargo.toml | 20 +- README.md | 30 +- flake.lock | 12 +- flake.nix | 22 +- proptest-regressions/core/deck.txt | 12 + proptest-regressions/tressette/game.txt | 7 + src/common/cards.rs | 372 -------------- src/common/hands.rs | 651 ----------------------- src/common/mod.rs | 4 - src/core.rs | 78 +++ src/core/deck.rs | 516 +++++++++++++++++++ src/core/french.rs | 129 +++++ src/core/italian.rs | 106 ++++ src/lib.rs | 23 +- src/tressette.rs | 379 +------------- src/tressette/card.rs | 197 +++++++ src/tressette/game.rs | 653 ++++++++++++++++++++++++ src/tressette/rules.rs | 452 ++++++++++++++++ src/trick_taking.rs | 64 +++ src/trick_taking/hand.rs | 230 +++++++++ src/trick_taking/player.rs | 230 +++++++++ src/trick_taking/trick.rs | 404 +++++++++++++++ tests/tressette.rs | 96 ++-- 24 files changed, 3518 insertions(+), 1558 deletions(-) create mode 100644 proptest-regressions/core/deck.txt create mode 100644 proptest-regressions/tressette/game.txt delete mode 100644 src/common/cards.rs delete mode 100644 src/common/hands.rs delete mode 100644 src/common/mod.rs create mode 100644 src/core.rs create mode 100644 src/core/deck.rs create mode 100644 src/core/french.rs create mode 100644 src/core/italian.rs create mode 100644 src/tressette/card.rs create mode 100644 src/tressette/game.rs create mode 100644 src/tressette/rules.rs create mode 100644 src/trick_taking.rs create mode 100644 src/trick_taking/hand.rs create mode 100644 src/trick_taking/player.rs create mode 100644 src/trick_taking/trick.rs diff --git a/Cargo.lock b/Cargo.lock index fefa68c..a0be75c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "array-init" @@ -16,58 +16,121 @@ checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" [[package]] name = "autocfg" -version = "1.3.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bit-set" -version = "0.5.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" -version = "0.6.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] -name = "byteorder" -version = "1.5.0" +name = "bon" +version = "3.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61138465baf186c63e8d9b6b613b508cd832cba4ce93cf37ce5f096f91ac1a6" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +checksum = "40d1dad34aa19bf02295382f08d9bc40651585bd497266831d40ee6296fb49ca" +dependencies = [ + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn", +] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "convert_case" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] [[package]] name = "errno" -version = "0.3.9" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] name = "fastrand" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fnv" @@ -77,20 +140,48 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "getrandom" -version = "0.2.15" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", + "r-efi", "wasi", ] [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "kinded" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "ce4bdbb2f423660b19f0e9f7115182214732d8dd5f840cd0a3aee3e22562f34c" +dependencies = [ + "kinded_macros", +] + +[[package]] +name = "kinded_macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13b4ddc5dcb32f45dac3d6f606da2a52fdb9964a18427e63cd5ef6c0d13288d" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn", +] [[package]] name = "lazy_static" @@ -100,21 +191,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" - -[[package]] -name = "libm" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "num-bigint" @@ -153,38 +238,71 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", +] + +[[package]] +name = "nutype" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3340cb6773b0794ecb3f62ff66631d580f57151d9415c10ee8a27a357aeb998b" +dependencies = [ + "nutype_macros", +] + +[[package]] +name = "nutype_macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c955e27d02868fe90b9c2dc901661fd7ed67ec382782bdc67c6aa8d2e957a9" +dependencies = [ + "cfg-if", + "kinded", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "urlencoding", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" dependencies = [ "bit-set", "bit-vec", @@ -208,29 +326,34 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.36" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" -version = "0.8.5" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ - "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" -version = "0.3.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", @@ -238,46 +361,55 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.4" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom", ] [[package]] name = "rand_xorshift" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ "rand_core", ] [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] [[package]] name = "rustix" -version = "0.38.34" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "rusty-fork" @@ -291,32 +423,77 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "shuftlib" version = "0.1.2" dependencies = [ "anyhow", "array-init", + "bon", "num-rational", + "nutype", "proptest", "rand", + "serde", "strum", + "thiserror", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" -version = "0.25.0" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.25.3" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" dependencies = [ "heck", "proc-macro2", @@ -327,9 +504,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.72" +version = "2.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" dependencies = [ "proc-macro2", "quote", @@ -338,15 +515,35 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.12.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ - "cfg-if", "fastrand", + "getrandom", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -357,32 +554,38 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] name = "wait-timeout" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ - "windows-targets", + "wit-bindgen-rt", ] [[package]] @@ -458,21 +661,29 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 3846bc7..636c230 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "shuftlib" version = "0.1.2" -edition = "2021" +edition = "2024" authors = ["Sebastiano Giordano"] categories = ["game-development"] description = "A generic library for card games and related topics" @@ -15,14 +15,22 @@ repository = "https://github.com/shuftle/shuftlib" path = "src/lib.rs" [dependencies] -anyhow = "1.0.75" +anyhow = "1.0.98" array-init = "2.1.0" -num-rational = "0.4.1" -rand="0.8" -strum = {version="0.25", default-features=false, features=["derive"]} +bon = "3.6.4" +num-rational = "0.4.2" +nutype = "0.6.1" +rand="0.9" +serde = { version = "1.0", optional = true } +strum = {version="0.27", default-features=false, features=["derive"]} +thiserror = "2.0" + +[features] +default = [] +serde = ["dep:serde"] [dev-dependencies] -proptest="1.4" +proptest="1.7" [profile.test.package.proptest] opt-level = 3 diff --git a/README.md b/README.md index da42c8f..0950454 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,29 @@ # Shuftlib -Shuftlib is a general purpose card games library. It aims to contain reusable -types and behaviour for any card game, any deck type. +Shuftlib is a Rust library for implementing card games. It provides reusable components for deck management, shuffling, and game mechanics. -## Status +## Usage -For now I'm just playing around with the library. Bugs and breaking changes are -not only possible, but expected. +```rust +use shuftlib::tressette::Game; + +let mut game = Game::new(); + +while !matches!(game.status(), shuftlib::tressette::Status::Finished { .. }) { + let legal_cards = game.legal_cards(); + let chosen_card = legal_cards[0]; + game.play_card(chosen_card).expect("Legal move"); +} + +if let shuftlib::tressette::Status::Finished { winner } = game.status() { + println!("Winner: {:?}", winner); +} +``` + +## Documentation + +Documentation is at [docs.rs/shuftlib](https://docs.rs/shuftlib). + +## License + +Licensed under either the MIT License or the Apache License 2.0. diff --git a/flake.lock b/flake.lock index 22b7c0b..5985718 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1750134718, - "narHash": "sha256-v263g4GbxXv87hMXMCpjkIxd/viIF7p3JpJrwgKdNiI=", + "lastModified": 1765186076, + "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9e83b64f727c88a7711a2c463a7b16eedb69a84c", + "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8", "type": "github" }, "original": { @@ -62,11 +62,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1750300711, - "narHash": "sha256-4XHPocwP+66PhxyyObPXfI+Rql4PoGe/xBK791N8I78=", + "lastModified": 1765507345, + "narHash": "sha256-fq34mBLvAgv93EuZjGp7cVV633pxnph9AVuB/Ql5y5Q=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "4178888556c15e0a1c57850d2f103ac300a6e9e2", + "rev": "a9471b23bf656d69ceb2d5ddccdc5082d51fc0e3", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 9fdc552..603a4be 100644 --- a/flake.nix +++ b/flake.nix @@ -2,13 +2,20 @@ description = "Rust devshell for Shuftle"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; rust-overlay.url = "github:oxalica/rust-overlay"; - flake-utils.url = "github:numtide/flake-utils"; + flake-utils.url = "github:numtide/flake-utils"; }; - outputs = { nixpkgs, rust-overlay, flake-utils, ... }: - flake-utils.lib.eachDefaultSystem (system: + outputs = + { + nixpkgs, + rust-overlay, + flake-utils, + ... + }: + flake-utils.lib.eachDefaultSystem ( + system: let overlays = [ (import rust-overlay) ]; pkgs = import nixpkgs { @@ -19,11 +26,12 @@ { devShells.default = mkShell { buildInputs = [ - cargo-watch - cargo-udeps + bacon + cargo-machete cargo-deny + cargo-edit rust-analyzer - rust-bin.nightly.latest.default + rust-bin.stable.latest.default ]; }; } diff --git a/proptest-regressions/core/deck.txt b/proptest-regressions/core/deck.txt new file mode 100644 index 0000000..efcdb24 --- /dev/null +++ b/proptest-regressions/core/deck.txt @@ -0,0 +1,12 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 504d5514fdaf5842380fd98544b9e79acb7ba0dbb34837faa7a5d2c54a0f1e1f # shrinks to card = (Ace, Hearts), mut deck = Deck { cards: [] } +cc ed53cdee34dc5ffaaa5301eb8df346430662b237c8102337c0b3ac2958301e76 # shrinks to mut deck = Deck { cards: [ItalianCard { rank: Ace, suit: Hearts }, ItalianCard { rank: Ace, suit: Hearts }] } +cc a0bea90e964a3f59c109c2b0f3812f3cf577b1d3bedf918af96466d63ad6c4c8 # shrinks to card = (Seven, Hearts), mut deck = Deck { cards: [ItalianCard { rank: King, suit: Diamonds }, ItalianCard { rank: Two, suit: Clubs }, ItalianCard { rank: Three, suit: Diamonds }, ItalianCard { rank: Ace, suit: Clubs }, ItalianCard { rank: Seven, suit: Diamonds }] } +cc 40fd6a3829946fd2211407dbd5a44ad7c3ca7e4471dbcae106a4990fed46ab1f # shrinks to card = (Ace, Hearts) +cc 7f14002fa9acd7850104a1552f96c9205f6a351be7ceca964577ecb2210da768 # shrinks to card = (Three, Clubs), mut deck = Deck { cards: [ItalianCard { rank: Two, suit: Clubs }, ItalianCard { rank: Three, suit: Diamonds }, ItalianCard { rank: Five, suit: Spades }, ItalianCard { rank: Six, suit: Clubs }, ItalianCard { rank: Three, suit: Clubs }] } +cc be4cff658b83cbaacfba7c0505c9644c0dcf57e023db627ff060b5958a2b13ea # shrinks to card = (Ace, Hearts), mut deck = Deck { cards: [ItalianCard { rank: Ace, suit: Hearts }, ItalianCard { rank: King, suit: Diamonds }] } diff --git a/proptest-regressions/tressette/game.txt b/proptest-regressions/tressette/game.txt new file mode 100644 index 0000000..58197de --- /dev/null +++ b/proptest-regressions/tressette/game.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc a5ec1862c112ea6bee9299981886b00188439613c7dd3e8aae95ba382be2cf94 # shrinks to num_cards = 40 diff --git a/src/common/cards.rs b/src/common/cards.rs deleted file mode 100644 index 8c4bd44..0000000 --- a/src/common/cards.rs +++ /dev/null @@ -1,372 +0,0 @@ -use std::{ - fmt::{Debug, Display}, - ops::{Deref, DerefMut}, -}; - -use rand::Rng; -use strum::{EnumIter, FromRepr, IntoEnumIterator}; - -/// A trait representing a card. The actual implementation depends on the game where this is used. -pub trait Card: Display + Default + Sized + Debug + Copy + Eq + PartialEq {} - -/// Representation of a card that goes into an Italian deck. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ItalianCard { - rank: ItalianRank, - suit: Suit, -} - -impl ItalianCard { - /// Generates a card with the given rank and suit - pub fn new(rank: ItalianRank, suit: Suit) -> Self { - Self { rank, suit } - } - - /// The rank of the card. - pub fn rank(&self) -> ItalianRank { - self.rank - } - - /// The suit of the card. - pub fn suit(&self) -> Suit { - self.suit - } -} - -impl Default for ItalianCard { - fn default() -> Self { - ItalianCard { - rank: ItalianRank::Ace, - suit: Suit::Clubs, - } - } -} - -impl Display for ItalianCard { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}{}", self.rank as u8, self.suit) - } -} - -impl Card for ItalianCard {} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -/// Representation of a card that goes into an French deck. -pub struct FrenchCard { - rank: FrenchRank, - suit: Suit, -} - -impl FrenchCard { - /// Generates a card with the given rank and suit - pub fn new(rank: FrenchRank, suit: Suit) -> Self { - Self { rank, suit } - } - - /// The rank of the card. - pub fn rank(&self) -> FrenchRank { - self.rank - } - - /// The suit of the card. - pub fn suit(&self) -> Suit { - self.suit - } -} - -impl Default for FrenchCard { - fn default() -> Self { - FrenchCard { - rank: FrenchRank::Ace, - suit: Suit::Hearts, - } - } -} - -impl Display for FrenchCard { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}{}", self.rank as u8, self.suit) - } -} - -impl Card for FrenchCard {} - -/// A Joker card, present in some card games. Its function depends on the game. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub struct Joker; - -impl Card for Joker {} - -impl Display for Joker { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "JK") - } -} - -/// A variant of the French card, which can either be an actual French card or a joker. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FrenchWithJoker { - /// A standard French card. - Normal(FrenchCard), - /// A Joker card. - Joker(Joker), -} -impl Card for FrenchWithJoker {} - -impl Default for FrenchWithJoker { - fn default() -> Self { - Self::Normal(FrenchCard { - rank: FrenchRank::Ace, - suit: Suit::Hearts, - }) - } -} - -impl Display for FrenchWithJoker { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - FrenchWithJoker::Normal(c) => write!(f, "{}", c), - FrenchWithJoker::Joker(c) => write!(f, "{}", c), - } - } -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq, EnumIter, FromRepr, Hash)] -#[repr(u8)] -/// The rank of the card. In an Italian deck, ranks go from the ace to the 7, then they also have a jack, knight and king, -/// In most games they each have a different value that depends on the game itself. -pub enum ItalianRank { - /// 1 - Ace = 1, - /// 2 - Two, - /// 3 - Three, - /// 4 - Four, - /// 5 - Five, - /// 6 - Six, - /// 7 - Seven, - /// 8 - Jack, - /// 9 - Knight, - /// 10 - King, -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq, EnumIter, FromRepr, Hash)] -#[repr(u8)] -/// The rank of the card. In a French deck, ranks go from the ace to 10, then there is a jack, queen and king, -/// In most games they each have a different value that depends on the game itself. -pub enum FrenchRank { - /// 1 - Ace = 1, - /// 2 - Two, - /// 3 - Three, - /// 4 - Four, - /// 5 - Five, - /// 6 - Six, - /// 7 - Seven, - /// 8 - Eight, - /// 9 - Nine, - /// 10 - Ten, - /// 11 - Jack, - /// 12 - Queen, - /// 13 - King, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, Hash)] -/// The 4 suits of a standard deck. They have an equivalent in pretty much all regional decks. -/// In some games they have a hierarchical order. -pub enum Suit { - /// Hearts (French, German), Cups (Latin). - Hearts, - /// Diamonds or Tiles (French), Bells (German), Coins (Latin). - Diamonds, - /// Clubs or Clover (French), Acorns (German), Clubs or Batons (Latin). - Clubs, - /// Spades or Pikes (French), Leaves (German), Swords (Latin). - Spades, -} - -impl Display for Suit { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = match self { - Suit::Hearts => "H", - Suit::Diamonds => "D", - Suit::Clubs => "C", - Suit::Spades => "S", - }; - write!(f, "{}", s) - } -} - -#[derive(Default)] -/// Represents a deck of cards. Cards can be added or removed at will. -pub struct Deck -where - T: Card, -{ - cards: Vec, -} - -const FRENCH_CARDS: usize = 52; -const ITALIAN_CARDS: usize = 40; - -impl Deck { - /// Creates a new deck in the Italian format. - pub fn italian() -> Deck { - let mut cards = Vec::with_capacity(ITALIAN_CARDS); - for suit in Suit::iter() { - for rank in ItalianRank::iter() { - cards.push(ItalianCard { rank, suit }); - } - } - - Deck { cards } - } -} - -impl Deck { - /// Creates a new 52 cards French deck. - pub fn french() -> Deck { - let mut cards = Vec::with_capacity(FRENCH_CARDS); - for suit in Suit::iter() { - for rank in FrenchRank::iter() { - cards.push(FrenchCard { suit, rank }); - } - } - - Deck { cards } - } - - /// Creates a new 52 cards French deck, with the addition of the specified amount of jokers. - pub fn french_with_jokers(jokers: u8) -> Deck { - let mut cards = Vec::with_capacity(FRENCH_CARDS + jokers as usize); - for suit in Suit::iter() { - for rank in FrenchRank::iter() { - cards.push(FrenchWithJoker::Normal(FrenchCard { suit, rank })); - } - } - - for _ in 0..jokers { - cards.push(FrenchWithJoker::Joker(Joker {})); - } - - Deck { cards } - } -} - -impl Deck { - /// Performs a random permutation on the deck with the Fisher–Yates shuffle algorithm, repeated 10 times. - pub fn shuffle(&mut self) { - let mut rng = rand::thread_rng(); - let max = self.cards.len(); - for _ in 0..10 { - for i in 0..max - 2 { - let j = rng.gen_range(i..max); - self.cards.swap(i, j); - } - } - } - - /// Adds a card in a random position inside the deck. - pub fn shuffle_card(&mut self, card: T) { - let mut rng = rand::thread_rng(); - let max = self.cards.len(); - let position = rng.gen_range(1..max); - self.cards.insert(position, card); - } - - /// Adds a card to the top of the deck. - pub fn push(&mut self, card: T) { - self.cards.push(card); - } - - /// Draws the top-most card in the deck. It returns None if there are no cards left. - pub fn draw(&mut self) -> Option { - self.cards.pop() - } - - /// Creates a new empty deck. - pub fn new() -> Deck { - Deck { cards: Vec::new() } - } - - /// Creates a new empty deck with specified capacity. - pub fn with_capacity(capacity: usize) -> Deck { - Deck { - cards: Vec::with_capacity(capacity), - } - } - - /// Creates a new deck from a given vec of cards. - pub fn from_vec(cards: Vec) -> Deck { - Deck { cards } - } - - /// Returns the number of cards left in the deck. - pub fn len(&self) -> usize { - self.cards.len() - } - - /// Returns whether or not the deck is empty. - pub fn is_empty(&self) -> bool { - self.cards.is_empty() - } -} - -impl Deref for Deck -where - T: Card, -{ - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.cards - } -} - -impl DerefMut for Deck -where - T: Card, -{ - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.cards - } -} - -#[cfg(test)] -mod tests { - use crate::common::cards::Deck; - - #[test] - fn should_shuffle() { - let sorted_deck = Deck::italian(); - - let mut shuffled_deck = Deck::italian(); - shuffled_deck.shuffle(); - - let decks = sorted_deck.cards.iter().zip(shuffled_deck.cards.iter()); - - let count_of_different_cards = decks.filter(|&(&card1, &card2)| card1 != card2).count(); - - assert_ne!(count_of_different_cards, 0); - } -} diff --git a/src/common/hands.rs b/src/common/hands.rs deleted file mode 100644 index e34ffdb..0000000 --- a/src/common/hands.rs +++ /dev/null @@ -1,651 +0,0 @@ -use std::{fmt::Display, ops::Deref}; - -use anyhow::bail; - -use super::cards::Card; - -/// Many of the types contained in this module are generic over certain -/// constants related to the game. This trait is the summary of these -/// constraints. -pub trait TrickTakingGame { - /// Define the type of card that's going to be used in this game. - type CardType: Card; - /// Every game has a fixed number of players defined by the rules of the - /// game or, anyway, before starting it. - const PLAYERS: usize; - /// Usually trick taking games have a fixed number of "turns" for each - /// player. These "turns" are called tricks - const TRICKS: usize; - - /// Every trick taking game has some logic to determine the winner (or - /// taker) of the trick. The taker is generally determined by the cards that - /// have been played and it can depend by the order in which the players - /// played their cards. - fn determine_taker( - cards: &[Self::CardType; Self::PLAYERS], - first_to_play: PlayerId<{ Self::PLAYERS }>, - ) -> PlayerId<{ Self::PLAYERS }>; -} - -/// Represents a player of a game. This type is generic over the type of the -/// card used for the specific game and over the number of players of such game. -#[derive(Clone, Default, Debug)] -pub struct Player -where - G: TrickTakingGame, - [(); G::PLAYERS]:, -{ - /// The cards traditionally held in the hand by the player. - hand: Vec, - /// The ID of this player. This is used to determine their turn to play. - id: PlayerId<{ G::PLAYERS }>, -} - -impl Player -where - G: TrickTakingGame, - [(); G::PLAYERS]:, -{ - /// Adds a card to the hand of the player. - /// - /// # Examples - /// ``` - /// #![feature(generic_const_exprs)] - /// use shuftlib::common::hands::{Player, TrickTakingGame, PlayerId}; - /// use shuftlib::common::cards::{ItalianRank, Suit}; - /// use shuftlib::tressette::{TressetteRules, TressetteCard}; - /// - /// let player_id = PlayerId::<{TressetteRules::PLAYERS}>::new(0).unwrap(); - /// let mut player = Player::::new(player_id); - /// // Players have no cards when created. - /// assert_eq!(player.hand().len(), 0); - /// - /// let card = TressetteCard::new(ItalianRank::Ace, Suit::Spades); - /// player.give(card); - /// assert_eq!(player.hand().len(), 1); - /// ``` - pub fn give(&mut self, card: G::CardType) { - self.hand.push(card); - } - - /// Removes a card from the hand of the player. - /// - /// # Examples - /// ``` - /// #![feature(generic_const_exprs)] - /// use shuftlib::common::hands::{Player, TrickTakingGame, PlayerId}; - /// use shuftlib::common::cards::{ItalianRank, Suit}; - /// use shuftlib::tressette::{TressetteRules, TressetteCard}; - /// - /// let player_id = PlayerId::<{TressetteRules::PLAYERS}>::new(0).unwrap(); - /// let mut player = Player::::new(player_id); - /// // Players have no cards when created. - /// assert_eq!(player.hand().len(), 0); - /// - /// let card = TressetteCard::new(ItalianRank::Ace, Suit::Spades); - /// player.give(card); - /// assert_eq!(player.hand().len(), 1); - /// - /// player.remove(card); - /// assert_eq!(player.hand().len(), 0); - /// ``` - pub fn remove(&mut self, card: G::CardType) { - self.hand.retain(|&c| c != card); - } - - /// Getter for the cards held by this player. - pub fn hand(&self) -> &[G::CardType] { - &self.hand - } - - /// Getter for the id of this player. - pub fn id(&self) -> PlayerId<{ G::PLAYERS }> { - self.id - } - - /// Generates a new player from a `PlayerId`. Players are initialized with - /// no cards. - /// - /// # Examples. - /// - /// ``` - /// #![feature(generic_const_exprs)] - /// use shuftlib::{common::hands::{Player, PlayerId, TrickTakingGame}, tressette::TressetteRules}; - /// - /// let id = PlayerId::<{TressetteRules::PLAYERS}>::new(0).unwrap(); - /// let player = Player::::new(id); - /// - /// assert_eq!(*player.id(), 0); - /// assert_eq!(player.hand().len(), 0); - /// ```` - pub fn new(id: PlayerId<{ G::PLAYERS }>) -> Self { - Self { - id, - hand: Vec::new(), - } - } -} - -/// A player id can only be in the range 0..N, where N depends on the game being -/// played and it's the number of players playing that specific game. -#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] -pub struct PlayerId(usize); - -impl PlayerId { - /// This method simply increments `self` by 1. Note that `PlayerId` can only - /// be in the range 0..N, so incrementing `self` when the value is N-1, will - /// reset its value to 0, since the purpose of this type is to determine the - /// player's turn and the first person to play is not necessarily the person - /// with ID = 0. - /// - /// # Examples - /// ``` - /// #![feature(generic_const_exprs)] - /// use shuftlib::common::hands::PlayerId; - /// - /// let mut player_id: PlayerId<4>= PlayerId::new(0).unwrap(); - /// player_id.inc(); - /// assert_eq!(player_id, PlayerId::<4>::new(1).unwrap()); - /// player_id.inc(); - /// player_id.inc(); - /// player_id.inc(); - /// assert_eq!(player_id, PlayerId::<4>::new(0).unwrap()); - /// ``` - pub fn inc(&mut self) { - if self.0 < PLAYERS - 1 { - self.0 += 1; - } else { - self.0 = 0; - } - } - - /// Creates a value of type `PlayerId`. Returns None if value is >= N, - /// otherwise returns Some(PlayerId(value)). - /// - /// # Examples - /// - /// ``` - /// #![feature(generic_const_exprs)] - /// use shuftlib::common::hands::PlayerId; - /// - /// let id = PlayerId::<4>::new(0); - /// assert!(id.is_some()); - /// - /// let id = PlayerId::<4>::new(4); - /// assert!(id.is_none()); - /// ``` - pub fn new(value: usize) -> Option { - if value < PLAYERS { - Some(PlayerId(value)) - } else { - None - } - } -} - -impl TryFrom for PlayerId { - type Error = anyhow::Error; - - fn try_from(value: usize) -> Result { - if (0..PLAYERS).contains(&value) { - Ok(PlayerId(value)) - } else { - bail!( - "Tried to convert {} into a PlayerId, but acceptable values are in range 0..PLAYERS", - value - ) - } - } -} - -impl Display for PlayerId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Deref for PlayerId { - type Target = usize; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// A trick is a set containing the cards played and the player who won the -/// trick, represented as `PlayerId`. -#[derive(Debug, Copy, Clone)] -pub struct Trick -where - G: TrickTakingGame, - [(); G::PLAYERS]:, -{ - cards: [G::CardType; G::PLAYERS], - taker: PlayerId<{ G::PLAYERS }>, -} - -impl Display for Trick -where - G: TrickTakingGame, - [(); G::PLAYERS]:, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{} {} {} {} {}", - self.cards[0], self.cards[1], self.cards[2], self.cards[3], self.taker - ) - } -} - -impl Trick -where - G: TrickTakingGame, - [(); G::PLAYERS]:, -{ - /// Returns the card this trick has been won with. - pub fn taken_with(&self) -> G::CardType { - self.cards[self.taker.0] - } - - /// Getter for the `PlayerId` of the player who won the trick. - pub fn taker(&self) -> PlayerId<{ G::PLAYERS }> { - self.taker - } - - /// Getter for the cards played during this trick. - pub fn cards(&self) -> &[G::CardType] { - &self.cards - } -} - -/// A temporary state of a trick that's still not over: not all the players made -/// their move or a taker hasn't been determined yet. -#[derive(Clone, Copy, Debug)] -pub struct OngoingTrick -where - G: TrickTakingGame, - [(); G::PLAYERS]:, -{ - cards: [Option; G::PLAYERS], - first_to_play: PlayerId<{ G::PLAYERS }>, - next_to_play: PlayerId<{ G::PLAYERS }>, - play_count: usize, -} - -impl Deref for OngoingTrick -where - G: TrickTakingGame, - [(); G::PLAYERS]:, -{ - type Target = [Option; G::PLAYERS]; - - fn deref(&self) -> &Self::Target { - &self.cards - } -} - -impl OngoingTrick -where - G: TrickTakingGame, - [(); G::PLAYERS]:, -{ - /// Adds the `Card` passed as parameter to the `OngoingTrick`. - /// Checking the validity of the card played is a responsability of the - /// caller. - /// - /// # Examples - /// ``` - /// #![feature(generic_const_exprs)] - /// use shuftlib::common::{hands::{OngoingTrick, PlayerId, TrickTakingGame}, cards::{Card, ItalianRank, Suit}}; - /// use shuftlib::tressette::{TressetteRules, TressetteCard}; - /// - /// let first_to_play = PlayerId::<{TressetteRules::PLAYERS}>::new(0).unwrap(); - /// let card = TressetteCard::new(ItalianRank::Ace, Suit::Hearts); - /// let mut trick = OngoingTrick::::new(first_to_play); - /// trick.play(card); - /// let mut second_to_play = first_to_play; - /// second_to_play.inc(); - /// - /// assert_eq!(trick[0], Some(card)); - /// assert_eq!(trick.next_to_play(), second_to_play) - /// ``` - pub fn play(&mut self, card: G::CardType) { - self.cards[self.next_to_play.0] = Some(card); - self.next_to_play.inc(); - self.play_count += 1; - } - - /// Tries to transform the current `OngoingTrick` into a `Trick` by - /// determining the taker of the trick. It doesn't make any assumption on - /// previously played cards during the current `OngoingHand`. It also does - /// not check if it contains duplicates since that could be valid in some games. - /// - /// # Errors - /// - /// Fails if any of the moves of the `OngoingTrick` this is called on is - /// None. It means that not all players made their move yet, so a taker - /// can't be determined. - /// - /// # Examples - /// - /// ``` - /// #![feature(generic_const_exprs)] - /// use shuftlib::common::{hands::{OngoingTrick, PlayerId, TrickTakingGame}, cards::{ItalianRank, Suit}}; - /// use shuftlib::tressette::{TressetteRules, TressetteCard}; - /// - /// let cards = [ - /// TressetteCard::new(ItalianRank::Ace, Suit::Hearts), - /// TressetteCard::new(ItalianRank::Two, Suit::Hearts), - /// TressetteCard::new(ItalianRank::Three, Suit::Hearts), - /// TressetteCard::new(ItalianRank::Four, Suit::Hearts), - /// ]; - /// let first_to_play = PlayerId::<{TressetteRules::PLAYERS}>::new(0).unwrap(); - /// let mut ongoing_trick = OngoingTrick::::new(first_to_play); - /// ongoing_trick.play(cards[0]); - /// - /// // After only playing a card, it's not possible to finish the OngoingTrick. - /// assert!(ongoing_trick.clone().finish().is_none()); - /// - /// let mut to_play = first_to_play; - /// to_play.inc(); - /// cards.iter().skip(1).for_each(|&c| { - /// ongoing_trick.play(c); - /// to_play.inc(); - /// }); - /// - /// // After every player made their play, it's possible to get the trick. - /// let trick = ongoing_trick.finish().unwrap(); - /// // Finishing the trick also means determining a taker. Since in this - /// // example we are using the tressette game rules, player 2 is the taker. - /// assert_eq!(Some(trick.taker()), PlayerId::<{TressetteRules::PLAYERS}>::new(2)); - /// ``` - pub fn finish(self) -> Option> { - let mut cards: [G::CardType; G::PLAYERS] = [G::CardType::default(); G::PLAYERS]; - if self - .iter() - .enumerate() - .map(|(i, &x)| { - if let Some(c) = x { - cards[i] = c; - true - } else { - false - } - }) - .any(|is_some| !is_some) - { - return None; - } - - let taker = G::determine_taker(&cards, self.first_to_play); - Some(Trick { cards, taker }) - } - - /// Getter for the cards contained in this `OngoingTrick`. - pub fn cards(&self) -> &[Option] { - &self.cards - } - - /// Getter for the id of the person who starts the trick. - pub fn first_to_play(&self) -> PlayerId<{ G::PLAYERS }> { - self.first_to_play - } - - /// Getter for the id of the person who playes last in the trick. - pub fn next_to_play(&self) -> PlayerId<{ G::PLAYERS }> { - self.next_to_play - } - - /// Creates a new `OngoingTrick`, by defining the logic to determine the - /// taker. - /// - /// # Examples. - /// - /// ``` - /// #![feature(generic_const_exprs)] - /// use shuftlib::common::hands::{OngoingTrick, PlayerId, TrickTakingGame}; - /// use shuftlib::tressette::TressetteRules; - /// - /// let first_to_play = PlayerId::<{TressetteRules::PLAYERS}>::new(0).unwrap(); - /// let ongoing_trick = OngoingTrick::::new(first_to_play); - /// - /// assert_eq!(ongoing_trick.first_to_play(), first_to_play); - /// ongoing_trick.cards().iter().for_each(|&c| assert!(c.is_none())); - /// ``` - pub fn new(first_to_play: PlayerId<{ G::PLAYERS }>) -> Self { - let mut last_to_play = first_to_play; - (0..G::PLAYERS - 1).for_each(|_| last_to_play.inc()); - Self { - cards: [None; G::PLAYERS], - first_to_play, - next_to_play: first_to_play, - play_count: 0, - } - } -} - -/// Various games are usually played multiple times, until one team reaches a -/// certain score. These "multiple times" are called hands: "We played a game of -/// tressette and our team won in just 2 hands!". -/// -/// This type is generic over the -/// actual card type, the number of players allowed and the number of tricks it -/// takes to finish the hand. -#[derive(Debug, Clone, Copy)] -pub struct Hand -where - G: TrickTakingGame, - [(); G::PLAYERS]:, - [(); G::TRICKS]:, -{ - tricks: [Trick; G::TRICKS], -} - -impl Hand -where - G: TrickTakingGame, - [(); G::PLAYERS]:, - [(); G::TRICKS]:, -{ - /// Returns a reference to the tricks of this [`Hand`]. - pub fn tricks(&self) -> &[Trick; G::TRICKS] { - &self.tricks - } -} - -/// A hand takes multiple turns for each player to be completed, this is the -/// representation of a `Hand` which hasn't been completed yet. -#[derive(Clone, Copy, Debug)] -pub struct OngoingHand -where - G: TrickTakingGame, - [(); G::PLAYERS]:, - [(); G::TRICKS]:, -{ - current_trick: Option>, - index: usize, - tricks: [Option>; G::TRICKS], -} - -impl OngoingHand -where - G: TrickTakingGame, - [(); G::PLAYERS]:, - [(); G::TRICKS]:, -{ - /// Returns the current trick of this [`OngoingHand`]. - pub fn current_trick(&self) -> &Option> { - &self.current_trick - } - - /// Returns a reference to the tricks of this [`OngoingHand`]. - pub fn tricks(&self) -> &[Option>; G::TRICKS] { - &self.tricks - } - - /// Returns the index of this [`OngoingHand`]. - pub fn index(&self) -> usize { - self.index - } - - /// Transforms an `OngoingHand` into a `Hand`, a read-only data structure - /// used to just story the information related to a hand that has been played. - pub fn finish(self) -> Option> { - if self.tricks.iter().any(|t| t.is_none()) { - return None; - } - - let tricks: [Trick; G::TRICKS] = self - .tricks - .into_iter() - .flatten() - .collect::>() - .try_into() - .ok()?; - Some(Hand { tricks }) - } - - /// Constructor for `OngoingHand`. All the internal fields are initialized - /// as empty or None. - /// - /// # Examples - /// - /// ``` - /// #![feature(generic_const_exprs)] - /// use shuftlib::{common::{hands::OngoingHand}, tressette::TressetteRules}; - /// - /// let ongoing_hand = OngoingHand::::new(); - /// - /// assert_eq!(ongoing_hand.index(), 0); - /// assert!(ongoing_hand.current_trick().is_none()); - /// ongoing_hand.tricks().iter().for_each(|t| assert!(t.is_none())); - /// ``` - pub fn new() -> Self { - let tricks: [Option>; G::TRICKS] = array_init::array_init(|_| None); - - let current_trick = None; - let index = 0; - - Self { - tricks, - current_trick, - index, - } - } - - /// Adds a trick to this hand. - pub fn add(&mut self, trick: Trick, id: usize) { - self.tricks[id] = Some(trick); - } -} - -impl Default for OngoingHand -where - G: TrickTakingGame, - [(); G::PLAYERS]:, - [(); G::TRICKS]:, -{ - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use proptest::collection::hash_set; - use proptest::{array, prelude::*}; - - use crate::common::cards::{ItalianCard, ItalianRank, Suit}; - - use super::{OngoingTrick, PlayerId, TrickTakingGame}; - - /// Strategy to create a random `TressetteCard`. - fn italian_card_strategy() -> impl Strategy { - ( - prop_oneof![ - Just(ItalianRank::Ace), - Just(ItalianRank::Two), - Just(ItalianRank::Three), - Just(ItalianRank::Four), - Just(ItalianRank::Five), - Just(ItalianRank::Six), - Just(ItalianRank::Seven), - Just(ItalianRank::Jack), - Just(ItalianRank::Knight), - Just(ItalianRank::King), - ], - prop_oneof![ - Just(Suit::Hearts), - Just(Suit::Clubs), - Just(Suit::Spades), - Just(Suit::Diamonds), - ], - ) - .prop_map(|(rank, suit)| ItalianCard::new(rank, suit)) - } - - #[derive(Clone, Copy, Debug)] - struct TestGame {} - - impl TrickTakingGame for TestGame { - type CardType = ItalianCard; - - const PLAYERS: usize = 4; - - const TRICKS: usize = 10; - - fn determine_taker( - _cards: &[Self::CardType; Self::PLAYERS], - _first_to_play: super::PlayerId<{ Self::PLAYERS }>, - ) -> super::PlayerId<{ Self::PLAYERS }> { - PlayerId::new(0).unwrap() - } - } - - /// Strategy to create an `OngoingTrick` filled with random cards. Since - /// the `OngoingTrick` already contains the cards, `first_to_play` is - /// irrelevant. Change this function accordingly if you need those to have - /// a specific value. - fn ongoing_trick_strategy() -> impl Strategy> { - hash_set(italian_card_strategy(), TestGame::PLAYERS).prop_map(|hash_set| { - let mut cards = [None; TestGame::PLAYERS]; - hash_set - .iter() - .enumerate() - .for_each(|(i, &c)| cards[i] = Some(c)); - - OngoingTrick { - cards, - first_to_play: PlayerId(0), - next_to_play: PlayerId(0), - play_count: 0, - } - }) - } - - proptest! { - #[test] - fn play_method_works(cards in array::uniform4(italian_card_strategy())) { - let mut trick: OngoingTrick = OngoingTrick::new(PlayerId::new(0).unwrap()); - - for (index, &card) in cards.iter().enumerate() { - // Panicking if there are duplicates in the cards array. - trick.play(card); - // If the card was successfully played, it will be contained - // inside the `OngoingTrick` struct as `Some`. - assert_eq!(trick[index], Some(card)); - } - } - - #[test] - fn finish_method_works(ongoing_trick in ongoing_trick_strategy()) { - let trick = ongoing_trick.finish().unwrap(); - - let cards = ongoing_trick.cards(); - - prop_assert_eq!(trick.taker(), PlayerId::new(0).unwrap()); - prop_assert_eq!(trick.taken_with(), cards[0].unwrap()); - } - } -} diff --git a/src/common/mod.rs b/src/common/mod.rs deleted file mode 100644 index f9bcfd7..0000000 --- a/src/common/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -/// Common cards and decks types. -pub mod cards; -/// Common utility types to define tricks, hands, players. -pub mod hands; diff --git a/src/core.rs b/src/core.rs new file mode 100644 index 0000000..32d41c6 --- /dev/null +++ b/src/core.rs @@ -0,0 +1,78 @@ +//! Core primitives for card games. +//! +//! This module contains the fundamental building blocks for card games: +//! - Generic card types, suits, ranks, and decks (applicable to any card game) +//! - Trick-taking game mechanics (players, tricks, hands) + +use std::fmt::{Debug, Display}; +use strum::EnumIter; + +/// Italian card deck types and ranks. +pub mod italian; + +/// French card deck types, ranks, and Joker. +pub mod french; + +/// Generic deck container and operations. +pub mod deck; + +/// Trick-taking game mechanics including players, tricks, and hands. +/// Contains specific types and traits for trick-taking games. +pub use crate::trick_taking; + +/// A trait representing a card. The actual implementation depends on the game where this is used. +/// +/// This trait ensures cards can be displayed, compared, and used in collections. +/// Implementations must provide equality based on card identity (e.g., rank and suit). +pub trait Card: Display + Default + Sized + Debug + Copy + Eq + PartialEq {} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, Hash)] +/// The 4 suits of a standard deck. They have an equivalent in pretty much all regional decks. +/// In some games they have a hierarchical order. +/// +/// # Examples +/// ``` +/// use shuftlib::core::Suit; +/// +/// let heart = Suit::Hearts; +/// assert_eq!(format!("{}", heart), "H"); +/// ``` +pub enum Suit { + /// Hearts (French, German), Cups (Latin). + Hearts, + /// Diamonds or Tiles (French), Bells (German), Coins (Latin). + Diamonds, + /// Clubs or Clover (French), Acorns (German), Clubs or Batons (Latin). + Clubs, + /// Spades or Pikes (French), Leaves (German), Swords (Latin). + Spades, +} + +impl Display for Suit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + Suit::Hearts => "H", + Suit::Diamonds => "D", + Suit::Clubs => "C", + Suit::Spades => "S", + }; + write!(f, "{s}") + } +} + +/// Test utilities for the core module. +#[cfg(test)] +pub mod test_utils { + use super::Suit; + use proptest::prelude::*; + + /// Generates a random suit strategy for property testing. + pub fn suit_strategy() -> impl Strategy { + prop_oneof![ + Just(Suit::Hearts), + Just(Suit::Diamonds), + Just(Suit::Clubs), + Just(Suit::Spades), + ] + } +} diff --git a/src/core/deck.rs b/src/core/deck.rs new file mode 100644 index 0000000..bf3503e --- /dev/null +++ b/src/core/deck.rs @@ -0,0 +1,516 @@ +use rand::{Rng, seq::SliceRandom}; +use strum::IntoEnumIterator; + +use crate::core::{ + Card, Suit, + french::{FrenchCard, FrenchRank, FrenchWithJoker, Joker}, + italian::{ItalianCard, ItalianRank}, +}; + +#[derive(Default, Debug, Clone)] +/// Represents a deck of cards. Cards can be added or removed at will. +/// +/// # Examples +/// +/// Creating an Italian deck: +/// ``` +/// use shuftlib::core::deck::Deck; +/// use shuftlib::core::italian::{ItalianCard, ItalianRank}; +/// use shuftlib::core::Suit; +/// +/// let deck = Deck::::italian(); +/// assert_eq!(deck.len(), 40); +/// assert!(deck.iter().any(|card| card.rank() == ItalianRank::Ace && card.suit() == Suit::Hearts)); +/// ``` +/// +/// Creating a French deck: +/// ``` +/// use shuftlib::core::deck::Deck; +/// use shuftlib::core::french::{FrenchCard, FrenchRank}; +/// use shuftlib::core::Suit; +/// +/// let deck = Deck::::french(); +/// assert_eq!(deck.len(), 52); +/// assert!(deck.iter().any(|card| card.rank() == FrenchRank::Ace && card.suit() == Suit::Hearts)); +/// ``` +/// +/// Creating a French deck with jokers: +/// ``` +/// use shuftlib::core::deck::Deck; +/// use shuftlib::core::french::{FrenchWithJoker, Joker}; +/// +/// let deck = Deck::french_with_jokers(2); +/// assert_eq!(deck.len(), 54); +/// assert_eq!(deck.iter().filter(|c| matches!(c, FrenchWithJoker::Joker(_))).count(), 2); +/// ``` +pub struct Deck +where + T: Card, +{ + cards: Vec, +} + +const FRENCH_CARDS: usize = 52; +const ITALIAN_CARDS: usize = 40; + +impl Deck { + /// Creates a new deck in the Italian format. + /// Creates a new deck in the Italian format. + /// + /// # Examples + /// ``` + /// use shuftlib::core::deck::Deck; + /// use shuftlib::core::italian::ItalianCard; + /// + /// let deck = Deck::italian(); + /// assert_eq!(deck.len(), 40); + /// ``` + pub fn italian() -> Deck { + let mut cards = Vec::with_capacity(ITALIAN_CARDS); + for suit in Suit::iter() { + for rank in ItalianRank::iter() { + cards.push(ItalianCard::new(rank, suit)); + } + } + + Deck { cards } + } +} + +impl Deck { + /// Creates a new 52 cards French deck. + /// Creates a new 52 cards French deck. + /// + /// # Examples + /// ``` + /// use shuftlib::core::deck::Deck; + /// use shuftlib::core::french::FrenchCard; + /// + /// let deck = Deck::french(); + /// assert_eq!(deck.len(), 52); + /// ``` + pub fn french() -> Deck { + let mut cards = Vec::with_capacity(FRENCH_CARDS); + for suit in Suit::iter() { + for rank in FrenchRank::iter() { + cards.push(FrenchCard::new(rank, suit)); + } + } + + Deck { cards } + } + + /// Creates a new 52 cards French deck, with the addition of the specified amount of jokers. + /// Creates a new 52 cards French deck, with the addition of the specified amount of jokers. + /// + /// # Examples + /// ``` + /// use shuftlib::core::deck::Deck; + /// use shuftlib::core::french::{FrenchWithJoker, Joker}; + /// + /// let deck = Deck::french_with_jokers(2); + /// assert_eq!(deck.len(), 54); + /// assert_eq!(deck.iter().filter(|c| matches!(c, FrenchWithJoker::Joker(_))).count(), 2); + /// ``` + pub fn french_with_jokers(jokers: u8) -> Deck { + let mut cards = Vec::with_capacity(FRENCH_CARDS + jokers as usize); + for suit in Suit::iter() { + for rank in FrenchRank::iter() { + cards.push(FrenchWithJoker::Normal(FrenchCard::new(rank, suit))); + } + } + + for _ in 0..jokers { + cards.push(FrenchWithJoker::Joker(Joker {})); + } + + Deck { cards } + } +} + +impl Deck { + /// Performs a random permutation on the deck using the Fisher-Yates shuffle algorithm. + /// + /// # Examples + /// ``` + /// use shuftlib::core::deck::Deck; + /// use shuftlib::core::italian::ItalianCard; + /// + /// let mut deck = Deck::italian(); + /// deck.shuffle(); + /// assert_eq!(deck.len(), 40); + /// ``` + pub fn shuffle(&mut self) { + let mut rng = rand::rng(); + self.cards.shuffle(&mut rng); + } + + /// Adds a card in a random position inside the deck. + /// + /// # Examples + /// ``` + /// use shuftlib::core::deck::Deck; + /// use shuftlib::core::italian::{ItalianCard, ItalianRank}; + /// use shuftlib::core::Suit; + /// + /// let mut deck = Deck::italian(); + /// let card = ItalianCard::new(ItalianRank::Ace, Suit::Hearts); + /// deck.shuffle_card(card); + /// assert!(deck.len() >= 41); + /// ``` + pub fn shuffle_card(&mut self, card: T) { + let len = self.cards.len(); + let mut rng = rand::rng(); + let pos = match len { + 0 => 0, + 1 => 1, + 2 => 1, + _ => rng.random_range(1..len - 1), + }; + self.cards.insert(pos, card); + } + + /// Adds a card to the top of the deck. + /// + /// # Examples + /// ``` + /// use shuftlib::core::deck::Deck; + /// use shuftlib::core::italian::{ItalianCard, ItalianRank}; + /// use shuftlib::core::Suit; + /// + /// let mut deck = Deck::new(); + /// let card = ItalianCard::new(ItalianRank::Ace, Suit::Hearts); + /// deck.push(card); + /// assert_eq!(deck.len(), 1); + /// ``` + pub fn push(&mut self, card: T) { + self.cards.push(card); + } + + /// Draws the top-most card in the deck. It returns None if there are no cards left. + /// + /// # Examples + /// ``` + /// use shuftlib::core::deck::Deck; + /// use shuftlib::core::italian::ItalianCard; + /// + /// let mut deck = Deck::italian(); + /// let card = deck.draw(); + /// assert!(card.is_some()); + /// ``` + pub fn draw(&mut self) -> Option { + self.cards.pop() + } + + /// Creates a new empty deck. + /// + /// # Examples + /// ``` + /// use shuftlib::core::deck::Deck; + /// use shuftlib::core::italian::ItalianCard; + /// + /// let deck: Deck = Deck::new(); + /// assert!(deck.is_empty()); + /// ``` + pub fn new() -> Deck { + Deck { cards: Vec::new() } + } + + /// Creates a new empty deck with specified capacity. + /// + /// # Examples + /// ``` + /// use shuftlib::core::deck::Deck; + /// use shuftlib::core::italian::ItalianCard; + /// + /// let deck: Deck = Deck::with_capacity(10); + /// assert!(deck.is_empty()); + /// ``` + pub fn with_capacity(capacity: usize) -> Deck { + Deck { + cards: Vec::with_capacity(capacity), + } + } + + /// Returns an iterator over the cards in the deck. + /// + /// # Examples + /// ``` + /// use shuftlib::core::deck::Deck; + /// use shuftlib::core::italian::{ItalianCard, ItalianRank}; + /// use shuftlib::core::Suit; + /// + /// let cards = vec![ + /// ItalianCard::new(ItalianRank::Ace, Suit::Hearts), + /// ItalianCard::new(ItalianRank::Two, Suit::Hearts), + /// ItalianCard::new(ItalianRank::Three, Suit::Hearts), + /// ]; + /// let deck: Deck = Deck::from(cards.clone()); + /// assert_eq!(deck.iter().count(), 3); + /// assert_eq!(*deck.iter().next().unwrap(), cards[0]); + /// ``` + pub fn iter(&self) -> std::slice::Iter<'_, T> { + self.cards.iter() + } + + /// Returns a mutable iterator over the cards in the deck. + /// + /// # Examples + /// ``` + /// use shuftlib::core::deck::Deck; + /// use shuftlib::core::italian::{ItalianCard, ItalianRank}; + /// use shuftlib::core::Suit; + /// + /// let mut deck = Deck::from(vec![ + /// ItalianCard::new(ItalianRank::Ace, Suit::Hearts), + /// ItalianCard::new(ItalianRank::Two, Suit::Hearts), + /// ]); + /// for card in deck.iter_mut() { + /// // Modify cards if needed + /// } + /// assert_eq!(deck.len(), 2); + /// ``` + pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, T> { + self.cards.iter_mut() + } + + /// Returns a slice of all cards in the deck. + /// + /// # Examples + /// ``` + /// use shuftlib::core::deck::Deck; + /// use shuftlib::core::italian::ItalianCard; + /// + /// let deck = Deck::italian(); + /// let slice = deck.as_slice(); + /// assert_eq!(slice.len(), 40); + /// ``` + pub fn as_slice(&self) -> &[T] { + &self.cards + } + + /// Returns a mutable slice of all cards in the deck. + /// + /// # Examples + /// ``` + /// use shuftlib::core::deck::Deck; + /// use shuftlib::core::italian::ItalianCard; + /// + /// let mut deck = Deck::italian(); + /// let slice = deck.as_mut_slice(); + /// // Modify the slice if needed + /// assert_eq!(slice.len(), 40); + /// ``` + pub fn as_mut_slice(&mut self) -> &mut [T] { + &mut self.cards + } + + /// Returns the number of cards left in the deck. + /// + /// # Examples + /// ``` + /// use shuftlib::core::deck::Deck; + /// use shuftlib::core::italian::{ItalianCard, ItalianRank}; + /// use shuftlib::core::Suit; + /// + /// let cards = vec![ + /// ItalianCard::new(ItalianRank::Ace, Suit::Hearts), + /// ItalianCard::new(ItalianRank::Two, Suit::Hearts), + /// ItalianCard::new(ItalianRank::Three, Suit::Hearts), + /// ]; + /// let deck: Deck = Deck::from(cards); + /// assert_eq!(deck.len(), 3); + /// ``` + pub fn len(&self) -> usize { + self.cards.len() + } + + /// Returns whether or not the deck is empty. + /// + /// # Examples + /// ``` + /// use shuftlib::core::deck::Deck; + /// use shuftlib::core::italian::ItalianCard; + /// + /// let deck: Deck = Deck::new(); + /// assert!(deck.is_empty()); + /// ``` + pub fn is_empty(&self) -> bool { + self.cards.is_empty() + } +} + +impl From> for Deck { + fn from(cards: Vec) -> Self { + Deck { cards } + } +} + +impl FromIterator for Deck { + fn from_iter>(iter: I) -> Self { + Deck { + cards: Vec::from_iter(iter), + } + } +} + +impl AsRef<[T]> for Deck { + fn as_ref(&self) -> &[T] { + &self.cards + } +} + +impl IntoIterator for Deck { + type Item = T; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.cards.into_iter() + } +} + +impl<'a, T: Card> IntoIterator for &'a Deck { + type Item = &'a T; + type IntoIter = std::slice::Iter<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.cards.iter() + } +} + +impl<'a, T: Card> IntoIterator for &'a mut Deck { + type Item = &'a mut T; + type IntoIter = std::slice::IterMut<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.cards.iter_mut() + } +} + +#[cfg(test)] +mod tests { + use super::Deck; + use crate::core::deck::test_utils::deck_strategy; + use crate::core::italian::ItalianCard; + use crate::core::italian::test_utils::italian_rank_strategy; + use crate::core::test_utils::suit_strategy; + use proptest::prelude::*; + + #[test] + fn shuffle_changes_order() { + let sorted_deck = Deck::italian(); + + let mut shuffled_deck = Deck::italian(); + shuffled_deck.shuffle(); + + assert_ne!(sorted_deck.as_slice(), shuffled_deck.as_slice()); + } + + #[test] + fn shuffle_empty_deck() { + let mut deck: Deck = Deck::new(); + deck.shuffle(); + assert!(deck.is_empty()); + } + + proptest! { + #[test] + fn shuffle_preserves_cards( + mut deck in deck_strategy(0..=39) + ) { + let original: Vec = deck.as_slice().to_vec(); + deck.shuffle(); + let shuffled: Vec = deck.as_slice().to_vec(); + + // Same length and same set of cards (order may differ) + prop_assert_eq!(original.len(), shuffled.len()); + let mut original_sorted = original.clone(); + let mut shuffled_sorted = shuffled.clone(); + original_sorted.sort_by_key(|c: &ItalianCard| (c.rank() as u8, c.suit() as u8)); + shuffled_sorted.sort_by_key(|c: &ItalianCard| (c.rank() as u8, c.suit() as u8)); + prop_assert_eq!(original_sorted, shuffled_sorted); + } + + #[test] + fn draw_all_cards_yields_unique( + mut deck in deck_strategy(0..=39) + ) { + let mut seen = std::collections::HashSet::new(); + while let Some(card) = deck.draw() { + prop_assert!(seen.insert(card)); + } + } + + #[test] + fn push_and_draw_returns_same_card( + card in (italian_rank_strategy(), suit_strategy()) + ) { + let mut deck = Deck::new(); + let card = ItalianCard::new(card.0, card.1); + deck.push(card); + prop_assert_eq!(deck.draw(), Some(card)); + } + + #[test] + fn shuffle_card_inserts_card( + card in (italian_rank_strategy(), suit_strategy()), + mut deck in deck_strategy(0..=39) + ) { + let card = ItalianCard::new(card.0, card.1); + let old_len: usize = deck.len(); + deck.shuffle_card(card); + prop_assert!(deck.as_slice().contains(&card)); + prop_assert_eq!(deck.len(), old_len + 1); + } + + #[test] + fn shuffle_card_never_inserts_at_top_or_bottom_for_large_deck( + mut deck in deck_strategy(2..=40), + card in (italian_rank_strategy(), suit_strategy()) + ) { + let card = ItalianCard::new(card.0, card.1); + prop_assume!(!deck.as_slice().contains(&card)); + deck.shuffle_card(card); + let pos = deck.as_slice().iter().position(|c| *c == card).unwrap(); + let new_len = deck.len(); + prop_assert!(pos != 0 && pos != new_len - 1); + } + } +} + +/// Test utilities for the deck module. +#[cfg(test)] +pub mod test_utils { + use super::*; + use crate::core::Suit; + use crate::core::italian::{ItalianCard, ItalianRank}; + use proptest::prelude::*; + + /// Generates a Deck with unique cards, of size in the given range. + pub fn deck_strategy( + size: std::ops::RangeInclusive, + ) -> impl Strategy> { + // All possible unique cards + let all_cards: Vec = [ + ItalianRank::Ace, + ItalianRank::Two, + ItalianRank::Three, + ItalianRank::Four, + ItalianRank::Five, + ItalianRank::Six, + ItalianRank::Seven, + ItalianRank::Jack, + ItalianRank::Knight, + ItalianRank::King, + ] + .iter() + .flat_map(|&rank| { + [Suit::Hearts, Suit::Diamonds, Suit::Clubs, Suit::Spades] + .iter() + .map(move |&suit| ItalianCard::new(rank, suit)) + }) + .collect(); + + proptest::sample::subsequence(all_cards, size).prop_map(Deck::from) + } +} diff --git a/src/core/french.rs b/src/core/french.rs new file mode 100644 index 0000000..8c50e2e --- /dev/null +++ b/src/core/french.rs @@ -0,0 +1,129 @@ +use std::fmt::Display; + +use strum::{EnumIter, FromRepr}; + +use crate::core::{Card, Suit}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Representation of a card that goes into an French deck. +pub struct FrenchCard { + rank: FrenchRank, + suit: Suit, +} + +impl FrenchCard { + /// Generates a card with the given rank and suit + /// + /// # Examples + /// ``` + /// use shuftlib::core::french::{FrenchCard, FrenchRank}; + /// use shuftlib::core::Suit; + /// + /// let card = FrenchCard::new(FrenchRank::Ace, Suit::Hearts); + /// assert_eq!(card.rank(), FrenchRank::Ace); + /// assert_eq!(card.suit(), Suit::Hearts); + /// ``` + pub fn new(rank: FrenchRank, suit: Suit) -> Self { + Self { rank, suit } + } + + /// The rank of the card. + pub fn rank(&self) -> FrenchRank { + self.rank + } + + /// The suit of the card. + pub fn suit(&self) -> Suit { + self.suit + } +} + +impl Default for FrenchCard { + fn default() -> Self { + FrenchCard { + rank: FrenchRank::Ace, + suit: Suit::Hearts, + } + } +} + +impl Display for FrenchCard { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}{}", self.rank as u8, self.suit) + } +} + +impl Card for FrenchCard {} + +/// A Joker card, present in some card games. Its function depends on the game. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct Joker; + +impl Card for Joker {} + +impl Display for Joker { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "JK") + } +} + +/// A variant of the French card, which can either be an actual French card or a joker. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FrenchWithJoker { + /// A standard French card. + Normal(FrenchCard), + /// A Joker card. + Joker(Joker), +} +impl Card for FrenchWithJoker {} + +impl Default for FrenchWithJoker { + fn default() -> Self { + Self::Normal(FrenchCard { + rank: FrenchRank::Ace, + suit: Suit::Hearts, + }) + } +} + +impl Display for FrenchWithJoker { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FrenchWithJoker::Normal(c) => write!(f, "{c}"), + FrenchWithJoker::Joker(c) => write!(f, "{c}"), + } + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, EnumIter, FromRepr, Hash)] +#[repr(u8)] +/// The rank of the card. In a French deck, ranks go from the ace to 10, then there is a jack, queen and king, +/// In most games they each have a different value that depends on the game itself. +pub enum FrenchRank { + /// 1 + Ace = 1, + /// 2 + Two, + /// 3 + Three, + /// 4 + Four, + /// 5 + Five, + /// 6 + Six, + /// 7 + Seven, + /// 8 + Eight, + /// 9 + Nine, + /// 10 + Ten, + /// 11 + Jack, + /// 12 + Queen, + /// 13 + King, +} diff --git a/src/core/italian.rs b/src/core/italian.rs new file mode 100644 index 0000000..dae7baa --- /dev/null +++ b/src/core/italian.rs @@ -0,0 +1,106 @@ +use std::fmt::Display; + +use strum::{EnumIter, FromRepr}; + +use crate::core::{Card, Suit}; + +/// Representation of a card that goes into an Italian deck. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ItalianCard { + rank: ItalianRank, + suit: Suit, +} + +impl ItalianCard { + /// Generates a card with the given rank and suit + /// + /// # Examples + /// ``` + /// use shuftlib::core::italian::{ItalianCard, ItalianRank}; + /// use shuftlib::core::Suit; + /// + /// let card = ItalianCard::new(ItalianRank::Ace, Suit::Hearts); + /// assert_eq!(card.rank(), ItalianRank::Ace); + /// assert_eq!(card.suit(), Suit::Hearts); + /// ``` + pub fn new(rank: ItalianRank, suit: Suit) -> Self { + Self { rank, suit } + } + + /// The rank of the card. + pub fn rank(&self) -> ItalianRank { + self.rank + } + + /// The suit of the card. + pub fn suit(&self) -> Suit { + self.suit + } +} + +impl Default for ItalianCard { + fn default() -> Self { + ItalianCard { + rank: ItalianRank::Ace, + suit: Suit::Clubs, + } + } +} + +impl Display for ItalianCard { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}{}", self.rank as u8, self.suit) + } +} + +impl Card for ItalianCard {} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, EnumIter, FromRepr, Hash)] +#[repr(u8)] +/// The rank of the card. In an Italian deck, ranks go from the ace to the 7, then they also have a jack, knight and king, +/// In most games they each have a different value that depends on the game itself. +pub enum ItalianRank { + /// 1 + Ace = 1, + /// 2 + Two, + /// 3 + Three, + /// 4 + Four, + /// 5 + Five, + /// 6 + Six, + /// 7 + Seven, + /// 8 + Jack, + /// 9 + Knight, + /// 10 + King, +} + +#[cfg(test)] +/// Test utilities for the Italian card module. +pub mod test_utils { + use super::ItalianRank; + use proptest::prelude::*; + + /// Generates a random Italian rank strategy for property testing. + pub fn italian_rank_strategy() -> impl Strategy { + prop_oneof![ + Just(ItalianRank::Ace), + Just(ItalianRank::Two), + Just(ItalianRank::Three), + Just(ItalianRank::Four), + Just(ItalianRank::Five), + Just(ItalianRank::Six), + Just(ItalianRank::Seven), + Just(ItalianRank::Jack), + Just(ItalianRank::Knight), + Just(ItalianRank::King), + ] + } +} diff --git a/src/lib.rs b/src/lib.rs index 07f4f1c..096ea5b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,18 @@ -//! This crate contains all the necessary types, methods, functions and traits -//! to work with cards, decks and card games. -#![expect(incomplete_features)] -#![feature(generic_const_exprs)] +// Allow panics, unwraps, and expects in test code +#![cfg_attr( + test, + allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + clippy::panic_in_result_fn + ) +)] +#![doc = include_str!("../README.md")] -/// Contains basic types common to various card games. -pub mod common; -/// Contains the logic relative to the tressette engine. +/// Core primitives for card games (cards, decks, game mechanics). +pub mod core; +/// Contains the logic for the tressette game. pub mod tressette; +/// Trick-taking game mechanics available at the crate root. +pub mod trick_taking; diff --git a/src/tressette.rs b/src/tressette.rs index 065ed23..4e87d62 100644 --- a/src/tressette.rs +++ b/src/tressette.rs @@ -1,368 +1,19 @@ -use std::{fmt::Display, ops::Deref}; +//! Tressette card game implementation. +//! +//! This module contains the core types and logic for playing tressette, +//! including card representations, game rules, position management, and +//! high-level game state. -use crate::common::{ - cards::{Card, ItalianCard, ItalianRank, Suit}, - hands::{Hand, OngoingTrick, Player, PlayerId, TrickTakingGame}, -}; -use num_rational::Rational32; -use std::cmp::Ordering; +/// Card representation for tressette. +pub mod card; -#[derive(Clone, Debug, Default)] -/// Contains the rules of the tressette game. -pub struct TressetteRules {} +/// Game rules implementation. +pub mod rules; -impl TrickTakingGame for TressetteRules { - type CardType = TressetteCard; +/// High-level game state management for move-based gameplay. +pub mod game; - const PLAYERS: usize = 4; - const TRICKS: usize = 10; - - /// Contains the logic to determine who won the trick in a standard - /// tressette game: The winner of the trick is always the player who played - /// the highest card with the same `Suit` of the first `TressetteCard` - /// played that trick. See the implementation of `Ord` and `PartialOrd` for - /// `TressetteCard` for more info. The implementation of this trait is meant - /// to only be used internally by `OngoingTrick`, however it's possible to - /// call it elsewhere if needed. It also assumes the slice `cards` is valid - /// for the tressette game, so it assumes there are no duplicates. It's a - /// responsability of the caller to make sure that's the case. - /// - /// # Panics - /// - /// It can only panic in case of a bug in this crate. - /// - /// # Examples - /// - /// ``` - /// #![feature(generic_const_exprs)] - /// use shuftlib::common::{hands::{TrickTakingGame, PlayerId}, cards::{ItalianRank, Suit}}; - /// use shuftlib::tressette::{TressetteRules, TressetteCard}; - /// - /// let cards = [ - /// TressetteCard::new(ItalianRank::Ace, Suit::Hearts), - /// TressetteCard::new(ItalianRank::Two, Suit::Hearts), - /// TressetteCard::new(ItalianRank::Three, Suit::Hearts), - /// TressetteCard::new(ItalianRank::Four, Suit::Hearts), - /// ]; - /// - /// let taker = TressetteRules::determine_taker(&cards, PlayerId::new(2).unwrap()); - /// assert_eq!(taker, PlayerId::new(2).unwrap()); - /// ``` - #[allow(clippy::expect_used)] - fn determine_taker( - cards: &[TressetteCard; Self::PLAYERS], - first_to_play: PlayerId<{ Self::PLAYERS }>, - ) -> PlayerId<{ Self::PLAYERS }> { - let leading_suit = cards[*first_to_play].suit(); - let (taker, _) = cards - .iter() - .enumerate() - .filter(|&(_, &c)| c.suit() == leading_suit) - .max_by_key(|&(_, &c)| c) - .expect("Max by key returned None. This shouldn't have happened, since it's being called on a non empty slice."); - - PlayerId::new(taker).expect("Initialization of a new PlayerId failed. This shouldn't have happened, since the input usize was computed starting from a fixed length slice.") - } -} - -/// The score a team has to reach to win a game of tressette. -pub const SCORE_TO_WIN: u8 = 31; - -impl TressetteRules { - /// Determines if a team won the game. A team wins the game when its score is - /// greater than 31 and has a higher score than the other team. - pub fn is_completed(score: (u8, u8)) -> bool { - (score.0 >= SCORE_TO_WIN && score.0 > score.1) - || (score.1 >= SCORE_TO_WIN && score.1 > score.0) - } - - /// Returns a view of the playable cards held by a player, based on the suit - /// of a card that has been played before and by the rules of tressette. If - /// the player is the first to play, the leading suit can be None. - /// - /// # Examples - /// - /// ``` - /// #![feature(generic_const_exprs)] - /// use shuftlib::tressette::{TressetteRules, TressetteCard}; - /// use shuftlib::common::hands::Player; - /// use shuftlib::common::cards::{Suit, ItalianRank}; - /// - /// let mut player = Player::default(); - /// player.give(TressetteCard::new(ItalianRank::Ace, Suit::Spades)); - /// player.give(TressetteCard::new(ItalianRank::Two, Suit::Spades)); - /// player.give(TressetteCard::new(ItalianRank::Ace, Suit::Hearts)); - /// - /// assert_eq![TressetteRules::playable(&player, Some(Suit::Spades)).len(), 2]; - /// assert_eq![TressetteRules::playable(&player, Some(Suit::Clubs)).len(), 3]; - /// ``` - pub fn playable( - player: &Player, - leading_suit: Option, - ) -> Vec { - if let Some(leading_suit) = leading_suit { - if player.hand().iter().any(|c| c.suit() == leading_suit) { - return player - .hand() - .iter() - .filter(|c| c.suit() == leading_suit) - .cloned() - .collect(); - } - } - - player.hand().into() - } - - /// Plays the specified card for the player - pub fn play( - player: &mut Player, - card: TressetteCard, - ongoing_trick: &mut OngoingTrick, - ) { - player.remove(card); - ongoing_trick.play(card); - } - - /// Computes the score for a hand of the tressette game. - /// Score is always a maximum of 11 points. - pub fn compute_score(hand: &Hand, score: &mut (u8, u8)) { - let mut tmp_score = (Rational32::new(0, 3), Rational32::new(0, 3)); - - let mut taker = 0; - for trick in hand.tricks() { - if *trick.taker() == 0 || *trick.taker() == 2 { - tmp_score.0 += trick.cards().iter().map(|c| c.value()).sum::(); - } else { - tmp_score.1 += trick.cards().iter().map(|c| c.value()).sum::(); - } - - taker = *trick.taker(); - } - - score.0 += tmp_score.0.to_integer() as u8; - score.1 += tmp_score.1.to_integer() as u8; - - if taker == 0 || taker == 2 { - score.0 += 1; - } else { - score.1 += 1; - } - } -} - -#[derive(PartialEq, Eq, Debug, Clone, Copy, Default, Hash)] -/// Representation of a card used in variations of the Tressette game. It's just -/// a new type over `ItalianCard`. -pub struct TressetteCard { - card: ItalianCard, -} - -impl PartialOrd for TressetteCard { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for TressetteCard { - #[allow(clippy::expect_used)] - fn cmp(&self, other: &Self) -> Ordering { - let rank_order = [ - ItalianRank::Four, - ItalianRank::Five, - ItalianRank::Six, - ItalianRank::Seven, - ItalianRank::Jack, - ItalianRank::Knight, - ItalianRank::King, - ItalianRank::Ace, - ItalianRank::Two, - ItalianRank::Three, - ]; - - let self_rank_index = rank_order.iter().position(|&r| self.card.rank() == r).expect("The rank of self wasn't found inside the Ord implementation for TressetteCard. This shouldn't have happened, please file a bug report."); - let other_rank_index = rank_order.iter().position(|&r| other.card.rank() == r).expect("The rank of other wasn't found inside the Ord implementation for TressetteCard. This shouldn't have happened, please file a bug report."); - - self_rank_index.cmp(&other_rank_index) - } -} - -impl Display for TressetteCard { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.card) - } -} - -impl Card for TressetteCard {} - -impl From for TressetteCard { - fn from(value: ItalianCard) -> Self { - TressetteCard { card: value } - } -} - -impl Deref for TressetteCard { - type Target = ItalianCard; - - fn deref(&self) -> &Self::Target { - &self.card - } -} - -impl TressetteCard { - /// Gets the value of the card by the rules of the Tressette game: - /// - Ace = 1 - /// - 2, 3 and figures = 1/3 - /// - the rest = 0/3 - /// - /// # Examples - /// ``` - /// #![feature(generic_const_exprs)] - /// use shuftlib::{tressette::TressetteCard, common::cards::{Suit, ItalianRank}}; - /// use num_rational::Rational32; - /// - /// let ace = TressetteCard::new(ItalianRank::Ace, Suit::Hearts); - /// let two = TressetteCard::new(ItalianRank::Two, Suit::Spades); - /// let four = TressetteCard::new(ItalianRank::Four, Suit::Clubs); - /// assert_eq!(ace.value(), Rational32::new(3,3)); - /// assert_eq!(two.value(), Rational32::new(1,3)); - /// assert_eq!(four.value(), Rational32::new(0,3)); - /// ``` - pub fn value(&self) -> Rational32 { - match self.rank() { - ItalianRank::Ace => Rational32::new(3, 3), - ItalianRank::Two - | ItalianRank::Three - | ItalianRank::King - | ItalianRank::Knight - | ItalianRank::Jack => Rational32::new(1, 3), - ItalianRank::Four | ItalianRank::Five | ItalianRank::Six | ItalianRank::Seven => { - Rational32::new(0, 3) - } - } - } - - /// Generates a new `TressetteCard` starting from an `ItalianRank` and - /// a `Suit`. - /// - /// # Examples. - /// ``` - /// #![feature(generic_const_exprs)] - /// use shuftlib::common::cards::{ItalianCard, ItalianRank, Suit}; - /// use shuftlib::tressette::TressetteCard; - /// - /// let suit = Suit::Spades; - /// let rank = ItalianRank::Ace; - /// assert_eq!(*TressetteCard::new(rank, suit), ItalianCard::new(rank,suit)); - /// ``` - pub fn new(rank: ItalianRank, suit: Suit) -> Self { - let card = ItalianCard::new(rank, suit); - - TressetteCard { card } - } -} - -#[cfg(test)] -mod tests { - use crate::{ - common::{ - cards::{ItalianRank, Suit}, - hands::{Player, PlayerId, TrickTakingGame}, - }, - tressette::SCORE_TO_WIN, - }; - use prop::collection::hash_set; - use proptest::prelude::*; - - use super::{TressetteCard, TressetteRules}; - - fn tressette_card_strategy() -> impl Strategy { - ( - prop_oneof![ - Just(ItalianRank::Ace), - Just(ItalianRank::Two), - Just(ItalianRank::Three), - Just(ItalianRank::Four), - Just(ItalianRank::Five), - Just(ItalianRank::Six), - Just(ItalianRank::Seven), - Just(ItalianRank::Jack), - Just(ItalianRank::Knight), - Just(ItalianRank::King), - ], - prop_oneof![ - Just(Suit::Hearts), - Just(Suit::Clubs), - Just(Suit::Spades), - Just(Suit::Diamonds), - ], - ) - .prop_map(|(rank, suit)| TressetteCard::new(rank, suit)) - } - - fn player_strategy() -> impl Strategy> { - ( - 0..TressetteRules::PLAYERS, - hash_set(tressette_card_strategy(), 1..TressetteRules::TRICKS), - ) - .prop_map(|(index, cards)| { - let mut player = Player::new(PlayerId::new(index).unwrap()); - for card in cards { - player.give(card); - } - player - }) - } - - proptest! { - #[test] - fn a_team_won_with_both_below(team1_score in 0u8..SCORE_TO_WIN, team2_score in 0u8..SCORE_TO_WIN) { - let result = TressetteRules::is_completed((team1_score, team2_score)); - assert!(!result); - } - - #[test] - fn a_team_won_with_both_above_and_same(score in SCORE_TO_WIN..u8::MAX) { - let result = TressetteRules::is_completed((score, score)); - assert!(!result); - } - - #[test] - fn a_team_won_with_both_above_and_different(score in SCORE_TO_WIN..u8::MAX) { - let result = TressetteRules::is_completed((score, score+1)); - assert!(result); - } - - #[test] - fn a_team_won_with_team1_above(team1_score in 0u8..SCORE_TO_WIN, team2_score in SCORE_TO_WIN..u8::MAX) { - let result = TressetteRules::is_completed((team1_score, team2_score)); - assert!(result); - } - - #[test] - fn a_team_won_with_team2_above(team1_score in SCORE_TO_WIN..u8::MAX, team2_score in 0u8..SCORE_TO_WIN ) { - let result = TressetteRules::is_completed((team1_score, team2_score)); - assert!(result); - } - - #[test] - fn playable_works(player in player_strategy(), suit in prop_oneof![Just(Suit::Hearts), Just(Suit::Spades), Just(Suit::Clubs), Just(Suit::Diamonds)]) { - let playable = TressetteRules::playable(&player, None); - - // When the player is the first to go, he can play whatever he wants. - prop_assert_eq!(playable.len(), player.hand().len()); - - let playable = TressetteRules::playable(&player, Some(suit)); - let same_suit_cards = player.hand().iter().filter(|c| c.suit() == suit).count(); - - // When the player is not the first, if he doesn't have cards of the - // same suit as the first, he can play whatever he wants, otherwise - // he can only play cards of the same suit. - if same_suit_cards == 0 { - prop_assert_eq!(playable.len(), player.hand().len()); - } else { - prop_assert_eq!(playable.len(), same_suit_cards); - } - } - } -} +// Re-export main types for convenience +pub use card::TressetteCard; +pub use rules::{TressetteRules, SCORE_TO_WIN}; +pub use game::{Game, MoveEffect, Status, Error}; diff --git a/src/tressette/card.rs b/src/tressette/card.rs new file mode 100644 index 0000000..d6a1dfd --- /dev/null +++ b/src/tressette/card.rs @@ -0,0 +1,197 @@ +//! Card implementation for the Tressette game. +//! +//! This module provides [`TressetteCard`], a wrapper around [`ItalianCard`] that implements +//! Tressette-specific ordering and scoring rules. + +use std::{cmp::Ordering, fmt::Display, ops::Deref}; + +use crate::core::italian::{ItalianCard, ItalianRank}; +use crate::core::{Card, Suit}; +use num_rational::Rational32; + +/// A card in the Tressette game. +/// +/// Wraps an [`ItalianCard`] and implements Tressette-specific ordering where: +/// 4 < 5 < 6 < 7 < Jack < Knight < King < Ace < 2 < 3 +/// +/// # Examples +/// +/// ``` +/// use shuftlib::tressette::TressetteCard; +/// use shuftlib::core::Suit; +/// use shuftlib::core::italian::ItalianRank; +/// +/// let ace = TressetteCard::new(ItalianRank::Ace, Suit::Hearts); +/// let three = TressetteCard::new(ItalianRank::Three, Suit::Hearts); +/// +/// assert!(three > ace); +/// assert_eq!(ace.suit(), Suit::Hearts); +/// ``` +#[derive(PartialEq, Eq, Debug, Clone, Copy, Default, Hash)] +pub struct TressetteCard { + card: ItalianCard, +} + +impl PartialOrd for TressetteCard { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for TressetteCard { + /// Compare cards by Tressette rank ordering. + /// + /// In Tressette, the rank order from lowest to highest is: + /// 4 < 5 < 6 < 7 < Jack < Knight < King < Ace < 2 < 3 + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::TressetteCard; + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::ItalianRank; + /// + /// // Test all adjacent pairs (transitivity guarantees full ordering) + /// let four = TressetteCard::new(ItalianRank::Four, Suit::Hearts); + /// let five = TressetteCard::new(ItalianRank::Five, Suit::Hearts); + /// let six = TressetteCard::new(ItalianRank::Six, Suit::Hearts); + /// let seven = TressetteCard::new(ItalianRank::Seven, Suit::Hearts); + /// let jack = TressetteCard::new(ItalianRank::Jack, Suit::Hearts); + /// let knight = TressetteCard::new(ItalianRank::Knight, Suit::Hearts); + /// let king = TressetteCard::new(ItalianRank::King, Suit::Hearts); + /// let ace = TressetteCard::new(ItalianRank::Ace, Suit::Hearts); + /// let two = TressetteCard::new(ItalianRank::Two, Suit::Hearts); + /// let three = TressetteCard::new(ItalianRank::Three, Suit::Hearts); + /// + /// assert!(four < five); + /// assert!(five < six); + /// assert!(six < seven); + /// assert!(seven < jack); + /// assert!(jack < knight); + /// assert!(knight < king); + /// assert!(king < ace); + /// assert!(ace < two); + /// assert!(two < three); + /// ``` + fn cmp(&self, other: &Self) -> Ordering { + fn rank_order(rank: ItalianRank) -> u8 { + match rank { + ItalianRank::Four => 0, + ItalianRank::Five => 1, + ItalianRank::Six => 2, + ItalianRank::Seven => 3, + ItalianRank::Jack => 4, + ItalianRank::Knight => 5, + ItalianRank::King => 6, + ItalianRank::Ace => 7, + ItalianRank::Two => 8, + ItalianRank::Three => 9, + } + } + + rank_order(self.rank()).cmp(&rank_order(other.rank())) + } +} + +impl Display for TressetteCard { + /// Formats the card for display using rank number and suit symbol. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::TressetteCard; + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::ItalianRank; + /// + /// let card = TressetteCard::new(ItalianRank::Ace, Suit::Hearts); + /// assert_eq!(format!("{}", card), "1H"); + /// ``` + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.card) + } +} + +impl Card for TressetteCard {} + +impl From for TressetteCard { + /// Creates a `TressetteCard` from an `ItalianCard`. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::TressetteCard; + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::{ItalianCard, ItalianRank}; + /// + /// let italian_card = ItalianCard::new(ItalianRank::King, Suit::Clubs); + /// let tressette_card: TressetteCard = italian_card.into(); + /// assert_eq!(*tressette_card, italian_card); + /// ``` + fn from(value: ItalianCard) -> Self { + TressetteCard { card: value } + } +} + +impl Deref for TressetteCard { + type Target = ItalianCard; + + fn deref(&self) -> &Self::Target { + &self.card + } +} + +impl TressetteCard { + /// Returns the point value of the card in Tressette. + /// + /// - Ace = 1 point (3/3) + /// - Two, Three, and figures (Jack, Knight, King) = 1/3 point + /// - All other cards = 0 points + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::TressetteCard; + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::ItalianRank; + /// use num_rational::Rational32; + /// + /// let ace = TressetteCard::new(ItalianRank::Ace, Suit::Hearts); + /// let two = TressetteCard::new(ItalianRank::Two, Suit::Spades); + /// let four = TressetteCard::new(ItalianRank::Four, Suit::Clubs); + /// + /// assert_eq!(ace.value(), Rational32::new(3, 3)); + /// assert_eq!(two.value(), Rational32::new(1, 3)); + /// assert_eq!(four.value(), Rational32::new(0, 3)); + /// ``` + pub fn value(&self) -> Rational32 { + match self.rank() { + ItalianRank::Ace => Rational32::new(3, 3), + ItalianRank::Two + | ItalianRank::Three + | ItalianRank::King + | ItalianRank::Knight + | ItalianRank::Jack => Rational32::new(1, 3), + ItalianRank::Four | ItalianRank::Five | ItalianRank::Six | ItalianRank::Seven => { + Rational32::new(0, 3) + } + } + } + + /// Creates a new `TressetteCard` from a rank and suit. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::{ItalianCard, ItalianRank}; + /// use shuftlib::tressette::TressetteCard; + /// + /// let card = TressetteCard::new(ItalianRank::Ace, Suit::Spades); + /// assert_eq!(*card, ItalianCard::new(ItalianRank::Ace, Suit::Spades)); + /// ``` + pub fn new(rank: ItalianRank, suit: Suit) -> Self { + let card = ItalianCard::new(rank, suit); + + TressetteCard { card } + } +} diff --git a/src/tressette/game.rs b/src/tressette/game.rs new file mode 100644 index 0000000..7f35c24 --- /dev/null +++ b/src/tressette/game.rs @@ -0,0 +1,653 @@ +//! High-level game state management for Tressette. + +use crate::core::{ + Suit, + deck::Deck, + trick_taking::{Hand, OngoingHand, OngoingTrick, PLAYERS, PlayerId, TrickTakingGame}, +}; + +use super::{TressetteCard, TressetteRules}; + +/// The effect of making a move in the game. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MoveEffect { + /// A card was played, but the trick is not yet complete. + CardPlayed, + /// A trick was completed, and the winner is now the next to play. + TrickCompleted { + /// The player who won the trick. + winner: PlayerId, + }, + /// A hand was completed, scores were updated, and a new hand was dealt. + HandComplete { + /// The player who won the last trick of the hand. + trick_winner: PlayerId, + /// The updated scores after the hand: (team_0_2_score, team_1_3_score). + score: (u8, u8), + }, + /// The game is over. + GameOver { + /// The player who won the last trick. + trick_winner: PlayerId, + /// The final scores: (team_0_2_score, team_1_3_score). + final_score: (u8, u8), + }, +} + +/// The current status of the game. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The game is ongoing. + Ongoing, + /// The game is finished. + Finished { + /// The winning team, or `None` if it's a draw (which doesn't happen in Tressette). + winner: Option, + }, +} + +/// An error that can occur when making a move. +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum Error { + /// The card is not in the current player's hand. + #[error("Card {card} is not in player {player}'s hand")] + CardNotInHand { + /// The card that was attempted. + card: TressetteCard, + /// The player who tried to play it. + player: PlayerId, + }, + /// The player must follow suit but did not. + #[error("Must follow suit: {required_suit}")] + MustFollowSuit { + /// The suit that must be followed. + required_suit: Suit, + }, + /// The game is already over. + #[error("Game is already over")] + GameOver, + /// An internal state error occurred. + /// + /// This indicates a bug in the library. + #[error("Internal state error: {0}")] + InternalError(String), +} + +/// The state of a Tressette game at a given point. +/// +/// This includes the players' hands, the current trick, scores, history, and other game state. +#[derive(Debug, Clone, bon::Builder)] +pub struct Game { + hands: [Vec; PLAYERS], + current_hand: OngoingHand, + next_to_play: PlayerId, + trick_leader: PlayerId, + score: (u8, u8), + hands_completed: usize, + completed_hands: Vec>, + history: Vec<(TressetteCard, MoveEffect)>, +} + +impl Game { + /// Creates a new game with cards dealt. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::Game; + /// + /// let game = Game::new(); + /// assert_eq!(game.score(), (0, 0)); + /// ``` + pub fn new() -> Self { + let mut game = Self { + hands: [vec![], vec![], vec![], vec![]], + current_hand: OngoingHand::new(), + next_to_play: PlayerId::PLAYER_0, + trick_leader: PlayerId::PLAYER_0, + score: (0, 0), + hands_completed: 0, + completed_hands: Vec::new(), + history: Vec::new(), + }; + game.deal(); + game + } + + /// Returns the player whose turn it is to move. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::Game; + /// + /// let game = Game::new(); + /// let player = game.current_player(); + /// ``` + pub fn current_player(&self) -> PlayerId { + self.next_to_play + } + + /// Returns the current scores: (team_0_2_score, team_1_3_score). + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::Game; + /// + /// let game = Game::new(); + /// assert_eq!(game.score(), (0, 0)); + /// ``` + pub fn score(&self) -> (u8, u8) { + self.score + } + + /// Returns the current status of the game. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::{Game, Status}; + /// + /// let game = Game::new(); + /// assert_eq!(game.status(), Status::Ongoing); + /// ``` + pub fn status(&self) -> Status { + if TressetteRules::is_completed(self.score) { + let winner = if self.score.0 > self.score.1 { + Some(0) + } else if self.score.1 > self.score.0 { + Some(1) + } else { + None + }; + Status::Finished { winner } + } else { + Status::Ongoing + } + } + + /// Returns the cards in the given player's hand. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::Game; + /// use shuftlib::trick_taking::PlayerId; + /// + /// let game = Game::new(); + /// let hand = game.hand(PlayerId::PLAYER_0); + /// assert_eq!(hand.len(), 10); + /// ``` + pub fn hand(&self, player: PlayerId) -> &[TressetteCard] { + &self.hands[player.as_usize()] + } + + /// Returns the cards currently played in the ongoing trick. + /// + /// The array is indexed by player ID, with `None` for players who haven't played yet. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::Game; + /// + /// let game = Game::new(); + /// let trick = game.current_trick(); + /// assert!(trick.iter().all(|c| c.is_none())); + /// ``` + pub fn current_trick(&self) -> &[Option; PLAYERS] { + self.current_hand + .current_trick() + .as_ref() + .map(|ot| ot as &[Option; PLAYERS]) + .unwrap_or(&[None; PLAYERS]) + } + + /// Returns the player who led the current trick. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::Game; + /// use shuftlib::trick_taking::PlayerId; + /// + /// let game = Game::new(); + /// assert_eq!(game.trick_leader(), PlayerId::PLAYER_0); + /// ``` + pub fn trick_leader(&self) -> PlayerId { + self.trick_leader + } + + /// Returns the number of tricks completed in the current hand. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::Game; + /// + /// let game = Game::new(); + /// assert_eq!(game.tricks_this_hand(), 0); + /// ``` + pub fn tricks_this_hand(&self) -> usize { + self.current_hand + .tricks() + .iter() + .filter(|t| t.is_some()) + .count() + } + + /// Returns the number of hands completed so far. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::Game; + /// + /// let game = Game::new(); + /// assert_eq!(game.hands_completed(), 0); + /// ``` + pub fn hands_completed(&self) -> usize { + self.hands_completed + } + + /// Returns all legal cards for the current player. + /// + /// Returns an empty vector if the game is over. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::Game; + /// + /// let game = Game::new(); + /// let cards = game.legal_cards(); + /// assert!(!cards.is_empty()); + /// ``` + pub fn legal_cards(&self) -> Vec { + if matches!(self.status(), Status::Finished { .. }) { + return vec![]; + } + + let hand = &self.hands[self.next_to_play.as_usize()]; + + let leading_suit = self + .current_hand + .current_trick() + .as_ref() + .and_then(|ot| ot.iter().flatten().next().map(|c| c.suit())); + + match leading_suit { + Some(suit) => { + let same_suit: Vec<_> = hand.iter().filter(|c| c.suit() == suit).copied().collect(); + if same_suit.is_empty() { + hand.clone() + } else { + same_suit + } + } + None => hand.clone(), + } + } + + /// Returns `true` if the given card is legal for the current player. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::Game; + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::ItalianRank; + /// use shuftlib::tressette::TressetteCard; + /// + /// let game = Game::new(); + /// let card = game.hand(game.current_player())[0]; + /// assert!(game.is_legal_card(card)); + /// ``` + pub fn is_legal_card(&self, card: TressetteCard) -> bool { + self.legal_cards().contains(&card) + } + + /// Plays a card, updating the game state and recording the card in history. + /// + /// This is the core state transition function. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::Game; + /// + /// let mut game = Game::new(); + /// let legal = game.legal_cards(); + /// let effect = game.play_card(legal[0]).unwrap(); + /// ``` + /// + /// # Errors + /// + /// Returns an error if: + /// - The game is already over ([`Error::GameOver`]). + /// - The card is not in the current player's hand ([`Error::CardNotInHand`]). + /// - The player must follow suit but didn't ([`Error::MustFollowSuit`]). + /// - Internal state is inconsistent ([`Error::InternalError`]). + pub fn play_card(&mut self, card: TressetteCard) -> Result { + // Check game not over + if matches!(self.status(), Status::Finished { .. }) { + return Err(Error::GameOver); + } + + // Validate card + if !self.is_legal_card(card) { + let hand = &self.hands[self.next_to_play.as_usize()]; + if !hand.contains(&card) { + return Err(Error::CardNotInHand { + card, + player: self.next_to_play, + }); + } else { + // Compute required suit + let required_suit = self + .leading_suit() + .ok_or(Error::InternalError("No leading suit".to_string()))?; + return Err(Error::MustFollowSuit { required_suit }); + } + } + + // Apply move + let player_idx = self.next_to_play.as_usize(); + self.hands[player_idx].retain(|&c| c != card); + + // Add to trick + if let Some(ot) = self.current_hand.current_trick_mut().as_mut() { + ot.play(card); + self.next_to_play = ot.next_to_play(); + } else { + // Should not happen + return Err(Error::InternalError("No current trick".to_string())); + } + + // Check if trick is complete + let effect = if self.is_trick_complete() { + self.complete_trick()? + } else { + MoveEffect::CardPlayed + }; + + // Record in history + self.history.push((card, effect)); + + Ok(effect) + } + + /// Returns the suit of the leading card in the current trick, if any. + fn leading_suit(&self) -> Option { + self.current_hand + .current_trick() + .as_ref() + .and_then(|ot| ot.iter().flatten().next().map(|c| c.suit())) + } + + /// Returns the card history. + /// + /// Each element is a tuple of the card played and its effect. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::Game; + /// + /// let mut game = Game::new(); + /// let legal = game.legal_cards(); + /// game.play_card(legal[0]).unwrap(); + /// assert_eq!(game.history().len(), 1); + /// ``` + pub fn history(&self) -> &[(TressetteCard, MoveEffect)] { + &self.history + } + + /// Returns the game state after the first `n` cards in the history. + /// + /// Returns `None` if `n` is greater than the number of cards played. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::Game; + /// + /// let game = Game::new(); + /// let initial_state = game.state_after_card(0); + /// assert!(initial_state.is_some()); + /// ``` + pub fn state_after_card(&self, n: usize) -> Option { + if n > self.history.len() { + return None; + } + + let mut game = Game::new(); + for (card, _) in &self.history[..n] { + if game.play_card(*card).is_err() { + return None; + } + } + Some(game) + } + + fn is_trick_complete(&self) -> bool { + self.current_hand + .current_trick() + .as_ref() + .map(|ot| ot.iter().all(|c| c.is_some())) + .unwrap_or(false) + } + + fn complete_trick(&mut self) -> Result { + // Finish the current trick + let trick = self + .current_hand + .current_trick() + .as_ref() + .and_then(|ot| ot.clone().finish()) + .ok_or_else(|| Error::InternalError("Trick not complete".to_string()))?; + + // Determine winner using game rules + let winner = trick.taker(); + + // Add to current hand + let trick_index = self + .current_hand + .tricks() + .iter() + .position(|t| t.is_none()) + .ok_or_else(|| Error::InternalError("No space for trick".to_string()))?; + self.current_hand.add(trick, trick_index); + + // Update state + self.trick_leader = winner; + self.next_to_play = winner; + + // Start new trick + self.current_hand + .set_current_trick(Some(OngoingTrick::new(winner))); + + // Check if hand is complete (all cards played) + if self.hands.iter().all(|h| h.is_empty()) { + self.complete_hand(winner) + } else { + Ok(MoveEffect::TrickCompleted { winner }) + } + } + + fn complete_hand(&mut self, last_trick_winner: PlayerId) -> Result { + // Finish the current hand + let hand = std::mem::take(&mut self.current_hand) + .finish() + .ok_or_else(|| Error::InternalError("Hand not complete".to_string()))?; + + // Compute and update scores + let rules = TressetteRules {}; + let (score_0, score_1) = rules.score_hand(&hand); + self.score.0 += score_0; + self.score.1 += score_1; + + // Store completed hand + self.completed_hands.push(hand); + + self.hands_completed += 1; + + // Check if game is over + let rules = TressetteRules {}; + if rules.is_game_over(self.score) { + Ok(MoveEffect::GameOver { + trick_winner: last_trick_winner, + final_score: self.score, + }) + } else { + let score = self.score; + // Auto-deal next hand + self.deal(); + Ok(MoveEffect::HandComplete { + trick_winner: last_trick_winner, + score, + }) + } + } + + fn deal(&mut self) { + // Clear hands + for hand in &mut self.hands { + hand.clear(); + } + + // Shuffle and deal + let mut deck = Deck::italian(); + deck.shuffle(); + + let rules = TressetteRules {}; + let hand_size = rules.hand_size(); + let total_cards = PLAYERS * hand_size; + + for (i, card) in deck.iter().take(total_cards).cloned().enumerate() { + self.hands[i % PLAYERS].push(TressetteCard::from(card)); + } + + // Reset hand state + self.current_hand = OngoingHand::new(); + self.current_hand + .set_current_trick(Some(OngoingTrick::new(self.trick_leader))); + } +} + +impl Default for Game { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::Suit; + use crate::core::italian::ItalianRank; + use proptest::prelude::*; + + #[test] + fn new_game_starts_dealt() { + let game = Game::new(); + assert_eq!(game.score(), (0, 0)); + assert_eq!(game.hands_completed(), 0); + + // Each player should have 10 cards + for i in 0..PLAYERS { + assert_eq!( + game.hand(PlayerId::try_from(i).unwrap()).len(), + 10, + "Player {} should have 10 cards", + i + ); + } + } + + #[test] + fn can_make_legal_move() { + let mut game = Game::new(); + let legal = game.legal_cards(); + assert!(!legal.is_empty()); + + let initial_player = game.current_player(); + let effect = game.play_card(legal[0]).unwrap(); + assert!(matches!(effect, MoveEffect::CardPlayed)); + assert_ne!(game.current_player(), initial_player); + } + + #[test] + fn cannot_make_illegal_move() { + let mut game = Game::new(); + + // Set up a specific scenario + let hearts_ace = TressetteCard::new(ItalianRank::Ace, Suit::Hearts); + let spades_two = TressetteCard::new(ItalianRank::Two, Suit::Spades); + + game.hands[1] = vec![hearts_ace, spades_two]; + let mut ongoing_trick = OngoingTrick::new(PlayerId::PLAYER_0); + ongoing_trick.set_card(PlayerId::PLAYER_0, hearts_ace); + game.current_hand.set_current_trick(Some(ongoing_trick)); + game.next_to_play = PlayerId::PLAYER_1; + + // Player has hearts, must play hearts + let result = game.play_card(spades_two); + assert!(matches!(result, Err(Error::MustFollowSuit { .. }))); + } + + #[test] + fn game_records_history() { + let mut game = Game::new(); + + let legal1 = game.legal_cards(); + game.play_card(legal1[0]).unwrap(); + + let legal2 = game.legal_cards(); + game.play_card(legal2[0]).unwrap(); + + assert_eq!(game.history().len(), 2); + } + + proptest! { + #[test] + fn legal_cards_lead_to_valid_game(num_cards in 0..=TressetteRules::HAND_SIZE * PLAYERS) { + let mut game = Game::new(); + let mut cards_played = 0; + let mut prev_hand_sizes = [TressetteRules::HAND_SIZE; PLAYERS]; + let mut last_hands_completed = 0; + + while cards_played < num_cards && !matches!(game.status(), Status::Finished { .. }) { + let legal = game.legal_cards(); + if !legal.is_empty() { + game.play_card(legal[0]).unwrap(); + cards_played += 1; + + // Reset prev_hand_sizes if a new hand was dealt + if game.hands_completed() > last_hands_completed { + prev_hand_sizes = [TressetteRules::HAND_SIZE; PLAYERS]; + last_hands_completed = game.hands_completed(); + } + + // Making sure the score doesn't go crazy + prop_assert!(game.score().0 <= 50); + prop_assert!(game.score().1 <= 50); + + let mut total_hand_size = 0; + for player in 0..PLAYERS { + let hand_len = game.hand(PlayerId::try_from(player).unwrap()).len(); + // Hand size should decrease every time. + prop_assert!(hand_len <= prev_hand_sizes[player]); + prev_hand_sizes[player] = hand_len; + total_hand_size += hand_len; + } + // Making sure new cards don't come out of oblivion + prop_assert!(total_hand_size + cards_played <= 40 * (game.hands_completed() + 1)); + } else { + break; + } + } + } + } +} diff --git a/src/tressette/rules.rs b/src/tressette/rules.rs new file mode 100644 index 0000000..6112902 --- /dev/null +++ b/src/tressette/rules.rs @@ -0,0 +1,452 @@ +use crate::core::{ + Suit, + trick_taking::{Hand, OngoingTrick, PLAYERS, Player, PlayerId, TrickTakingGame}, +}; +use num_rational::Rational32; + +use super::card::TressetteCard; +use anyhow::anyhow; + +/// Errors that can occur in Tressette game rule validation. +#[derive(Debug, thiserror::Error)] +pub enum RulesError { + /// The specified card index is out of bounds for the player's hand. + #[error("Card index {index} is out of bounds (hand has {hand_size} cards)")] + CardIndexOutOfBounds { + /// The invalid index that was requested + index: usize, + /// The actual number of cards in the player's hand + hand_size: usize, + }, + /// The card cannot be played because it doesn't follow the required suit. + #[error("Must follow {required_suit} suit when led (played {played_card})")] + MustFollowSuit { + /// The suit that must be followed + required_suit: Suit, + /// The card that was attempted to be played + played_card: TressetteCard, + }, + /// An internal error occurred. + #[error(transparent)] + Internal(anyhow::Error), +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +/// Contains the rules of the tressette game. +pub struct TressetteRules {} + +impl TrickTakingGame for TressetteRules { + type CardType = TressetteCard; + + /// Contains the logic to determine who won the trick in a standard + /// tressette game: The winner of the trick is always the player who played + /// the highest card with the same `Suit` of the first `TressetteCard` + /// played that trick. See the implementation of `Ord` and `PartialOrd` for + /// `TressetteCard` for more info. The implementation of this trait is meant + /// to only be used internally by `OngoingTrick`, however it's possible to + /// call it elsewhere if needed. It also assumes the slice `cards` is valid + /// for the tressette game, so it assumes there are no duplicates. It's a + /// responsability of the caller to make sure that's the case. + /// + /// # Panics + /// + /// It can only panic in case of a bug in this crate. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::trick_taking::{TrickTakingGame, PlayerId}; + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::ItalianRank; + /// use shuftlib::tressette::{TressetteRules, TressetteCard}; + /// + /// let cards = [ + /// TressetteCard::new(ItalianRank::Ace, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Two, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Three, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Four, Suit::Hearts), + /// ]; + /// + /// let taker = TressetteRules::determine_taker(&cards, PlayerId::try_from(2).unwrap()); + /// assert_eq!(taker, PlayerId::try_from(2).unwrap()); + /// ``` + #[allow(clippy::expect_used)] + fn determine_taker(cards: &[TressetteCard; PLAYERS], first_to_play: PlayerId) -> PlayerId { + let leading_suit = cards[first_to_play.as_usize()].suit(); + let (taker, _) = cards + .iter() + .enumerate() + .filter(|&(_, &c)| c.suit() == leading_suit) + .max_by_key(|&(_, &c)| c) + .expect("Max by key returned None. This shouldn't have happened, since it's being called on a non empty slice."); + + PlayerId::try_from(taker).expect("Initialization of a new PlayerId failed. This shouldn't have happened, since the input usize was computed starting from a fixed length slice.") + } + + fn score_hand(&self, hand: &Hand) -> (u8, u8) { + let mut score = (0, 0); + Self::compute_score(hand, &mut score); + score + } + + fn is_game_over(&self, scores: (u8, u8)) -> bool { + Self::is_completed(scores) + } +} + +/// The score a team has to reach to win a game of tressette. +pub const SCORE_TO_WIN: u8 = 31; + +impl TressetteRules { + /// Number of cards dealt to each player at the start of a Tressette hand. + pub const HAND_SIZE: usize = 10; + /// Determines if a team won the game. A team wins the game when its score is + /// greater than 31 and has a higher score than the other team. + pub fn is_completed(score: (u8, u8)) -> bool { + (score.0 >= SCORE_TO_WIN && score.0 > score.1) + || (score.1 >= SCORE_TO_WIN && score.1 > score.0) + } + + /// Returns playable cards along with their indices in the player's hand. + /// + /// This is useful for index-based card selection in UIs where users click + /// on cards at specific positions. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::{TressetteRules, TressetteCard}; + /// use shuftlib::trick_taking::Player; + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::ItalianRank; + /// + /// let mut player = Player::default(); + /// player.give(TressetteCard::new(ItalianRank::Ace, Suit::Spades)); + /// player.give(TressetteCard::new(ItalianRank::Two, Suit::Hearts)); + /// + /// let playable = TressetteRules::playable(&player, None); + /// assert_eq!(playable.len(), 2); + /// assert_eq!(playable[0].0, 0); // index + /// assert_eq!(playable[1].0, 1); // index + /// ``` + pub fn playable( + player: &Player, + leading_suit: Option, + ) -> Vec<(usize, TressetteCard)> { + if let Some(leading_suit) = leading_suit + && player.hand().iter().any(|c| c.suit() == leading_suit) + { + return player + .hand() + .iter() + .enumerate() + .filter(|(_, c)| c.suit() == leading_suit) + .map(|(i, &card)| (i, card)) + .collect(); + } + + player + .hand() + .iter() + .enumerate() + .map(|(i, &card)| (i, card)) + .collect() + } + + /// Plays the card at the specified index for the player. + /// + /// This method validates that: + /// 1. The index is within bounds of the player's hand + /// 2. The card at that index is actually playable given the game rules + /// + /// This provides protection against cheating or UI bugs where an invalid + /// card might be selected. + /// + /// # Errors + /// + /// Returns an error if the index is out of bounds or if the card at that + /// index is not playable according to the current game rules. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::tressette::{TressetteRules, TressetteCard}; + /// use shuftlib::trick_taking::{Player, PlayerId, OngoingTrick}; + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::ItalianRank; + /// + /// let mut player = Player::default(); + /// player.give(TressetteCard::new(ItalianRank::Ace, Suit::Spades)); + /// let mut trick = OngoingTrick::::new(PlayerId::PLAYER_0); + /// + /// // Playing a valid index succeeds + /// let result = TressetteRules::play(&mut player, 0, &mut trick, None); + /// assert!(result.is_ok()); + /// ``` + pub fn play( + player: &mut Player, + index: usize, + ongoing_trick: &mut OngoingTrick, + leading_suit: Option, + ) -> Result { + let card = player + .hand() + .get(index) + .ok_or(RulesError::CardIndexOutOfBounds { + index, + hand_size: player.hand().len(), + })?; + + // Validate the card is actually playable + let is_playable = if let Some(leading_suit) = leading_suit { + // If there's a leading suit, check if player has any cards of that suit + let has_leading_suit = player.hand().iter().any(|c| c.suit() == leading_suit); + if has_leading_suit { + // Must play the leading suit + card.suit() == leading_suit + } else { + // No cards of leading suit, so any card is playable + true + } + } else { + // No leading suit, any card is playable + true + }; + + if !is_playable { + let required_suit = leading_suit.ok_or_else(|| { + RulesError::Internal(anyhow!("Leading suit should be Some if not playable")) + })?; + return Err(RulesError::MustFollowSuit { + required_suit, + played_card: *card, + }); + } + + let card = player.remove_card(index).ok_or_else(|| { + RulesError::Internal(anyhow!("Card removal should succeed after validation")) + })?; + ongoing_trick.play(card); + Ok(card) + } + + /// Computes the score for a hand of the tressette game. + /// Score is always a maximum of 11 points. + pub fn compute_score(hand: &Hand, score: &mut (u8, u8)) { + let mut tmp_score = (Rational32::new(0, 3), Rational32::new(0, 3)); + + let mut taker = 0; + for trick in hand.tricks() { + taker = trick.taker().as_usize(); + if taker == 0 || taker == 2 { + tmp_score.0 += trick.cards().iter().map(|c| c.value()).sum::(); + } else { + tmp_score.1 += trick.cards().iter().map(|c| c.value()).sum::(); + } + } + + score.0 += tmp_score.0.to_integer() as u8; + score.1 += tmp_score.1.to_integer() as u8; + + if taker == 0 || taker == 2 { + score.0 += 1; + } else { + score.1 += 1; + } + } +} + +#[cfg(test)] +mod test_utils { + use crate::core::{ + Suit, + italian::ItalianRank, + trick_taking::{Player, PlayerId}, + }; + use proptest::collection::hash_set; + use proptest::prelude::*; + + use super::TressetteRules; + use crate::tressette::TressetteCard; + + /// Strategy to create a random `TressetteCard`. + pub fn tressette_card_strategy() -> impl Strategy { + ( + prop_oneof![ + Just(ItalianRank::Ace), + Just(ItalianRank::Two), + Just(ItalianRank::Three), + Just(ItalianRank::Four), + Just(ItalianRank::Five), + Just(ItalianRank::Six), + Just(ItalianRank::Seven), + Just(ItalianRank::Jack), + Just(ItalianRank::Knight), + Just(ItalianRank::King), + ], + prop_oneof![ + Just(Suit::Hearts), + Just(Suit::Clubs), + Just(Suit::Spades), + Just(Suit::Diamonds), + ], + ) + .prop_map(|(rank, suit)| TressetteCard::new(rank, suit)) + } + + /// Strategy to create a `Player` with random cards. + pub fn player_strategy() -> impl Strategy> { + ( + 0..crate::trick_taking::PLAYERS, + hash_set(tressette_card_strategy(), 1..crate::trick_taking::TRICKS), + ) + .prop_map(|(index, cards)| { + let player_id = PlayerId::try_from(index).ok()?; + let mut player = Player::new(player_id); + for card in cards { + player.give(card); + } + Some(player) + }) + .prop_filter_map("valid player id", |opt| opt) + } +} + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + + use crate::core::Suit; + use crate::trick_taking::PlayerId; + + use super::{SCORE_TO_WIN, TressetteRules}; + + proptest! { + #[test] + fn a_team_won_with_both_below(team1_score in 0u8..SCORE_TO_WIN, team2_score in 0u8..SCORE_TO_WIN) { + let result = TressetteRules::is_completed((team1_score, team2_score)); + assert!(!result); + } + + #[test] + fn a_team_won_with_both_above_and_same(score in SCORE_TO_WIN..u8::MAX) { + let result = TressetteRules::is_completed((score, score)); + assert!(!result); + } + + #[test] + fn a_team_won_with_both_above_and_different(score in SCORE_TO_WIN..u8::MAX) { + let result = TressetteRules::is_completed((score, score + 1)); + assert!(result); + } + + #[test] + fn a_team_won_with_team1_above(team1_score in 0u8..SCORE_TO_WIN, team2_score in SCORE_TO_WIN..u8::MAX) { + let result = TressetteRules::is_completed((team1_score, team2_score)); + assert!(result); + } + + #[test] + fn a_team_won_with_team2_above(team1_score in SCORE_TO_WIN..u8::MAX, team2_score in 0u8..SCORE_TO_WIN ) { + let result = TressetteRules::is_completed((team1_score, team2_score)); + assert!(result); + } + + + + #[test] + fn playable_returns_correct_indices(player in super::test_utils::player_strategy(), suit in prop_oneof![Just(Suit::Hearts), Just(Suit::Spades), Just(Suit::Clubs), Just(Suit::Diamonds)]) { + let playable = TressetteRules::playable(&player, None); + + // All indices should be valid + for (index, card) in &playable { + prop_assert!(*index < player.hand().len()); + prop_assert_eq!(player.hand()[*index], *card); + } + + // When no leading suit, all cards should be playable + prop_assert_eq!(playable.len(), player.hand().len()); + + // Test with leading suit + let playable_suited = TressetteRules::playable(&player, Some(suit)); + let same_suit_cards = player.hand().iter().filter(|c| c.suit() == suit).count(); + + // All returned cards should be of the leading suit (if any exist) + if same_suit_cards > 0 { + for (index, card) in &playable_suited { + prop_assert_eq!(card.suit(), suit); + prop_assert_eq!(player.hand()[*index], *card); + } + prop_assert_eq!(playable_suited.len(), same_suit_cards); + } else { + // If no cards of leading suit, all cards are playable + prop_assert_eq!(playable_suited.len(), player.hand().len()); + } + } + + #[test] + fn remove_card_at_valid_index_removes_correct_card(player in super::test_utils::player_strategy(), index in 0usize..10) { + let mut player = player; + let original_len = player.hand().len(); + + if index < original_len { + let expected_card = player.hand()[index]; + let removed = player.remove_card(index); + + prop_assert_eq!(removed, Some(expected_card)); + prop_assert_eq!(player.hand().len(), original_len - 1); + // Card should no longer be in hand at that index + if index < player.hand().len() { + prop_assert_ne!(player.hand()[index], expected_card); + } + } else { + // Invalid index should not modify hand + let removed = player.remove_card(index); + prop_assert_eq!(removed, None); + prop_assert_eq!(player.hand().len(), original_len); + } + } + + #[test] + fn play_validates_playability(player in super::test_utils::player_strategy(), index in 0usize..10, suit in prop_oneof![Just(Suit::Hearts), Just(Suit::Spades), Just(Suit::Clubs), Just(Suit::Diamonds)]) { + let mut player = player; + let original_hand_len = player.hand().len(); + let mut trick = crate::trick_taking::OngoingTrick::::new(PlayerId::PLAYER_0); + + // Get playable indices + let playable = TressetteRules::playable(&player, Some(suit)); + let playable_indices: Vec = playable.iter().map(|(i, _)| *i).collect(); + + let result = TressetteRules::play(&mut player, index, &mut trick, Some(suit)); + + if index < original_hand_len && playable_indices.contains(&index) { + // Should succeed - valid and playable + prop_assert!(result.is_ok()); + prop_assert_eq!(player.hand().len(), original_hand_len - 1); + } else { + // Should fail - either out of bounds or not playable + prop_assert!(result.is_err()); + prop_assert_eq!(player.hand().len(), original_hand_len); + } + } + + #[test] + fn play_with_no_suit_constraint_allows_any_card(player in super::test_utils::player_strategy(), index in 0usize..10) { + let mut player = player; + let original_hand_len = player.hand().len(); + let mut trick = crate::trick_taking::OngoingTrick::::new(PlayerId::PLAYER_0); + + let result = TressetteRules::play(&mut player, index, &mut trick, None); + + if index < original_hand_len { + // Should succeed - any card is playable when no suit constraint + prop_assert!(result.is_ok()); + prop_assert_eq!(player.hand().len(), original_hand_len - 1); + } else { + // Should fail - out of bounds + prop_assert!(result.is_err()); + prop_assert_eq!(player.hand().len(), original_hand_len); + } + } + } +} diff --git a/src/trick_taking.rs b/src/trick_taking.rs new file mode 100644 index 0000000..be3d80c --- /dev/null +++ b/src/trick_taking.rs @@ -0,0 +1,64 @@ +//! Trick-taking game mechanics including players, tricks, and hands. +//! +//! This module contains the fundamental building blocks for trick-taking card games: +//! - Generic player management and identification +//! - Trick and ongoing trick state management +//! - Hand completion and scoring mechanics +//! - Core trait for implementing different trick-taking games + +use crate::core::Card; + +/// Number of tricks in all games. +pub const TRICKS: usize = 10; + +/// Trait for trick-taking games. +/// +/// This trait provides a foundation for implementing different trick-taking card games. +pub trait TrickTakingGame { + /// Define the type of card that's going to be used in this game. + type CardType: Card; + + /// Every trick taking game has some logic to determine the winner (or + /// taker) of the trick. The taker is generally determined by the cards that + /// have been played and it can depend by the order in which the players + /// played their cards. + fn determine_taker(cards: &[Self::CardType; PLAYERS], first_to_play: PlayerId) -> PlayerId; + + /// Calculate the score for a completed hand. + /// + /// Returns the updated scores for both teams: (team_0_2_score, team_1_3_score). + /// Default implementation returns unchanged scores. + fn score_hand(&self, _hand: &Hand) -> (u8, u8) + where + Self: Sized, + { + (0, 0) + } + + /// Check if the game is over based on current scores. + /// + /// Returns true if the game should end with the given scores. + /// Default implementation never ends the game. + fn is_game_over(&self, _scores: (u8, u8)) -> bool { + false + } + + /// Get the number of cards dealt to each player at the start of a hand. + /// + /// Default is 10 cards (standard for Tressette). + fn hand_size(&self) -> usize { + 10 + } +} + +/// Generic player management and identification. +pub mod player; +pub use player::{PLAYERS, Player, PlayerId}; + +/// Trick and ongoing trick state management. +pub mod trick; +pub use trick::{OngoingTrick, Trick}; + +/// Hand completion and scoring mechanics. +pub mod hand; +pub use hand::{Hand, OngoingHand}; diff --git a/src/trick_taking/hand.rs b/src/trick_taking/hand.rs new file mode 100644 index 0000000..9be8e34 --- /dev/null +++ b/src/trick_taking/hand.rs @@ -0,0 +1,230 @@ +use crate::trick_taking::{OngoingTrick, TRICKS, Trick, TrickTakingGame}; + +/// Various games are usually played multiple times, until one team reaches a +/// certain score. These "multiple times" are called hands: "We played a game of +/// tressette and our team won in just 2 hands!". +/// +/// This type is generic over the +/// actual card type, the number of players allowed and the number of tricks it +/// takes to finish the hand. +#[derive(Debug, Clone, Copy)] +pub struct Hand +where + G: TrickTakingGame, +{ + tricks: [Trick; TRICKS], +} + +impl Hand +where + G: TrickTakingGame, +{ + /// Creates a new Hand from an array of tricks. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::trick_taking::{Hand, Trick, PlayerId}; + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::ItalianRank; + /// use shuftlib::tressette::{TressetteRules, TressetteCard}; + /// + /// let cards = [ + /// TressetteCard::new(ItalianRank::Ace, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Two, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Three, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Four, Suit::Hearts), + /// ]; + /// let trick = Trick::::new(cards, PlayerId::PLAYER_0); + /// let tricks = std::array::from_fn(|_| trick.clone()); + /// let hand = Hand::::new(tricks); + /// ``` + pub fn new(tricks: [Trick; TRICKS]) -> Self { + Self { tricks } + } + + /// Returns a reference to the tricks of this [`Hand`]. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::trick_taking::{Hand, Trick, PlayerId}; + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::ItalianRank; + /// use shuftlib::tressette::{TressetteRules, TressetteCard}; + /// + /// let cards = [ + /// TressetteCard::new(ItalianRank::Ace, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Two, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Three, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Four, Suit::Hearts), + /// ]; + /// let trick = Trick::::new(cards, PlayerId::PLAYER_0); + /// let tricks = std::array::from_fn(|_| trick.clone()); + /// let hand = Hand::::new(tricks); + /// assert_eq!(hand.tricks().len(), 10); + /// ``` + pub fn tricks(&self) -> &[Trick; TRICKS] { + &self.tricks + } +} + +/// A hand takes multiple turns for each player to be completed, this is the +/// representation of a `Hand` which hasn't been completed yet. +#[derive(Clone, Copy, Debug)] +pub struct OngoingHand +where + G: TrickTakingGame, +{ + current_trick: Option>, + index: usize, + tricks: [Option>; TRICKS], +} + +impl OngoingHand +where + G: TrickTakingGame, +{ + /// Returns the current trick of this [`OngoingHand`]. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::trick_taking::OngoingHand; + /// use shuftlib::tressette::TressetteRules; + /// + /// let hand = OngoingHand::::new(); + /// assert!(hand.current_trick().is_none()); + /// ``` + pub fn current_trick(&self) -> &Option> { + &self.current_trick + } + + /// Returns a reference to the tricks of this [`OngoingHand`]. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::trick_taking::OngoingHand; + /// use shuftlib::tressette::TressetteRules; + /// + /// let hand = OngoingHand::::new(); + /// assert!(hand.tricks().iter().all(|t| t.is_none())); + /// ``` + pub fn tricks(&self) -> &[Option>; TRICKS] { + &self.tricks + } + + /// Returns the index of this [`OngoingHand`]. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::trick_taking::OngoingHand; + /// use shuftlib::tressette::TressetteRules; + /// + /// let hand = OngoingHand::::new(); + /// assert_eq!(hand.index(), 0); + /// ``` + pub fn index(&self) -> usize { + self.index + } + + /// Transforms an `OngoingHand` into a `Hand`, a read-only data structure + /// used to just store the information related to a hand that has been played. + /// + /// Returns `None` if not all tricks have been completed. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::trick_taking::{OngoingHand, Hand, Trick, PlayerId}; + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::ItalianRank; + /// use shuftlib::tressette::{TressetteRules, TressetteCard}; + /// + /// let mut ongoing_hand = OngoingHand::::new(); + /// // Add tricks... + /// // let finished = ongoing_hand.finish(); // Would be None if incomplete + /// ``` + pub fn finish(self) -> Option> { + if self.tricks.iter().any(|t| t.is_none()) { + return None; + } + + let tricks: [Trick; TRICKS] = self + .tricks + .into_iter() + .flatten() + .collect::>() + .try_into() + .ok()?; + Some(Hand { tricks }) + } + + /// Constructor for `OngoingHand`. All the internal fields are initialized + /// as empty or None. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::trick_taking::OngoingHand; + /// use shuftlib::tressette::TressetteRules; + /// + /// let ongoing_hand = OngoingHand::::new(); + /// + /// assert_eq!(ongoing_hand.index(), 0); + /// assert!(ongoing_hand.current_trick().is_none()); + /// ongoing_hand.tricks().iter().for_each(|t| assert!(t.is_none())); + /// ``` + pub fn new() -> Self { + Self { + tricks: [const { None }; TRICKS], + current_trick: None, + index: 0, + } + } + + /// Adds a trick to this hand. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::trick_taking::{OngoingHand, Trick, PlayerId}; + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::ItalianRank; + /// use shuftlib::tressette::{TressetteRules, TressetteCard}; + /// + /// let mut hand = OngoingHand::::new(); + /// let cards = [ + /// TressetteCard::new(ItalianRank::Ace, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Two, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Three, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Four, Suit::Hearts), + /// ]; + /// let trick = Trick::::new(cards, PlayerId::PLAYER_0); + /// hand.add(trick, 0); + /// ``` + pub fn add(&mut self, trick: Trick, id: usize) { + self.tricks[id] = Some(trick); + } + + /// Returns a mutable reference to the current trick of this [`OngoingHand`]. + pub fn current_trick_mut(&mut self) -> &mut Option> { + &mut self.current_trick + } + + /// Sets the current trick of this [`OngoingHand`]. + pub fn set_current_trick(&mut self, trick: Option>) { + self.current_trick = trick; + } +} + +impl Default for OngoingHand +where + G: TrickTakingGame, +{ + fn default() -> Self { + Self::new() + } +} diff --git a/src/trick_taking/player.rs b/src/trick_taking/player.rs new file mode 100644 index 0000000..f3231f3 --- /dev/null +++ b/src/trick_taking/player.rs @@ -0,0 +1,230 @@ +use std::fmt::Display; + +use anyhow::bail; + +use crate::trick_taking::TrickTakingGame; + +/// Number of players in all games. +pub const PLAYERS: usize = 4; + +/// Represents a player of a game. This type is generic over the type of the +/// card used for the specific game. +#[derive(Clone, Default, Debug)] +pub struct Player +where + G: TrickTakingGame, +{ + /// The cards held in the player's hand. + hand: Vec, + /// The ID of this player. + id: PlayerId, +} + +impl Player +where + G: TrickTakingGame, +{ + /// Adds a card to the hand of the player. + /// + /// # Examples + /// ``` + /// use shuftlib::trick_taking::{Player, TrickTakingGame, PlayerId}; + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::ItalianRank; + /// use shuftlib::tressette::{TressetteRules, TressetteCard}; + /// + /// let player_id = PlayerId::PLAYER_0; + /// let mut player = Player::::new(player_id); + /// // Players have no cards when created. + /// assert_eq!(player.hand().len(), 0); + /// + /// let card = TressetteCard::new(ItalianRank::Ace, Suit::Spades); + /// player.give(card); + /// assert_eq!(player.hand().len(), 1); + /// ``` + pub fn give(&mut self, card: G::CardType) { + self.hand.push(card); + } + + /// Removes a card at the specified index from the player's hand. + /// + /// Returns `Some(card)` if the index is valid, `None` otherwise. + /// The hand is not modified if the index is out of bounds. + /// + /// # Examples + /// ``` + /// use shuftlib::trick_taking::{Player, TrickTakingGame, PlayerId}; + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::ItalianRank; + /// use shuftlib::tressette::{TressetteRules, TressetteCard}; + /// + /// let player_id = PlayerId::PLAYER_0; + /// let mut player = Player::::new(player_id); + /// + /// let card1 = TressetteCard::new(ItalianRank::Ace, Suit::Spades); + /// let card2 = TressetteCard::new(ItalianRank::King, Suit::Hearts); + /// player.give(card1); + /// player.give(card2); + /// + /// // Remove card at index 0 + /// let removed = player.remove_card(0); + /// assert_eq!(removed, Some(card1)); + /// assert_eq!(player.hand().len(), 1); + /// + /// // Try to remove at invalid index + /// let removed = player.remove_card(10); + /// assert_eq!(removed, None); + /// assert_eq!(player.hand().len(), 1); + /// ``` + pub fn remove_card(&mut self, index: usize) -> Option { + if index < self.hand.len() { + Some(self.hand.remove(index)) + } else { + None + } + } + + /// Returns the cards held by this player. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::trick_taking::{Player, PlayerId}; + /// use shuftlib::tressette::TressetteRules; + /// + /// let player = Player::::new(PlayerId::PLAYER_0); + /// assert_eq!(player.hand().len(), 0); + /// ``` + pub fn hand(&self) -> &[G::CardType] { + &self.hand + } + + /// Returns the ID of this player. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::trick_taking::{Player, PlayerId}; + /// use shuftlib::tressette::TressetteRules; + /// + /// let player = Player::::new(PlayerId::PLAYER_0); + /// assert_eq!(player.id(), PlayerId::PLAYER_0); + /// ``` + pub fn id(&self) -> PlayerId { + self.id + } + + /// Generates a new player from a `PlayerId`. Players are initialized with + /// no cards. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::{trick_taking::{Player, PlayerId, TrickTakingGame}, tressette::TressetteRules}; + /// + /// let id = PlayerId::PLAYER_0; + /// let player = Player::::new(id); + /// + /// assert_eq!(player.id().as_usize(), 0); + /// assert_eq!(player.hand().len(), 0); + /// ``` + pub fn new(id: PlayerId) -> Self { + Self { + id, + hand: Vec::new(), + } + } +} + +/// A player id can only be in the range 0..4. +#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] +pub struct PlayerId(usize); + +impl PlayerId { + /// Player ID constant for player 0. + pub const PLAYER_0: Self = PlayerId(0); + /// Player ID constant for player 1. + pub const PLAYER_1: Self = PlayerId(1); + /// Player ID constant for player 2. + pub const PLAYER_2: Self = PlayerId(2); + /// Player ID constant for player 3. + pub const PLAYER_3: Self = PlayerId(3); + + /// This method simply increments `self` by 1. Note that `PlayerId` can only + /// be in the range 0..N, so incrementing `self` when the value is N-1, will + /// reset its value to 0, since the purpose of this type is to determine the + /// player's turn and the first person to play is not necessarily the person + /// with ID = 0. + /// + /// # Examples + /// ``` + /// use shuftlib::trick_taking::PlayerId; + /// + /// let mut player_id = PlayerId::PLAYER_0; + /// player_id.inc(); + /// assert_eq!(player_id, PlayerId::PLAYER_1); + /// player_id.inc(); + /// player_id.inc(); + /// player_id.inc(); + /// assert_eq!(player_id, PlayerId::PLAYER_0); + /// ``` + pub fn inc(&mut self) { + if self.0 < PLAYERS - 1 { + self.0 += 1; + } else { + self.0 = 0; + } + } + + /// Returns the next player ID by incrementing this one. + /// Note that `PlayerId` wraps around from N-1 to 0. + /// + /// # Examples + /// ``` + /// use shuftlib::trick_taking::PlayerId; + /// + /// let player_id = PlayerId::PLAYER_0; + /// assert_eq!(player_id.next(), PlayerId::PLAYER_1); + /// + /// let last_player = PlayerId::PLAYER_3; + /// assert_eq!(last_player.next(), PlayerId::PLAYER_0); + /// ``` + pub fn next(mut self) -> Self { + self.inc(); + self + } + + /// Returns the underlying usize value of this PlayerId. + /// + /// # Examples + /// ``` + /// use shuftlib::trick_taking::PlayerId; + /// + /// let player_id = PlayerId::PLAYER_2; + /// assert_eq!(player_id.as_usize(), 2); + /// ``` + pub fn as_usize(&self) -> usize { + self.0 + } +} + +impl TryFrom for PlayerId { + type Error = anyhow::Error; + + fn try_from(value: usize) -> Result { + if (0..PLAYERS).contains(&value) { + Ok(PlayerId(value)) + } else { + bail!( + "Tried to convert {} into a PlayerId, but acceptable values are in range 0..PLAYERS", + value + ) + } + } +} + +impl Display for PlayerId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/src/trick_taking/trick.rs b/src/trick_taking/trick.rs new file mode 100644 index 0000000..cf13972 --- /dev/null +++ b/src/trick_taking/trick.rs @@ -0,0 +1,404 @@ +use std::{fmt::Display, ops::Deref}; + +use super::{PLAYERS, PlayerId, TrickTakingGame}; + +/// A trick is a set containing the cards played and the player who won the +/// trick, represented as `PlayerId`. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Trick +where + G: TrickTakingGame, +{ + cards: [G::CardType; PLAYERS], + taker: PlayerId, +} + +impl Display for Trick +where + G: TrickTakingGame, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} {} {} {} {}", + self.cards[0], self.cards[1], self.cards[2], self.cards[3], self.taker + ) + } +} + +impl Trick +where + G: TrickTakingGame, +{ + /// Creates a new Trick from cards and the player who won. + pub fn new(cards: [G::CardType; PLAYERS], taker: PlayerId) -> Self { + Self { cards, taker } + } + + /// Returns the card this trick has been won with. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::trick_taking::{Trick, PlayerId}; + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::ItalianRank; + /// use shuftlib::tressette::{TressetteRules, TressetteCard}; + /// + /// let cards = [ + /// TressetteCard::new(ItalianRank::Ace, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Two, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Three, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Four, Suit::Hearts), + /// ]; + /// let trick = Trick::::new(cards, PlayerId::PLAYER_0); + /// assert_eq!(trick.taken_with(), cards[0]); + /// ``` + pub fn taken_with(&self) -> G::CardType { + self.cards[self.taker.as_usize()] + } + + /// Returns the `PlayerId` of the player who won the trick. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::trick_taking::{Trick, PlayerId}; + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::ItalianRank; + /// use shuftlib::tressette::{TressetteRules, TressetteCard}; + /// + /// let cards = [ + /// TressetteCard::new(ItalianRank::Ace, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Two, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Three, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Four, Suit::Hearts), + /// ]; + /// let trick = Trick::::new(cards, PlayerId::PLAYER_0); + /// assert_eq!(trick.taker(), PlayerId::PLAYER_0); + /// ``` + pub fn taker(&self) -> PlayerId { + self.taker + } + + /// Returns the cards played during this trick. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::trick_taking::{Trick, PlayerId}; + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::ItalianRank; + /// use shuftlib::tressette::{TressetteRules, TressetteCard}; + /// + /// let cards = [ + /// TressetteCard::new(ItalianRank::Ace, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Two, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Three, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Four, Suit::Hearts), + /// ]; + /// let trick = Trick::::new(cards, PlayerId::PLAYER_0); + /// assert_eq!(trick.cards(), &cards); + /// ``` + pub fn cards(&self) -> &[G::CardType] { + &self.cards + } +} + +/// A temporary state of a trick that's still not over: not all the players made +/// their move or a taker hasn't been determined yet. +#[derive(Clone, Copy, Debug)] +pub struct OngoingTrick +where + G: TrickTakingGame, +{ + cards: [Option; PLAYERS], + first_to_play: PlayerId, + next_to_play: PlayerId, + play_count: usize, +} + +impl Deref for OngoingTrick +where + G: TrickTakingGame, +{ + type Target = [Option; PLAYERS]; + + fn deref(&self) -> &Self::Target { + &self.cards + } +} + +impl OngoingTrick +where + G: TrickTakingGame, +{ + /// Adds the `Card` passed as parameter to the `OngoingTrick`. + /// Checking the validity of the card played is a responsibility of the + /// caller. + /// + /// # Examples + /// ``` + /// use shuftlib::trick_taking::{OngoingTrick, PlayerId, TrickTakingGame}; + /// use shuftlib::core::{Card, Suit}; + /// use shuftlib::core::italian::ItalianRank; + /// use shuftlib::tressette::{TressetteRules, TressetteCard}; + /// + /// let first_to_play = PlayerId::PLAYER_0; + /// let card = TressetteCard::new(ItalianRank::Ace, Suit::Hearts); + /// let mut trick = OngoingTrick::::new(first_to_play); + /// trick.play(card); + /// let mut second_to_play = first_to_play; + /// second_to_play.inc(); + /// + /// assert_eq!(trick[0], Some(card)); + /// assert_eq!(trick.next_to_play(), second_to_play) + /// ``` + pub fn play(&mut self, card: G::CardType) { + self.cards[self.next_to_play.as_usize()] = Some(card); + self.next_to_play.inc(); + self.play_count += 1; + } + + /// Sets a card for a specific player in the trick. + /// This is primarily for testing or setup purposes. + pub fn set_card(&mut self, player: PlayerId, card: G::CardType) { + self.cards[player.as_usize()] = Some(card); + self.play_count += 1; + self.next_to_play = player.next(); + } + + /// Tries to transform the current `OngoingTrick` into a `Trick` by + /// determining the taker of the trick. It doesn't make any assumption on + /// previously played cards during the current `OngoingHand`. It also does + /// not check if it contains duplicates since that could be valid in some games. + /// + /// # Errors + /// + /// Fails if any of the moves of the `OngoingTrick` this is called on is + /// None. It means that not all players made their move yet, so a taker + /// can't be determined. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::trick_taking::{OngoingTrick, PlayerId, TrickTakingGame}; + /// use shuftlib::core::Suit; + /// use shuftlib::core::italian::ItalianRank; + /// use shuftlib::tressette::{TressetteRules, TressetteCard}; + /// + /// let cards = [ + /// TressetteCard::new(ItalianRank::Ace, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Two, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Three, Suit::Hearts), + /// TressetteCard::new(ItalianRank::Four, Suit::Hearts), + /// ]; + /// let first_to_play = PlayerId::PLAYER_0; + /// let mut ongoing_trick = OngoingTrick::::new(first_to_play); + /// ongoing_trick.play(cards[0]); + /// + /// // After only playing a card, it's not possible to finish the OngoingTrick. + /// assert!(ongoing_trick.clone().finish().is_none()); + /// + /// let mut to_play = first_to_play; + /// to_play.inc(); + /// cards.iter().skip(1).for_each(|&c| { + /// ongoing_trick.play(c); + /// to_play.inc(); + /// }); + /// + /// // After every player made their play, it's possible to get the trick. + /// let trick = ongoing_trick.finish().unwrap(); + /// // Finishing the trick also means determining a taker. Since in this + /// // example we are using the tressette game rules, player 2 is the taker. + /// assert_eq!(trick.taker(), PlayerId::PLAYER_2); + /// ``` + pub fn finish(self) -> Option> { + let mut cards: [G::CardType; PLAYERS] = [G::CardType::default(); PLAYERS]; + if self + .iter() + .enumerate() + .map(|(i, &x)| { + if let Some(c) = x { + cards[i] = c; + true + } else { + false + } + }) + .any(|is_some| !is_some) + { + return None; + } + + let taker = G::determine_taker(&cards, self.first_to_play); + Some(Trick { cards, taker }) + } + + /// Returns the cards contained in this `OngoingTrick`. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::core::trick_taking::{OngoingTrick, PlayerId}; + /// use shuftlib::tressette::TressetteRules; + /// + /// let trick = OngoingTrick::::new(PlayerId::PLAYER_0); + /// assert!(trick.cards().iter().all(|c| c.is_none())); + /// ``` + pub fn cards(&self) -> &[Option] { + &self.cards + } + + /// Returns the id of the person who starts the trick. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::core::trick_taking::{OngoingTrick, PlayerId}; + /// use shuftlib::tressette::TressetteRules; + /// + /// let trick = OngoingTrick::::new(PlayerId::PLAYER_0); + /// assert_eq!(trick.first_to_play(), PlayerId::PLAYER_0); + /// ``` + pub fn first_to_play(&self) -> PlayerId { + self.first_to_play + } + + /// Returns the id of the person who plays next in the trick. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::core::trick_taking::{OngoingTrick, PlayerId}; + /// use shuftlib::tressette::TressetteRules; + /// + /// let trick = OngoingTrick::::new(PlayerId::PLAYER_0); + /// assert_eq!(trick.next_to_play(), PlayerId::PLAYER_0); + /// ``` + pub fn next_to_play(&self) -> PlayerId { + self.next_to_play + } + + /// Creates a new `OngoingTrick` with all card slots empty, starting with + /// the specified player. + /// + /// # Examples + /// + /// ``` + /// use shuftlib::core::trick_taking::{OngoingTrick, PlayerId, TrickTakingGame}; + /// use shuftlib::tressette::TressetteRules; + /// + /// let first_to_play = PlayerId::PLAYER_0; + /// let ongoing_trick = OngoingTrick::::new(first_to_play); + /// + /// assert_eq!(ongoing_trick.first_to_play(), first_to_play); + /// ongoing_trick.cards().iter().for_each(|&c| assert!(c.is_none())); + /// ``` + pub fn new(first_to_play: PlayerId) -> Self { + Self { + cards: [None; PLAYERS], + first_to_play, + next_to_play: first_to_play, + play_count: 0, + } + } +} + +#[cfg(test)] +mod test_utils { + use proptest::collection::hash_set; + use proptest::prelude::*; + + use crate::core::Suit; + use crate::core::italian::{ItalianCard, ItalianRank}; + + use crate::trick_taking::{OngoingTrick, PlayerId, TrickTakingGame}; + + /// Strategy to create a random `ItalianCard`. + pub fn italian_card_strategy() -> impl Strategy { + ( + prop_oneof![ + Just(ItalianRank::Ace), + Just(ItalianRank::Two), + Just(ItalianRank::Three), + Just(ItalianRank::Four), + Just(ItalianRank::Five), + Just(ItalianRank::Six), + Just(ItalianRank::Seven), + Just(ItalianRank::Jack), + Just(ItalianRank::Knight), + Just(ItalianRank::King), + ], + prop_oneof![ + Just(Suit::Hearts), + Just(Suit::Clubs), + Just(Suit::Spades), + Just(Suit::Diamonds), + ], + ) + .prop_map(|(rank, suit)| ItalianCard::new(rank, suit)) + } + + #[derive(Clone, Copy, Debug)] + pub struct TestGame {} + + impl TrickTakingGame for TestGame { + type CardType = ItalianCard; + + fn determine_taker( + _cards: &[Self::CardType; super::super::PLAYERS], + _first_to_play: super::super::PlayerId, + ) -> super::super::PlayerId { + PlayerId::PLAYER_0 + } + } + + /// Strategy to create an `OngoingTrick` filled with random cards. + pub fn ongoing_trick_strategy() -> impl Strategy> { + hash_set(italian_card_strategy(), super::super::PLAYERS).prop_map(|hash_set| { + let mut cards = [None; super::super::PLAYERS]; + hash_set + .iter() + .enumerate() + .for_each(|(i, &c)| cards[i] = Some(c)); + + OngoingTrick { + cards, + first_to_play: PlayerId::PLAYER_0, + next_to_play: PlayerId::PLAYER_0, + play_count: 0, + } + }) + } +} + +#[cfg(test)] +mod tests { + use proptest::{array, prelude::*}; + + use super::test_utils::{TestGame, italian_card_strategy, ongoing_trick_strategy}; + use super::{OngoingTrick, PlayerId}; + + proptest! { + #[test] + fn play_method_works(cards in array::uniform4(italian_card_strategy())) { + let mut trick: OngoingTrick = OngoingTrick::new(PlayerId::PLAYER_0); + + for (index, &card) in cards.iter().enumerate() { + trick.play(card); + assert_eq!(trick[index], Some(card)); + } + } + + #[test] + fn finish_method_works(ongoing_trick in ongoing_trick_strategy()) { + let trick = ongoing_trick.finish().ok_or_else(|| TestCaseError::fail("trick should be complete"))?; + + let cards = ongoing_trick.cards(); + + prop_assert_eq!(trick.taker(), PlayerId::PLAYER_0); + prop_assert_eq!(trick.taken_with(), cards[0].ok_or_else(|| TestCaseError::fail("card should exist"))?); + } + } +} diff --git a/tests/tressette.rs b/tests/tressette.rs index ad5a670..eb157f6 100644 --- a/tests/tressette.rs +++ b/tests/tressette.rs @@ -1,58 +1,60 @@ -#![feature(generic_const_exprs)] -use shuftlib::{ - common::{ - cards::Deck, - hands::{OngoingHand, OngoingTrick, Player, PlayerId, TrickTakingGame}, - }, - tressette::{self, TressetteCard, TressetteRules}, -}; +#![allow(missing_docs)] +#![allow(clippy::expect_used)] + +use shuftlib::tressette::{Game, MoveEffect, Status}; #[test] -#[allow(clippy::unwrap_used)] fn tressette_works() { - let mut first = true; - let mut leading_suit = None; - let first_to_play = PlayerId::new(0).unwrap(); - let mut score = (0, 0); - let mut players = [ - Player::new(PlayerId::new(0).unwrap()), - Player::new(PlayerId::new(1).unwrap()), - Player::new(PlayerId::new(2).unwrap()), - Player::new(PlayerId::new(3).unwrap()), - ]; - let mut hands = Vec::new(); - - while !TressetteRules::is_completed(score) { - let mut ongoing_hand = OngoingHand::::new(); - let mut deck = Deck::italian(); - deck.shuffle(); + let mut game = Game::new(); - for (i, &card) in deck.iter().enumerate() { - let player_index = (i / 5) % TressetteRules::PLAYERS; - players[player_index].give(TressetteCard::from(card)); - } + while !matches!(game.status(), Status::Finished { .. }) { + let legal_cards = game.legal_cards(); + assert!( + !legal_cards.is_empty(), + "Should always have legal cards when game is ongoing" + ); - for trick_id in 0..TressetteRules::TRICKS { - let mut ongoing_trick = OngoingTrick::::new(first_to_play); - for _ in 0..TressetteRules::PLAYERS { - let next_to_play = ongoing_trick.next_to_play(); - let playable = TressetteRules::playable(&players[*next_to_play], leading_suit); - TressetteRules::play(&mut players[*next_to_play], playable[0], &mut ongoing_trick); + // Always pick the first legal card (simple strategy for testing) + let chosen_card = legal_cards[0]; + let effect = game + .play_card(chosen_card) + .expect("Legal card should succeed"); - if first { - leading_suit = Some(playable[0].suit()); - first = !first; - } + match effect { + MoveEffect::CardPlayed => { + // Continue playing + } + MoveEffect::TrickCompleted { winner: _ } => { + // Trick completed, continue to next trick + } + MoveEffect::HandComplete { + trick_winner: _, + score, + } => { + // Hand completed, scores updated, new hand auto-dealt + assert_eq!( + (score.0 + score.1) % 11, + 0, + "Scores should always sum to multiple of 11" + ); + } + MoveEffect::GameOver { + trick_winner: _, + final_score, + } => { + // Game is over + assert_eq!((final_score.0 + final_score.1) % 11, 0); + assert_ne!(final_score.0, final_score.1); + assert!( + final_score.0 >= shuftlib::tressette::SCORE_TO_WIN + || final_score.1 >= shuftlib::tressette::SCORE_TO_WIN + ); } - first = !first; - ongoing_hand.add(ongoing_trick.finish().unwrap(), trick_id); } - let hand = ongoing_hand.finish().unwrap(); - TressetteRules::compute_score(&hand, &mut score); - hands.push(hand); } - assert_eq!((score.0 + score.1) % 11, 0); - assert_ne!(score.0, score.1); - assert!(score.0 >= tressette::SCORE_TO_WIN || score.1 >= tressette::SCORE_TO_WIN); + // Verify final state + if let Status::Finished { winner } = game.status() { + assert!(winner.is_some(), "Tressette should never end in a draw"); + } } From 491d90269fe1ab572dd04fe3d884315fc28336dc Mon Sep 17 00:00:00 2001 From: Sebastiano Giordano <46520354+Krahos@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:09:14 +0100 Subject: [PATCH 3/7] feat: Add draw_n method to Deck and derive Hash for PlayerId --- src/core.rs | 1 + src/core/deck.rs | 40 ++++++++++++++++++++++++++++++++++++++ src/trick_taking/player.rs | 2 +- 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/core.rs b/src/core.rs index 32d41c6..a988586 100644 --- a/src/core.rs +++ b/src/core.rs @@ -6,6 +6,7 @@ use std::fmt::{Debug, Display}; use strum::EnumIter; +pub use strum::IntoEnumIterator; /// Italian card deck types and ranks. pub mod italian; diff --git a/src/core/deck.rs b/src/core/deck.rs index bf3503e..78b98dc 100644 --- a/src/core/deck.rs +++ b/src/core/deck.rs @@ -202,6 +202,33 @@ impl Deck { self.cards.pop() } + /// Draws `n` cards from the top of the deck, if enough cards remain. + /// + /// Returns `Some` iterator over the drawn cards if the deck contains at least `n` cards, + /// or `None` if there are not enough cards left (the deck is unchanged in that case). + /// + /// # Examples + /// ``` + /// use shuftlib::core::deck::Deck; + /// use shuftlib::core::italian::ItalianCard; + /// + /// let mut deck = Deck::italian(); + /// let drawn = deck.draw_n(3).unwrap().collect::>(); + /// assert_eq!(drawn.len(), 3); + /// assert_eq!(deck.len(), 37); + /// ``` + pub fn draw_n(&mut self, n: usize) -> Option> { + if self.cards.len() < n { + None + } else { + let mut drawn = Vec::with_capacity(n); + for _ in 0..n { + drawn.push(self.cards.pop().unwrap()); + } + Some(drawn.into_iter()) + } + } + /// Creates a new empty deck. /// /// # Examples @@ -413,6 +440,19 @@ mod tests { assert!(deck.is_empty()); } + #[test] + fn draw_n_basic() { + let mut deck = Deck::italian(); + let drawn: Vec<_> = deck.draw_n(5).unwrap().collect(); + assert_eq!(drawn.len(), 5); + assert_eq!(deck.len(), 35); + + // Drawing more than available returns None and does not change the deck + let mut deck = Deck::italian(); + assert!(deck.draw_n(41).is_none()); + assert_eq!(deck.len(), 40); + } + proptest! { #[test] fn shuffle_preserves_cards( diff --git a/src/trick_taking/player.rs b/src/trick_taking/player.rs index f3231f3..d35f8d8 100644 --- a/src/trick_taking/player.rs +++ b/src/trick_taking/player.rs @@ -137,7 +137,7 @@ where } /// A player id can only be in the range 0..4. -#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] +#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)] pub struct PlayerId(usize); impl PlayerId { From ba8991332e4cdc963dc40c2f8d486ece70cbf77f Mon Sep 17 00:00:00 2001 From: Sebastiano Giordano <46520354+Krahos@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:14:39 +0100 Subject: [PATCH 4/7] chore: updating dependencies and lints --- Cargo.lock | 456 +++++++++++++++++------- Cargo.toml | 10 +- proptest-regressions/core/deck.txt | 12 - proptest-regressions/tressette/game.txt | 7 - src/core/deck.rs | 4 +- 5 files changed, 335 insertions(+), 154 deletions(-) delete mode 100644 proptest-regressions/core/deck.txt delete mode 100644 proptest-regressions/tressette/game.txt diff --git a/Cargo.lock b/Cargo.lock index a0be75c..824b10f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "array-init" @@ -37,15 +37,15 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bon" -version = "3.6.4" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61138465baf186c63e8d9b6b613b508cd832cba4ce93cf37ce5f096f91ac1a6" +checksum = "2d13a61f2963b88eef9c1be03df65d42f6996dfeac1054870d950fcf66686f83" dependencies = [ "bon-macros", "rustversion", @@ -53,9 +53,9 @@ dependencies = [ [[package]] name = "bon-macros" -version = "3.6.4" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40d1dad34aa19bf02295382f08d9bc40651585bd497266831d40ee6296fb49ca" +checksum = "d314cc62af2b6b0c65780555abb4d02a03dd3b799cd42419044f0c38d99738c0" dependencies = [ "darling", "ident_case", @@ -68,9 +68,20 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core 0.10.0", +] [[package]] name = "convert_case" @@ -81,11 +92,20 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "darling" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -93,11 +113,10 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", @@ -107,20 +126,26 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", "syn", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" -version = "0.3.12" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys", @@ -138,30 +163,89 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "getrandom" -version = "0.3.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + [[package]] name = "kinded" version = "0.3.0" @@ -184,22 +268,34 @@ dependencies = [ ] [[package]] -name = "lazy_static" -version = "1.5.0" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "num-bigint" @@ -242,18 +338,18 @@ dependencies = [ [[package]] name = "nutype" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3340cb6773b0794ecb3f62ff66631d580f57151d9415c10ee8a27a357aeb998b" +checksum = "70587a780088c67dad31bf722b875c5616096b4d8c0ded8b7de03294ffb9bbb5" dependencies = [ "nutype_macros", ] [[package]] name = "nutype_macros" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c955e27d02868fe90b9c2dc901661fd7ed67ec382782bdc67c6aa8d2e957a9" +checksum = "148934b975faeddd8f0679d7cf11280b50c4c5495d6fe72bdaf07c932874b404" dependencies = [ "cfg-if", "kinded", @@ -281,9 +377,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.34" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", @@ -291,25 +387,24 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" -version = "1.7.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" dependencies = [ "bit-set", "bit-vec", "bitflags", - "lazy_static", "num-traits", - "rand", + "rand 0.9.2", "rand_chacha", "rand_xorshift", "regex-syntax", @@ -326,9 +421,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -341,12 +436,23 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.1", + "rand_core 0.10.0", ] [[package]] @@ -356,32 +462,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom", + "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "rand_xorshift" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core", + "rand_core 0.9.5", ] [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "rustc_version" @@ -394,9 +506,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags", "errno", @@ -407,15 +519,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", "quick-error", @@ -425,9 +537,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" @@ -458,6 +570,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "shuftlib" version = "0.1.2" @@ -468,7 +593,7 @@ dependencies = [ "num-rational", "nutype", "proptest", - "rand", + "rand 0.10.0", "serde", "strum", "thiserror", @@ -482,31 +607,30 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", "syn", ] [[package]] name = "syn" -version = "2.0.103" +version = "2.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" dependencies = [ "proc-macro2", "quote", @@ -515,12 +639,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.20.0" +version = "3.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.1", "once_cell", "rustix", "windows-sys", @@ -528,18 +652,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -554,9 +678,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" [[package]] name = "unicode-segmentation" @@ -564,6 +688,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "urlencoding" version = "2.1.3" @@ -580,112 +710,182 @@ dependencies = [ ] [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] -name = "windows-sys" -version = "0.59.0" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "windows-targets", + "wit-bindgen", ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "leb128fmt", + "wasmparser", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" +name = "wasm-metadata" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] [[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" +name = "wasmparser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] [[package]] -name = "windows_i686_gnu" -version = "0.52.6" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] [[package]] -name = "windows_i686_msvc" -version = "0.52.6" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] [[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" +name = "wit-bindgen-core" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" +name = "wit-bindgen-rust" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "wit-bindgen-rust-macro" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-component" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ + "anyhow", "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", ] [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 636c230..89ae4c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,12 +15,12 @@ repository = "https://github.com/shuftle/shuftlib" path = "src/lib.rs" [dependencies] -anyhow = "1.0.98" +anyhow = "1.0.101" array-init = "2.1.0" -bon = "3.6.4" +bon = "3.9.0" num-rational = "0.4.2" -nutype = "0.6.1" -rand="0.9" +nutype = "0.6.2" +rand="0.10" serde = { version = "1.0", optional = true } strum = {version="0.27", default-features=false, features=["derive"]} thiserror = "2.0" @@ -30,7 +30,7 @@ default = [] serde = ["dep:serde"] [dev-dependencies] -proptest="1.7" +proptest="1.10" [profile.test.package.proptest] opt-level = 3 diff --git a/proptest-regressions/core/deck.txt b/proptest-regressions/core/deck.txt deleted file mode 100644 index efcdb24..0000000 --- a/proptest-regressions/core/deck.txt +++ /dev/null @@ -1,12 +0,0 @@ -# Seeds for failure cases proptest has generated in the past. It is -# automatically read and these particular cases re-run before any -# novel cases are generated. -# -# It is recommended to check this file in to source control so that -# everyone who runs the test benefits from these saved cases. -cc 504d5514fdaf5842380fd98544b9e79acb7ba0dbb34837faa7a5d2c54a0f1e1f # shrinks to card = (Ace, Hearts), mut deck = Deck { cards: [] } -cc ed53cdee34dc5ffaaa5301eb8df346430662b237c8102337c0b3ac2958301e76 # shrinks to mut deck = Deck { cards: [ItalianCard { rank: Ace, suit: Hearts }, ItalianCard { rank: Ace, suit: Hearts }] } -cc a0bea90e964a3f59c109c2b0f3812f3cf577b1d3bedf918af96466d63ad6c4c8 # shrinks to card = (Seven, Hearts), mut deck = Deck { cards: [ItalianCard { rank: King, suit: Diamonds }, ItalianCard { rank: Two, suit: Clubs }, ItalianCard { rank: Three, suit: Diamonds }, ItalianCard { rank: Ace, suit: Clubs }, ItalianCard { rank: Seven, suit: Diamonds }] } -cc 40fd6a3829946fd2211407dbd5a44ad7c3ca7e4471dbcae106a4990fed46ab1f # shrinks to card = (Ace, Hearts) -cc 7f14002fa9acd7850104a1552f96c9205f6a351be7ceca964577ecb2210da768 # shrinks to card = (Three, Clubs), mut deck = Deck { cards: [ItalianCard { rank: Two, suit: Clubs }, ItalianCard { rank: Three, suit: Diamonds }, ItalianCard { rank: Five, suit: Spades }, ItalianCard { rank: Six, suit: Clubs }, ItalianCard { rank: Three, suit: Clubs }] } -cc be4cff658b83cbaacfba7c0505c9644c0dcf57e023db627ff060b5958a2b13ea # shrinks to card = (Ace, Hearts), mut deck = Deck { cards: [ItalianCard { rank: Ace, suit: Hearts }, ItalianCard { rank: King, suit: Diamonds }] } diff --git a/proptest-regressions/tressette/game.txt b/proptest-regressions/tressette/game.txt deleted file mode 100644 index 58197de..0000000 --- a/proptest-regressions/tressette/game.txt +++ /dev/null @@ -1,7 +0,0 @@ -# Seeds for failure cases proptest has generated in the past. It is -# automatically read and these particular cases re-run before any -# novel cases are generated. -# -# It is recommended to check this file in to source control so that -# everyone who runs the test benefits from these saved cases. -cc a5ec1862c112ea6bee9299981886b00188439613c7dd3e8aae95ba382be2cf94 # shrinks to num_cards = 40 diff --git a/src/core/deck.rs b/src/core/deck.rs index 78b98dc..0f0ddf4 100644 --- a/src/core/deck.rs +++ b/src/core/deck.rs @@ -1,4 +1,4 @@ -use rand::{Rng, seq::SliceRandom}; +use rand::{RngExt, seq::SliceRandom}; use strum::IntoEnumIterator; use crate::core::{ @@ -223,7 +223,7 @@ impl Deck { } else { let mut drawn = Vec::with_capacity(n); for _ in 0..n { - drawn.push(self.cards.pop().unwrap()); + drawn.push(self.cards.pop()?); } Some(drawn.into_iter()) } From 76561ce69f6c058ed0659d19b82ca62247ef7d1c Mon Sep 17 00:00:00 2001 From: Sebastiano Giordano <46520354+Krahos@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:17:57 +0100 Subject: [PATCH 5/7] chore: unused dependencies and fix allowed licenses --- Cargo.lock | 84 ------------------------------------------------------ Cargo.toml | 4 --- deny.toml | 2 +- 3 files changed, 1 insertion(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 824b10f..c92e834 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,12 +8,6 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" -[[package]] -name = "array-init" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" - [[package]] name = "autocfg" version = "1.5.0" @@ -83,15 +77,6 @@ dependencies = [ "rand_core 0.10.0", ] -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "cpufeatures" version = "0.3.0" @@ -246,27 +231,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" -[[package]] -name = "kinded" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce4bdbb2f423660b19f0e9f7115182214732d8dd5f840cd0a3aee3e22562f34c" -dependencies = [ - "kinded_macros", -] - -[[package]] -name = "kinded_macros" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13b4ddc5dcb32f45dac3d6f606da2a52fdb9964a18427e63cd5ef6c0d13288d" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "leb128fmt" version = "0.1.0" @@ -336,30 +300,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "nutype" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70587a780088c67dad31bf722b875c5616096b4d8c0ded8b7de03294ffb9bbb5" -dependencies = [ - "nutype_macros", -] - -[[package]] -name = "nutype_macros" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "148934b975faeddd8f0679d7cf11280b50c4c5495d6fe72bdaf07c932874b404" -dependencies = [ - "cfg-if", - "kinded", - "proc-macro2", - "quote", - "rustc_version", - "syn", - "urlencoding", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -495,15 +435,6 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustix" version = "1.1.3" @@ -588,13 +519,10 @@ name = "shuftlib" version = "0.1.2" dependencies = [ "anyhow", - "array-init", "bon", "num-rational", - "nutype", "proptest", "rand 0.10.0", - "serde", "strum", "thiserror", ] @@ -682,24 +610,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "wait-timeout" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 89ae4c3..4d37a08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,18 +16,14 @@ path = "src/lib.rs" [dependencies] anyhow = "1.0.101" -array-init = "2.1.0" bon = "3.9.0" num-rational = "0.4.2" -nutype = "0.6.2" rand="0.10" -serde = { version = "1.0", optional = true } strum = {version="0.27", default-features=false, features=["derive"]} thiserror = "2.0" [features] default = [] -serde = ["dep:serde"] [dev-dependencies] proptest="1.10" diff --git a/deny.toml b/deny.toml index ffd478b..5749a51 100644 --- a/deny.toml +++ b/deny.toml @@ -92,7 +92,7 @@ allow = [ "MIT", "Apache-2.0", "Apache-2.0 WITH LLVM-exception", - "Unicode-DFS-2016", + "Unicode-3.0", ] # The confidence threshold for detecting a license from license text. # The higher the value, the more closely the license text must be to the From e8681995a59fbd5c2cc799619a40cc1205a765a3 Mon Sep 17 00:00:00 2001 From: Sebastiano Giordano <46520354+Krahos@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:30:12 +0100 Subject: [PATCH 6/7] chore: updating workflows --- .github/workflows/audit.yml | 11 +++--- .github/workflows/cd.yml | 27 ------------- .github/workflows/ci.yml | 56 +++++++++++++-------------- .github/workflows/post-merge.yml | 66 ++++++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 61 deletions(-) delete mode 100644 .github/workflows/cd.yml create mode 100644 .github/workflows/post-merge.yml diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 16cd062..4e5b08d 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -1,14 +1,15 @@ name: Security audit & cargo deny on: schedule: - - cron: '0 0 * * *' + - cron: "0 0 * * 0" pull_request: paths: - - '**/Cargo.toml' - - '**/Cargo.lock' + - "**/Cargo.toml" + - "**/Cargo.lock" + - "**/deny.toml" jobs: security_audit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: EmbarkStudios/cargo-deny-action@v1 + - uses: actions/checkout@v6 + - uses: EmbarkStudios/cargo-deny-action@v2 diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml deleted file mode 100644 index dec080c..0000000 --- a/.github/workflows/cd.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Release-plz - -permissions: - pull-requests: write - contents: write - -on: - push: - branches: - - main - -jobs: - release-plz: - name: Release-plz - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@nightly - - name: Run release-plz - uses: MarcoIeni/release-plz-action@v0.5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a96338..ed3eac4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,69 +1,67 @@ -name: Rust +name: Rust CI on: - push: - branches: - - main pull_request: - types: [ opened, synchronize, reopened ] + types: + - opened + - synchronize + - reopened branches: - main workflow_dispatch: +permissions: + contents: read + env: CARGO_TERM_COLOR: always jobs: - test: - name: Test + nightly-test: + name: Test with nightly runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@nightly - name: Run tests run: cargo test + test: + name: Test with stable + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + - name: Run tests + run: cargo test + fmt: name: Rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@nightly with: components: rustfmt - name: Enforce formatting - run: cargo fmt --check + run: cargo fmt --all --check clippy: name: Clippy runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@nightly + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable with: components: clippy - name: Linting run: cargo clippy -- -D warnings - coverage: - name: Code coverage - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@nightly - - name: Install tarpaulin - run: cargo install cargo-tarpaulin - - name: Generate code coverage - run: cargo tarpaulin --verbose --workspace - dependencies: name: Unused dependencies runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@nightly - - run: cargo install cargo-udeps - - name: Check for unused dependencies - run: cargo +nightly udeps + uses: actions/checkout@v5 + - name: Machete + uses: bnjbvr/cargo-machete@main diff --git a/.github/workflows/post-merge.yml b/.github/workflows/post-merge.yml new file mode 100644 index 0000000..cd4c334 --- /dev/null +++ b/.github/workflows/post-merge.yml @@ -0,0 +1,66 @@ +name: Post-merge + +permissions: + pull-requests: write + contents: write + +on: + push: + branches: + - main + +jobs: + release-plz-release: + name: Release-plz release + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'shuftle' }} + permissions: + contents: write + steps: + - &checkout + name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - &install-rust + name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Run release-plz + uses: release-plz/action@v0.5 + with: + command: release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + release-plz-pr: + name: Release-plz + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'shuftle' }} + concurrency: + group: release-plz-${{ github.ref }} + cancel-in-progress: false + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Run release-plz + uses: release-plz/action@v0.5 + with: + command: release-pr + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + coverage: + name: Code coverage + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + - name: Install cargo-llvm-cov + run: cargo install cargo-llvm-cov + - name: Generate code coverage + run: cargo llvm-cov --workspace From 23100ad8d2ad1ff3e688635f86830a1331feb726 Mon Sep 17 00:00:00 2001 From: Sebastiano Giordano <46520354+Krahos@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:35:56 +0100 Subject: [PATCH 7/7] chor: fmt --- src/tressette.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tressette.rs b/src/tressette.rs index 4e87d62..4bd053f 100644 --- a/src/tressette.rs +++ b/src/tressette.rs @@ -15,5 +15,5 @@ pub mod game; // Re-export main types for convenience pub use card::TressetteCard; -pub use rules::{TressetteRules, SCORE_TO_WIN}; -pub use game::{Game, MoveEffect, Status, Error}; +pub use game::{Error, Game, MoveEffect, Status}; +pub use rules::{SCORE_TO_WIN, TressetteRules};