Skip to main content

playin_cards/
lib.rs

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