playin_cards/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2#![doc = include_str!("../README.md")]
3//! # Usage
4//! The main interface is the [`Card`] which represents a single playing card
5//! which may be one of the cards in a standard 52-card deck of playing cards,
6//! or a Joker which may be either black or red. A [`Card`] instance can be
7//! easily be parsed from a 2-`char` `str` with one of `A23456789TJQK` as the
8//! first char and one of `SsCcDdHh♥♣♠♦` as the second char.
9//!
10//! `Card` can also be formatted as a 2-`char` unicode string such `A♣`, `5♥`,
11//! `J♠` with the [`Display`] trait or as a sinlg-`char` unicode card char such
12//! as `🃑`, `🂵`, `🂫` with the [`UpperHex`] trait.
13
14#[allow(unused_imports)]
15use std::fmt::{Display, UpperHex};
16#[cfg(feature = "rank-partialord")]
17use std::cmp::{PartialOrd, Ordering};
18
19use itertools::Itertools;
20use strum_macros::EnumIter;
21use strum::IntoEnumIterator;
22
23/// number of cards in a standard 52-card deck of French-suited playing cards
24pub const CARDS_PER_DECK: u8 = 52;
25
26/// A French-suited playing card
27///
28/// The card is represented by either `Joker`, which has only a `Color` or a
29/// `Regular` which represents a normal card from a standard 52-card deck,
30/// comprised of a [`Rank`] and a [`Suit`]
31///
32/// ## Parsing
33///
34/// `Card` can easily be parsed from a 2-`char` `str` representation where the
35/// first char represents the [`Rank`] as one of `A23456789TJQK`) and the
36/// second char represents the [`Suit`] as one of `SsCcDdHh♠♥♣♦`
37/// ```
38/// use playin_cards::{Card::*, Rank::*, Suit::*};
39///
40/// // Parse a Card instance from a 2-char ASCII string:
41/// assert_eq!("AS".parse(), Ok(Regular{rank: Ace, suit: Spades}));
42/// assert_eq!("3D".parse(), Ok(Regular{rank: Three, suit: Diamonds}));
43/// assert_eq!("JC".parse(), Ok(Regular{rank: Jack, suit: Clubs}));
44/// assert_eq!("9H".parse(), Ok(Regular{rank: Nine, suit: Hearts}));
45///
46/// // A Card can also be parsed from Unicode representation:
47/// assert_eq!("A♣".parse(), Ok(Regular{rank: Ace, suit: Clubs}));
48/// assert_eq!("5♠".parse(), Ok(Regular{rank: Five, suit: Spades}));
49/// assert_eq!("8♥".parse(), Ok(Regular{rank: Eight, suit: Hearts}));
50/// ```
51///
52/// ## Formatting
53/// `Card` can be formatted as a 2-`char` Unicode `str` using [`Display`] or as
54/// a single-`char` unicode `str` using [`UpperHex`]
55///
56/// ```
57/// use playin_cards::{Card::*, Rank::*, Suit::*};
58///
59/// // `Display` uses ASCII for rank and Unicode for Suit:
60/// assert_eq!(format!("{}", Regular{rank: Ace, suit: Clubs}),     "A♣");
61/// assert_eq!(format!("{}", Regular{rank: King, suit: Diamonds}), "K♦");
62/// assert_eq!(format!("{}", Regular{rank: Ace, suit: Clubs}),     "A♣");
63///
64/// // `UpperHex` uses Unicode card chars
65/// assert_eq!(format!("{:X}", Regular{rank: Ace, suit: Clubs}), "🃑");
66/// ```
67#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
68pub enum Card {
69    /// Regular playing card from a standard 52-card deck.
70    Regular { rank: Rank, suit: Suit },
71    /// A Joker
72    Joker(Color),
73}
74
75/// Ranks of a standard French-suited playing card
76///
77/// ## Values
78/// `Rank` instances do not have a notion of "value". Value is not an intrinsic
79/// property of the rank of a playing card, rather it is a property imposed on
80/// it by the rank valuing system of the specific game in which the card is
81/// being used; for example one game might assign a value of 11 to `Ace` while
82/// another may assign it a value of 1, and others may allow the player to
83/// decide if it is 1 or 11 based on other cards in their hand. For this reason
84/// it is inappropriate for this library to expose a public interface to the
85/// concept of value for `Rank`, it is up to this library's consumers to define
86/// how values are assigned to ranks, if at all.
87///
88/// That said, the discriminants of the variants of `Rank` start at 1 for Ace
89/// and increase by 1 all the way up to `King`, as this facilitates the
90/// implementations of what is by-far the most common rank valuing system used
91/// in card games.
92/// ```
93/// use playin_cards::Rank;
94/// assert_eq!(Rank::Ace as u8, 1);
95/// assert_eq!(Rank::Six as u8, 6);
96/// assert_eq!(Rank::King as u8, 13);
97/// ```
98///
99/// ## Conversions
100///
101/// `Rank` is bidirectionally convertible with `char` via `From` and `TryFrom`:
102///
103/// ```
104/// use playin_cards::{Rank, ParseError};
105///
106/// // get the char corresponding to a `Rank` instance
107/// assert_eq!(char::from(Rank::King), 'K');
108/// assert_eq!(char::from(Rank::Five), '5');
109/// assert_eq!(char::from(Rank::Jack), 'J');
110///
111/// // parse a single `char` to a `Rank`
112/// assert_eq!(Rank::try_from('Q'), Ok(Rank::Queen));
113/// assert_eq!(Rank::try_from('4'), Ok(Rank::Four));
114/// assert_eq!(Rank::try_from('T'), Ok(Rank::Ten));
115/// assert_eq!(Rank::try_from('A'), Ok(Rank::Ace));
116///
117/// // Parsing fails if an invalid char is used
118/// assert_eq!(Rank::try_from('Y'), Err(ParseError::InvalidChar));
119/// ```
120///
121/// ## Iteration
122/// Thanks to [`strum`], the vairants of [`Rank`] can be iterated over:
123/// ```
124/// use playin_cards::Rank;
125/// use strum::IntoEnumIterator;
126///
127/// let mut rankiter = Rank::iter();
128/// assert_eq!(rankiter.next(), Some(Rank::Ace));
129/// assert_eq!(rankiter.next(), Some(Rank::Two));
130/// assert_eq!(rankiter.next(), Some(Rank::Three));
131/// assert_eq!(rankiter.next(), Some(Rank::Four));
132/// // You get the point...
133///
134/// ```
135#[derive(EnumIter, Debug, PartialEq, Clone, Copy, Eq, Hash)]
136pub enum Rank {
137    Ace = 1,
138    Two,
139    Three,
140    Four,
141    Five,
142    Six,
143    Seven,
144    Eight,
145    Nine,
146    Ten,
147    Jack,
148    Queen,
149    King,
150}
151
152#[cfg(feature = "rank-partialord")]
153impl PartialOrd for Rank {
154    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
155        PartialOrd::partial_cmp(self as u8, other as u8)
156    }
157}
158
159/// Suits of a standard French-suited playing card
160///
161/// ## Conversions
162/// `Suit` is bidirectionally convertible with `char`:
163/// ```
164/// use playin_cards::Suit;
165/// // Converting Suit -> char uses Unicode
166/// assert_eq!(char::from(Suit::Spades),   '♠');
167/// assert_eq!(char::from(Suit::Hearts),   '♥');
168/// assert_eq!(char::from(Suit::Diamonds), '♦');
169/// assert_eq!(char::from(Suit::Clubs),    '♣');
170/// // Converting char -> Suit allows uppercase ASCII, lowercase ASCII, or
171/// // Unicode
172/// assert_eq!(Suit::try_from('S'), Ok(Suit::Spades)); // Uppercase
173/// assert_eq!(Suit::try_from('s'), Ok(Suit::Spades)); // Lowercase
174/// assert_eq!(Suit::try_from('♠'), Ok(Suit::Spades)); // Unicode
175/// ```
176/// ## Iteration
177/// Thanks to [`strum`], the variants of [`Suit`] can be iterated over:
178/// ```
179/// use playin_cards::Suit;
180/// use strum::IntoEnumIterator;
181///
182/// let mut suititer = Suit::iter();
183/// assert_eq!(suititer.next(), Some(Suit::Hearts));
184/// assert_eq!(suititer.next(), Some(Suit::Diamonds));
185/// assert_eq!(suititer.next(), Some(Suit::Spades));
186/// assert_eq!(suititer.next(), Some(Suit::Clubs));
187///
188/// ```
189#[derive(EnumIter, Debug, Clone, Copy, PartialEq, Eq, Hash)]
190pub enum Suit {
191    Hearts,
192    Diamonds,
193    Spades,
194    Clubs,
195}
196
197/// Color of a [`Suit`] or [`Card`]. Either Red or Black
198#[derive(Debug, PartialEq, Clone, Copy, Eq, Hash)]
199pub enum Color {
200    Red,
201    Black,
202}
203
204mod format;
205mod parse;
206
207pub use parse::ParseError;
208
209impl Suit {
210    /// Set of allowed chars which can be converted to [`Suit`] with
211    /// [`TryFrom<char>`]
212    pub const ALLOWED_CHARS: &'static str = "SsCcHhDd♠♣♥♦";
213
214    /// Return the color of the `Suit`
215    pub fn color(self) -> Color {
216        match self {
217            Suit::Hearts | Suit::Diamonds => Color::Red,
218            Suit::Spades | Suit::Clubs => Color::Black,
219        }
220    }
221}
222
223impl Card {
224    /// Return the color of the `Card` which is just the color of its `Suit`
225    pub fn color(self) -> Color {
226        match self {
227            Self::Regular { rank: _, suit } => suit.color(),
228            Self::Joker(color) => color,
229        }
230    }
231
232    /// Return `true` if this card is a Joker, otherwise return `false`
233    pub fn is_joker(self) -> bool {
234        match self {
235            Self::Regular { rank: _, suit: _ } => false,
236            Self::Joker(_) => true,
237        }
238    }
239}
240
241impl Rank {
242    /// Set of allowed chars which can be converted to [`Rank`] with
243    /// [`TryFrom<char>`]
244    pub const ALLOWED_CHARS: &'static str = "A23456789TJQK";
245}
246
247impl From<(Rank, Suit)> for Card {
248    fn from((rank, suit): (Rank, Suit)) -> Self {
249        Self::Regular{rank, suit}
250    }
251}
252
253/// Generate an (unshuffled) [shoe](https://en.wikipedia.org/wiki/Shoe_(cards))
254///
255/// Return an unshuffled shoe containing `n_decks` decks. If `with_jokers`
256/// `true`, Each deck has 54 cards, all the cards in a standard 52-card deck,
257/// and 2 Jokers, one Black and one Red. If `with_jokers` is `false`, each deck
258/// only contains the 52 cards in a standard deck.
259pub fn gen_shoe(n_decks: u8, with_jokers: bool) -> Vec<Card> {
260
261    let extra_cards: u8 = if with_jokers {2} else {0};
262    // total number of cards in the shoe
263    let total_cards = n_decks as usize * (CARDS_PER_DECK + extra_cards) as usize;
264
265    // preallocate memory for the shoe
266    let mut output = Vec::with_capacity(total_cards);
267    for _ in 0..n_decks  {
268        let deck_iter = Rank::iter()
269            .cartesian_product(Suit::iter())
270            .map(Card::from);
271        output.extend(deck_iter);
272        if with_jokers {
273            output.extend([Card::Joker(Color::Red), Card::Joker(Color::Black)]);
274        }
275    }
276    output
277}
278
279#[cfg(test)]
280mod test {
281    use super::*;
282    use strum::IntoEnumIterator;
283    use itertools::Itertools;
284    use std::collections::HashMap;
285
286    pub const FULL_DECK_CARDS: [Card; 52] = [
287        Card::Regular{suit: Suit::Hearts, rank: Rank::Ace},
288        Card::Regular{suit: Suit::Hearts, rank: Rank::Two},
289        Card::Regular{suit: Suit::Hearts, rank: Rank::Three},
290        Card::Regular{suit: Suit::Hearts, rank: Rank::Four},
291        Card::Regular{suit: Suit::Hearts, rank: Rank::Five},
292        Card::Regular{suit: Suit::Hearts, rank: Rank::Six},
293        Card::Regular{suit: Suit::Hearts, rank: Rank::Seven},
294        Card::Regular{suit: Suit::Hearts, rank: Rank::Eight},
295        Card::Regular{suit: Suit::Hearts, rank: Rank::Nine},
296        Card::Regular{suit: Suit::Hearts, rank: Rank::Ten},
297        Card::Regular{suit: Suit::Hearts, rank: Rank::Jack},
298        Card::Regular{suit: Suit::Hearts, rank: Rank::Queen},
299        Card::Regular{suit: Suit::Hearts, rank: Rank::King},
300        Card::Regular{suit: Suit::Diamonds, rank: Rank::Ace},
301        Card::Regular{suit: Suit::Diamonds, rank: Rank::Two},
302        Card::Regular{suit: Suit::Diamonds, rank: Rank::Three},
303        Card::Regular{suit: Suit::Diamonds, rank: Rank::Four},
304        Card::Regular{suit: Suit::Diamonds, rank: Rank::Five},
305        Card::Regular{suit: Suit::Diamonds, rank: Rank::Six},
306        Card::Regular{suit: Suit::Diamonds, rank: Rank::Seven},
307        Card::Regular{suit: Suit::Diamonds, rank: Rank::Eight},
308        Card::Regular{suit: Suit::Diamonds, rank: Rank::Nine},
309        Card::Regular{suit: Suit::Diamonds, rank: Rank::Ten},
310        Card::Regular{suit: Suit::Diamonds, rank: Rank::Jack},
311        Card::Regular{suit: Suit::Diamonds, rank: Rank::Queen},
312        Card::Regular{suit: Suit::Diamonds, rank: Rank::King},
313        Card::Regular{suit: Suit::Spades, rank: Rank::Ace},
314        Card::Regular{suit: Suit::Spades, rank: Rank::Two},
315        Card::Regular{suit: Suit::Spades, rank: Rank::Three},
316        Card::Regular{suit: Suit::Spades, rank: Rank::Four},
317        Card::Regular{suit: Suit::Spades, rank: Rank::Five},
318        Card::Regular{suit: Suit::Spades, rank: Rank::Six},
319        Card::Regular{suit: Suit::Spades, rank: Rank::Seven},
320        Card::Regular{suit: Suit::Spades, rank: Rank::Eight},
321        Card::Regular{suit: Suit::Spades, rank: Rank::Nine},
322        Card::Regular{suit: Suit::Spades, rank: Rank::Ten},
323        Card::Regular{suit: Suit::Spades, rank: Rank::Jack},
324        Card::Regular{suit: Suit::Spades, rank: Rank::Queen},
325        Card::Regular{suit: Suit::Spades, rank: Rank::King},
326        Card::Regular{suit: Suit::Clubs, rank: Rank::Ace},
327        Card::Regular{suit: Suit::Clubs, rank: Rank::Two},
328        Card::Regular{suit: Suit::Clubs, rank: Rank::Three},
329        Card::Regular{suit: Suit::Clubs, rank: Rank::Four},
330        Card::Regular{suit: Suit::Clubs, rank: Rank::Five},
331        Card::Regular{suit: Suit::Clubs, rank: Rank::Six},
332        Card::Regular{suit: Suit::Clubs, rank: Rank::Seven},
333        Card::Regular{suit: Suit::Clubs, rank: Rank::Eight},
334        Card::Regular{suit: Suit::Clubs, rank: Rank::Nine},
335        Card::Regular{suit: Suit::Clubs, rank: Rank::Ten},
336        Card::Regular{suit: Suit::Clubs, rank: Rank::Jack},
337        Card::Regular{suit: Suit::Clubs, rank: Rank::Queen},
338        Card::Regular{suit: Suit::Clubs, rank: Rank::King},
339    ];
340
341    #[test]
342    /// Tests that all Ranks cast to their respective values of u8
343    fn rank_cast_u8() {
344        assert_eq!(Rank::Ace as u8, 1, "Ace as u8 was not 1");
345        assert_eq!(Rank::Two as u8, 2, "Two as u8 was not 2");
346        assert_eq!(Rank::Three as u8, 3, "Three as u8 was not 3");
347        assert_eq!(Rank::Four as u8, 4, "Four as u8 was not 4");
348        assert_eq!(Rank::Five as u8, 5, "Five as u8 was not 5");
349        assert_eq!(Rank::Six as u8, 6, "Six as u8 was not 6");
350        assert_eq!(Rank::Seven as u8, 7, "Seven as u8 was not 7");
351        assert_eq!(Rank::Eight as u8, 8, "Eight as u8 was not 8");
352        assert_eq!(Rank::Nine as u8, 9, "Nine as u8 was not 9");
353        assert_eq!(Rank::Ten as u8, 10, "Ten as u8 was not 10");
354        assert_eq!(Rank::Jack as u8, 11, "Jack as u8 was not 11");
355        assert_eq!(Rank::Queen as u8, 12, "Queen as u8 was not 12");
356        assert_eq!(Rank::King as u8, 13, "King as u8 was not 13");
357    }
358    #[test]
359    fn suit_colors() {
360        assert_eq!(Suit::Spades.color(), Color::Black);
361        assert_eq!(Suit::Hearts.color(), Color::Red);
362        assert_eq!(Suit::Clubs.color(), Color::Black);
363        assert_eq!(Suit::Diamonds.color(), Color::Red);
364    }
365    #[test]
366    fn card_colors() {
367        for card in FULL_DECK_CARDS[..26].iter() {
368            assert_eq!(card.color(), Color::Red, "{} incorrectly identified as Red", card);
369        }
370        for card in FULL_DECK_CARDS[26..].iter() {
371            assert_eq!(card.color(), Color::Black, "{} incorrectly identified as Black", card);
372        }
373        assert_eq!(Card::Joker(Color::Black).color(), Color::Black);
374        assert_eq!(Card::Joker(Color::Red).color(), Color::Red);
375    }
376    #[test]
377    fn is_joker() {
378        for card in FULL_DECK_CARDS.iter() {
379            assert!(!card.is_joker(), "{} incorrectly identified as joker", card)
380        }
381        assert!(Card::Joker(Color::Black).is_joker());
382        assert!(Card::Joker(Color::Red).is_joker());
383    }
384    fn count(shoe: &[Card]) -> HashMap<Card, u16> {
385        let mut map = HashMap::new();
386        for &card in shoe {
387            *map.entry(card).or_insert(0) += 1;
388        }
389        map
390    }
391    fn test_shoe(n: u8, w_jokers: bool) {
392        let shoe = gen_shoe(n, w_jokers);
393        let n_jokers = if w_jokers {2 * n} else {0} as usize;
394        assert_eq!(shoe.len(), CARDS_PER_DECK as usize * n as usize + n_jokers);
395        let map = count(&shoe);
396        for (rank, suit) in Rank::iter().cartesian_product(Suit::iter()) {
397            assert_eq!(map.get(&Card::Regular{rank, suit}), Some(&(n as u16)));
398        }
399        if w_jokers {
400            assert_eq!(map.get(&Card::Joker(Color::Black)), Some(&(n as u16)));
401            assert_eq!(map.get(&Card::Joker(Color::Red)), Some(&(n as u16)));
402        }
403    }
404    /// Assert that there is exactly 1 of each card in the results of `gen_shoe`.
405    #[test]
406    fn gen_shoe_no_jokers_single() {
407        test_shoe(1, false);
408    }
409    /// Assert that there is exactly N of each card in the results of `gen_shoe`.
410    #[test]
411    fn gen_shoe_no_jokers_multi() {
412        test_shoe(6, false);
413    }
414    #[test]
415    fn gen_shoe_w_jokers() {
416        test_shoe(1, true);
417    }
418    #[test]
419    fn gen_shoe_w_jokers_multi() {
420        test_shoe(4, true);
421    }
422}