Skip to main content

gin_rummy/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3// The 52-card bitset design extracts 16-bit suit lanes and 4-bit ranks from
4// `u64` masks all over the crate; every such cast keeps exactly the intended
5// bits.  Allowed here so a pedantic lint run is quiet on `src/`; this is a
6// no-op under the default CI lint set.
7#![allow(clippy::cast_possible_truncation)]
8
9#[cfg(feature = "rand")]
10pub mod deck;
11pub mod game;
12pub mod hand;
13pub mod meld;
14pub mod player;
15pub mod round;
16pub mod rules;
17
18pub use game::{FinalScore, Game};
19pub use hand::{Card, Hand, Holding, Rank};
20pub use meld::{Meld, MeldKind, Melds, best_melds, deadwood, pip_sum};
21pub use player::Player;
22pub use round::{Phase, Round, RoundResult};
23pub use rules::{OklahomaAce, Rules, Shutout};
24
25use core::fmt::{self, Write as _};
26use core::str::FromStr;
27use thiserror::Error;
28
29/// A suit of playing cards
30///
31/// Suits are ranked alphabetically, clubs low, by deriving [`PartialOrd`] and
32/// [`Ord`].  Gin rummy itself treats suits as equals; the order only fixes
33/// the display and encoding conventions of this crate.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36#[repr(u8)]
37pub enum Suit {
38    /// ♣
39    Clubs,
40    /// ♦
41    Diamonds,
42    /// ♥
43    Hearts,
44    /// ♠
45    Spades,
46}
47
48impl Suit {
49    /// Suits in the ascending order, the order in this crate
50    pub const ASC: [Self; 4] = [Self::Clubs, Self::Diamonds, Self::Hearts, Self::Spades];
51
52    /// Suits in the descending order
53    pub const DESC: [Self; 4] = [Self::Spades, Self::Hearts, Self::Diamonds, Self::Clubs];
54
55    /// Uppercase letter
56    #[must_use]
57    #[inline]
58    pub const fn letter(self) -> char {
59        match self {
60            Self::Clubs => 'C',
61            Self::Diamonds => 'D',
62            Self::Hearts => 'H',
63            Self::Spades => 'S',
64        }
65    }
66}
67
68impl fmt::Display for Suit {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        f.write_char(match self {
71            Self::Clubs => '♣',
72            Self::Diamonds => '♦',
73            Self::Hearts => '♥',
74            Self::Spades => '♠',
75        })
76    }
77}
78
79/// Unicode variation selectors that may appear after suit emojis
80///
81/// We want to ignore these suffixes when parsing suits.
82const EMOJI_SELECTORS: [char; 2] = ['\u{FE0F}', '\u{FE0E}'];
83
84/// Error returned when parsing a [`Suit`] fails
85#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
86#[error("Invalid suit: expected one of C, D, H, S, ♣, ♦, ♥, ♠, ♧, ♢, ♡, ♤")]
87pub struct ParseSuitError;
88
89impl FromStr for Suit {
90    type Err = ParseSuitError;
91    fn from_str(s: &str) -> Result<Self, Self::Err> {
92        match s
93            .to_ascii_uppercase()
94            .as_str()
95            .trim_end_matches(EMOJI_SELECTORS)
96        {
97            "C" | "♣" | "♧" => Ok(Self::Clubs),
98            "D" | "♦" | "♢" => Ok(Self::Diamonds),
99            "H" | "♥" | "♡" => Ok(Self::Hearts),
100            "S" | "♠" | "♤" => Ok(Self::Spades),
101            _ => Err(ParseSuitError),
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn suit_order_matches_arrays() {
112        assert!(Suit::Clubs < Suit::Diamonds);
113        assert!(Suit::Diamonds < Suit::Hearts);
114        assert!(Suit::Hearts < Suit::Spades);
115
116        let mut sorted = Suit::DESC;
117        sorted.sort();
118        assert_eq!(sorted, Suit::ASC);
119    }
120
121    #[test]
122    fn suit_parsing() {
123        for suit in Suit::ASC {
124            assert_eq!(suit.to_string().parse(), Ok(suit));
125            assert_eq!(suit.letter().to_string().parse(), Ok(suit));
126            assert_eq!(
127                suit.letter().to_ascii_lowercase().to_string().parse(),
128                Ok(suit)
129            );
130        }
131
132        assert_eq!("SPADES".parse::<Suit>(), Err(ParseSuitError));
133        assert_eq!("".parse::<Suit>(), Err(ParseSuitError));
134        assert_eq!("♠♠".parse::<Suit>(), Err(ParseSuitError));
135        assert_eq!("♠\u{FE0F}".parse(), Ok(Suit::Spades));
136        assert_eq!("♤\u{FE0E}".parse(), Ok(Suit::Spades));
137    }
138}