Skip to main content

server/domain/types/
card.rs

1use serde::{Deserialize, Serialize};
2use strum::IntoEnumIterator;
3
4use crate::domain::CardsError;
5mod face;
6mod rank;
7mod suit;
8mod value;
9
10pub use self::{face::Face, rank::Rank, suit::Suit, value::Value};
11
12/// A playing card consisting of a [`Face`] and a [`Suit`].
13///
14/// Note `Card` is `Copy`able.
15#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub struct Card {
17    face: Face,
18    suit: Suit,
19}
20
21impl Card {
22    /// Constructs a new `Card` from a face and a suit.
23    #[must_use]
24    const fn new(face: Face, suit: Suit) -> Self {
25        Self { face, suit }
26    }
27
28    /// Returns an iterator over **all** 52 standard playing cards.
29    ///
30    /// The cards are returned in the order of `Suit::iter()` × `Face::iter()`.
31    #[must_use]
32    pub fn all() -> Vec<Self> {
33        let cards_for_suit = |s: Suit| Face::iter().map(move |f| Self::new(f, s));
34        Suit::iter().flat_map(cards_for_suit).collect::<Vec<_>>()
35    }
36
37    /// Returns a compact identifier string for the card (e.g. `"AS"`, `"KD"`, `"T♥"`).
38    ///
39    /// The format is single character anglicised face abbreviation followed by the
40    /// single character anglicised suit abbreviation,
41    #[must_use]
42    pub fn cid(&self) -> String {
43        let Self { face, suit } = self;
44        format!("{face}{suit}")
45    }
46
47    /// Returns the face (rank) part of the card.
48    #[must_use]
49    pub fn face(&self) -> Face {
50        self.face
51    }
52
53    /// Helper function returning `true` if this card is a Jack, regardless of suit.
54    #[must_use]
55    pub fn is_jack(&self) -> bool {
56        self.face.is_jack()
57    }
58
59    /// Returns the suit part of the card.
60    #[must_use]
61    pub fn suit(&self) -> Suit {
62        self.suit
63    }
64
65    /// Returns the rank value used for most card games (Ace = 14, King = 13, …, Two = 2).
66    ///
67    /// See [`Face::rank()`] for details.
68    #[must_use]
69    pub fn rank(&self) -> Rank {
70        self.face.rank()
71    }
72
73    /// Returns the numeric value of the card as used in a particular game.
74    ///
75    /// See [`Face::value()`] for details.
76    #[must_use]
77    pub fn value(&self) -> Value {
78        self.face.value()
79    }
80}
81
82impl std::str::FromStr for Card {
83    type Err = CardsError;
84
85    fn from_str(s: &str) -> Result<Self, Self::Err> {
86        if s.len() == 2 {
87            let mut chars = s.chars();
88            let face = chars.next().ok_or(CardsError::InvalidCard(s.into()))?;
89            let face = Face::try_from(face)?;
90            let suit = chars.next().ok_or(CardsError::InvalidCard(s.into()))?;
91            let suit = Suit::try_from(suit)?;
92            Ok(Card::new(face, suit))
93        } else {
94            Err(CardsError::InvalidCard(s.into()))
95        }
96    }
97}
98
99#[cfg(test)]
100pub(crate) trait CardExt {
101    fn cards_from(self) -> Result<Vec<Card>, CardsError>;
102}
103
104#[cfg(test)]
105impl CardExt for &str {
106    fn cards_from(self) -> Result<Vec<Card>, CardsError> {
107        use std::str::FromStr;
108
109        if !self.len().is_multiple_of(2) {
110            Err(CardsError::InvalidCard(self.into()))
111        } else {
112            (0..self.len())
113                .step_by(2)
114                .map(|i| Card::from_str(&self[i..i + 2]))
115                .collect()
116        }
117    }
118}
119
120impl std::fmt::Debug for Card {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        write!(f, "Card({})", self)
123    }
124}
125
126impl std::fmt::Display for Card {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        self.cid().fmt(f)
129    }
130}
131
132#[cfg(test)]
133#[coverage(off)]
134mod test {
135    use super::*;
136
137    #[test]
138    fn cards_have_definitive_id_rank_and_value() {
139        use std::str::FromStr;
140
141        let suits = "HCDS";
142        let faces = "A23456789TJQK";
143
144        for suit in suits.chars() {
145            for face in faces.chars() {
146                let cid = format!("{face}{suit}");
147                let face = Face::try_from(face).expect("valid face");
148                let card = Card::from_str(cid.as_str()).expect("valid cards str");
149                assert_eq!(card.cid(), cid);
150                assert_eq!(card.rank(), face.rank());
151                assert_eq!(card.value(), face.value());
152            }
153        }
154    }
155}