1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3#![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#[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 Clubs,
40 Diamonds,
42 Hearts,
44 Spades,
46}
47
48impl Suit {
49 pub const ASC: [Self; 4] = [Self::Clubs, Self::Diamonds, Self::Hearts, Self::Spades];
51
52 pub const DESC: [Self; 4] = [Self::Spades, Self::Hearts, Self::Diamonds, Self::Clubs];
54
55 #[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
79const EMOJI_SELECTORS: [char; 2] = ['\u{FE0F}', '\u{FE0E}'];
83
84#[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}