Skip to main content

card_core/
traditional.rs

1//! A traditional implementation of playing cards using the standard 52 card deck.
2
3#![allow(
4    clippy::exhaustive_enums,
5    reason = "Nutype generates exhaustive validation error enums."
6)]
7
8use crate::traits::Card as _;
9use core::fmt::Display;
10use core::{ops::Div as _, str::FromStr};
11use num_enum::{IntoPrimitive, TryFromPrimitive};
12use nutype::nutype;
13use rand::seq::SliceRandom as _;
14use thiserror::Error;
15
16/// The number of ranks in a standard deck.
17pub const NUM_RANKS: u8 = 13;
18/// The number of suits in a standard deck.
19pub const NUM_SUITS: u8 = 4;
20
21const RANKS: &str = "23456789TJQKA";
22const SUITS: &str = "cdhs";
23
24/// The error type for failing to parse a `Card`.
25#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
26#[error("invalid card")]
27#[non_exhaustive]
28pub struct ParseCardError;
29
30/// A playing card.
31#[nutype(
32    new_unchecked,
33    validate(less = (NUM_RANKS * NUM_SUITS)),
34    derive(Clone, Copy, Debug, Eq, Hash, Into, PartialEq, PartialOrd, Ord),
35)]
36pub struct Card(u8);
37
38impl Display for Card {
39    #[inline]
40    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
41        let rank = self.rank();
42        let suit = self.suit();
43        write!(f, "{rank}{suit}")
44    }
45}
46
47impl TryFrom<&str> for Card {
48    type Error = ParseCardError;
49
50    #[inline]
51    fn try_from(value: &str) -> Result<Self, Self::Error> {
52        if value.len() != 2 || !value.is_ascii() {
53            return Err(ParseCardError);
54        }
55
56        let (rank, suit) = value.split_at(1);
57        let rank = Rank::from_str(rank)?;
58        let suit = Suit::from_str(suit)?;
59        Ok((rank, suit).into())
60    }
61}
62
63impl From<(Rank, Suit)> for Card {
64    #[inline]
65    fn from(value: (Rank, Suit)) -> Self {
66        let (rank, suit) = value;
67        // SAFETY:
68        // A valid Rank and Suit should create a valid card.
69        unsafe { Self::new_unchecked(rank as u8 * NUM_SUITS + suit as u8) }
70    }
71}
72
73impl crate::traits::Card for Card {
74    type Rank = Rank;
75
76    type Suit = Suit;
77
78    #[inline]
79    fn rank(self) -> Self::Rank {
80        Self::Rank::try_from(self.into_inner().div(NUM_SUITS))
81            .expect("A valid card should have a valid rank")
82    }
83
84    #[inline]
85    fn suit(self) -> Self::Suit {
86        Self::Suit::try_from(self.into_inner() % NUM_SUITS)
87            .expect("A valid card should have a valid suit")
88    }
89}
90
91/// A `Card` wrapper that is printed with color.
92#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd, Eq, Ord)]
93pub struct PrettyCard(Card);
94
95impl crate::traits::PrettyCard for PrettyCard {
96    type Card = Card;
97}
98
99impl crate::traits::Card for PrettyCard {
100    type Rank = Rank;
101
102    type Suit = Suit;
103
104    #[inline]
105    fn rank(self) -> Self::Rank {
106        self.0.rank()
107    }
108
109    #[inline]
110    fn suit(self) -> Self::Suit {
111        self.0.suit()
112    }
113}
114
115impl From<Card> for PrettyCard {
116    #[inline]
117    fn from(value: Card) -> Self {
118        Self(value)
119    }
120}
121
122impl From<(Rank, Suit)> for PrettyCard {
123    #[inline]
124    fn from(value: (Rank, Suit)) -> Self {
125        Card::from(value).into()
126    }
127}
128
129impl From<PrettyCard> for u8 {
130    #[inline]
131    fn from(value: PrettyCard) -> Self {
132        value.0.into()
133    }
134}
135
136impl TryFrom<&str> for PrettyCard {
137    type Error = ParseCardError;
138
139    #[inline]
140    fn try_from(value: &str) -> Result<Self, Self::Error> {
141        let card = Card::try_from(value)?;
142        Ok(Self(card))
143    }
144}
145
146impl Display for PrettyCard {
147    #[inline]
148    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
149        let suit = self.suit();
150        write!(f, "{}{}{}", suit.color(), self.0, Suit::clear_color())
151    }
152}
153
154/// The `Rank` of a `Card`.
155#[allow(
156    clippy::arbitrary_source_item_ordering,
157    reason = "Ranks should be ordered by their semantic value."
158)]
159#[allow(
160    clippy::exhaustive_enums,
161    reason = "The set of ranks in a standard 52-card deck is fixed and complete"
162)]
163#[derive(
164    Clone, Copy, Debug, Eq, Hash, IntoPrimitive, Ord, PartialEq, PartialOrd, TryFromPrimitive,
165)]
166#[repr(u8)]
167pub enum Rank {
168    /// `2`.
169    Two = 0,
170    /// `3`.
171    Three = 1,
172    /// `4`.
173    Four = 2,
174    /// `5`.
175    Five = 3,
176    /// `6`.
177    Six = 4,
178    /// `7`.
179    Seven = 5,
180    /// `8`.
181    Eight = 6,
182    /// `9`.
183    Nine = 7,
184    /// `T`.
185    Ten = 8,
186    /// `J`.
187    Jack = 9,
188    /// `Q`.
189    Queen = 10,
190    /// `K`.
191    King = 11,
192    /// `A`.
193    Ace = 12,
194}
195
196impl Display for Rank {
197    #[inline]
198    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
199        let index = usize::from(u8::from(*self));
200        let rank = char::from(
201            RANKS
202                .as_bytes()
203                .get(index)
204                .expect("Index should be valid")
205                .to_owned(),
206        );
207        f.write_fmt(format_args!("{rank}"))
208    }
209}
210
211impl FromStr for Rank {
212    type Err = ParseCardError;
213
214    #[inline]
215    fn from_str(value: &str) -> Result<Self, Self::Err> {
216        let [rank] = value.as_bytes() else {
217            return Err(ParseCardError);
218        };
219
220        match rank.to_ascii_uppercase() {
221            b'2' => Ok(Self::Two),
222            b'3' => Ok(Self::Three),
223            b'4' => Ok(Self::Four),
224            b'5' => Ok(Self::Five),
225            b'6' => Ok(Self::Six),
226            b'7' => Ok(Self::Seven),
227            b'8' => Ok(Self::Eight),
228            b'9' => Ok(Self::Nine),
229            b'T' => Ok(Self::Ten),
230            b'J' => Ok(Self::Jack),
231            b'Q' => Ok(Self::Queen),
232            b'K' => Ok(Self::King),
233            b'A' => Ok(Self::Ace),
234            _ => Err(ParseCardError),
235        }
236    }
237}
238
239/// The `Suit` of a `Card`.
240#[allow(
241    clippy::arbitrary_source_item_ordering,
242    reason = "Suits should be ordered by their semantic value."
243)]
244#[allow(
245    clippy::exhaustive_enums,
246    reason = "The set of suits in a standard 52-card deck is fixed and complete"
247)]
248#[derive(
249    Clone, Copy, Debug, Eq, Hash, IntoPrimitive, Ord, PartialEq, PartialOrd, TryFromPrimitive,
250)]
251#[repr(u8)]
252pub enum Suit {
253    /// `c`.
254    Clubs = 0,
255    /// `d`.
256    Diamonds = 1,
257    /// `h`.
258    Hearts = 2,
259    /// `s`.
260    Spades = 3,
261}
262
263impl Display for Suit {
264    #[inline]
265    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
266        let index = usize::from(u8::from(*self));
267        let suit = char::from(
268            SUITS
269                .as_bytes()
270                .get(index)
271                .expect("Index should be valid")
272                .to_owned(),
273        );
274        f.write_fmt(format_args!("{suit}"))
275    }
276}
277
278impl FromStr for Suit {
279    type Err = ParseCardError;
280
281    #[inline]
282    fn from_str(value: &str) -> Result<Self, Self::Err> {
283        let [suit] = value.as_bytes() else {
284            return Err(ParseCardError);
285        };
286
287        match suit.to_ascii_lowercase() {
288            b'c' => Ok(Self::Clubs),
289            b'd' => Ok(Self::Diamonds),
290            b'h' => Ok(Self::Hearts),
291            b's' => Ok(Self::Spades),
292            _ => Err(ParseCardError),
293        }
294    }
295}
296
297impl Suit {
298    #[must_use]
299    const fn clear_color() -> &'static str {
300        "\x1b[0m"
301    }
302
303    #[must_use]
304    const fn color(self) -> &'static str {
305        match self {
306            Self::Clubs => "\x1b[32m",
307            Self::Diamonds => "\x1b[34m",
308            Self::Hearts => "\x1b[31m",
309            Self::Spades => "\x1b[33m",
310        }
311    }
312}
313
314/// A deck of cards.
315#[derive(Clone, Debug, Eq, PartialEq)]
316pub struct Deck(Vec<Card>);
317
318impl IntoIterator for Deck {
319    type IntoIter = alloc::vec::IntoIter<Card>;
320    type Item = Card;
321
322    #[inline]
323    fn into_iter(self) -> Self::IntoIter {
324        self.0.into_iter()
325    }
326}
327
328impl crate::traits::Deck for Deck {
329    type Card = Card;
330
331    #[inline]
332    fn as_slice(&self) -> &[Self::Card] {
333        &self.0
334    }
335
336    #[inline]
337    fn draw(&mut self) -> Option<Self::Card> {
338        self.0.pop()
339    }
340
341    #[inline]
342    fn new() -> Self {
343        Self(
344            (0..(NUM_RANKS * NUM_SUITS))
345                .map(|value| Card::try_new(value).expect("all deck indices are valid cards"))
346                .collect(),
347        )
348    }
349
350    #[inline]
351    fn remove(&mut self, card: &Self::Card) -> Option<Self::Card> {
352        let index = self.0.iter().position(|candidate| candidate == card)?;
353        Some(self.0.swap_remove(index))
354    }
355
356    #[inline]
357    fn shuffle(&mut self) {
358        self.0.shuffle(&mut rand::rng());
359    }
360}
361
362impl Default for Deck {
363    #[inline]
364    fn default() -> Self {
365        crate::traits::Deck::new()
366    }
367}
368
369impl Deck {
370    /// Create a three card deck for Kuhn Poker.
371    #[inline]
372    #[must_use]
373    pub fn kuhn() -> Self {
374        let inner = vec![
375            Card::from((Rank::King, Suit::Clubs)),
376            Card::from((Rank::Queen, Suit::Clubs)),
377            Card::from((Rank::Jack, Suit::Clubs)),
378        ];
379        Self(inner)
380    }
381
382    /// Create a six card deck for Leduc hold'em.
383    #[inline]
384    #[must_use]
385    pub fn leduc() -> Self {
386        let inner = vec![
387            Card::from((Rank::King, Suit::Clubs)),
388            Card::from((Rank::Queen, Suit::Clubs)),
389            Card::from((Rank::Jack, Suit::Clubs)),
390            Card::from((Rank::King, Suit::Diamonds)),
391            Card::from((Rank::Queen, Suit::Hearts)),
392            Card::from((Rank::Jack, Suit::Spades)),
393        ];
394        Self(inner)
395    }
396}
397
398#[cfg(test)]
399#[allow(clippy::inline_modules, reason = "Unit tests.")]
400mod tests {
401    use super::*;
402    use crate::traits::Deck as _;
403    use alloc::{format, vec};
404
405    const ALL_RANKS: [Rank; 13] = [
406        Rank::Two,
407        Rank::Three,
408        Rank::Four,
409        Rank::Five,
410        Rank::Six,
411        Rank::Seven,
412        Rank::Eight,
413        Rank::Nine,
414        Rank::Ten,
415        Rank::Jack,
416        Rank::Queen,
417        Rank::King,
418        Rank::Ace,
419    ];
420
421    const ALL_SUITS: [Suit; 4] = [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades];
422
423    fn all_cards() -> Vec<Card> {
424        ALL_RANKS
425            .into_iter()
426            .flat_map(|rank| {
427                ALL_SUITS
428                    .into_iter()
429                    .map(move |suit| Card::from((rank, suit)))
430            })
431            .collect()
432    }
433
434    #[test]
435    fn card_display_uses_rank_suit_and_color() {
436        let cases = [
437            (
438                PrettyCard::from((Rank::Two, Suit::Clubs)),
439                "\x1b[32m2c\x1b[0m",
440            ),
441            (
442                PrettyCard::from((Rank::Ten, Suit::Diamonds)),
443                "\x1b[34mTd\x1b[0m",
444            ),
445            (
446                PrettyCard::from((Rank::Queen, Suit::Hearts)),
447                "\x1b[31mQh\x1b[0m",
448            ),
449            (
450                PrettyCard::from((Rank::Ace, Suit::Spades)),
451                "\x1b[33mAs\x1b[0m",
452            ),
453        ];
454
455        for (card, expected) in cases {
456            assert_eq!(format!("{card}"), expected);
457        }
458    }
459
460    #[test]
461    fn card_from_rank_and_suit_encodes_the_card() {
462        let cases = [
463            ((Rank::Two, Suit::Clubs), 0),
464            ((Rank::Two, Suit::Spades), 3),
465            ((Rank::Ten, Suit::Diamonds), 33),
466            ((Rank::Ace, Suit::Clubs), 48),
467            ((Rank::Ace, Suit::Spades), 51),
468        ];
469
470        for ((rank, suit), expected) in cases {
471            assert_eq!(Card::from((rank, suit)).into_inner(), expected);
472        }
473    }
474
475    #[test]
476    fn card_into_u8_returns_the_inner_value() {
477        let card = Card::from((Rank::Ace, Suit::Spades));
478
479        assert_eq!(u8::from(card), 51);
480    }
481
482    #[test]
483    fn card_rank_returns_the_original_rank() {
484        for rank in ALL_RANKS {
485            for suit in ALL_SUITS {
486                let card = Card::from((rank, suit));
487
488                assert_eq!(card.rank(), rank);
489            }
490        }
491    }
492
493    #[test]
494    fn card_suit_returns_the_original_suit() {
495        for rank in ALL_RANKS {
496            for suit in ALL_SUITS {
497                let card = Card::from((rank, suit));
498
499                assert_eq!(card.suit(), suit);
500            }
501        }
502    }
503
504    #[test]
505    fn card_try_from_accepts_valid_cards() {
506        let cases = [
507            ("2c", Card::from((Rank::Two, Suit::Clubs))),
508            ("Td", Card::from((Rank::Ten, Suit::Diamonds))),
509            ("qh", Card::from((Rank::Queen, Suit::Hearts))),
510            ("aS", Card::from((Rank::Ace, Suit::Spades))),
511        ];
512
513        for (text, expected) in cases {
514            assert_eq!(Card::try_from(text), Ok(expected));
515        }
516    }
517
518    #[test]
519    fn card_try_from_rejects_invalid_cards() {
520        let invalid_cards = ["", "A", "Ahh", "10h", "1h", "Ax", "\u{00e9}"];
521
522        for text in invalid_cards {
523            assert_eq!(Card::try_from(text), Err(ParseCardError));
524        }
525    }
526
527    #[test]
528    fn card_try_new_validates_the_inner_value() {
529        assert!(matches!(
530            Card::try_new(0),
531            Ok(card) if card.into_inner() == 0
532        ));
533        assert!(matches!(
534            Card::try_new(51),
535            Ok(card) if card.into_inner() == 51
536        ));
537        _ = Card::try_new(52).expect_err("Out of bounds");
538        _ = Card::try_new(u8::MAX).expect_err("Out of bounds");
539    }
540
541    #[test]
542    fn deck_default_creates_a_standard_deck() {
543        assert_eq!(Deck::default(), Deck::new());
544    }
545
546    #[test]
547    fn deck_draw_removes_cards_from_the_end() {
548        let mut deck = Deck::kuhn();
549
550        assert_eq!(deck.draw(), Some(Card::from((Rank::Jack, Suit::Clubs))));
551        assert_eq!(deck.draw(), Some(Card::from((Rank::Queen, Suit::Clubs))));
552        assert_eq!(deck.draw(), Some(Card::from((Rank::King, Suit::Clubs))));
553        assert_eq!(deck.draw(), None);
554    }
555
556    #[test]
557    fn deck_into_iterator_returns_every_card() {
558        let cards = Deck::new().into_iter().collect::<Vec<_>>();
559
560        assert_eq!(cards, all_cards());
561    }
562
563    #[test]
564    fn deck_iter_borrows_every_card() {
565        let deck = Deck::kuhn();
566        let cards = deck.iter().copied().collect::<Vec<_>>();
567
568        assert_eq!(
569            cards,
570            vec![
571                Card::from((Rank::King, Suit::Clubs)),
572                Card::from((Rank::Queen, Suit::Clubs)),
573                Card::from((Rank::Jack, Suit::Clubs)),
574            ]
575        );
576
577        assert_eq!(deck.len(), 3);
578    }
579
580    #[test]
581    fn deck_kuhn_creates_the_expected_deck() {
582        let cards = Deck::kuhn().into_iter().collect::<Vec<_>>();
583
584        assert_eq!(
585            cards,
586            vec![
587                Card::from((Rank::King, Suit::Clubs)),
588                Card::from((Rank::Queen, Suit::Clubs)),
589                Card::from((Rank::Jack, Suit::Clubs)),
590            ]
591        );
592    }
593
594    #[test]
595    fn deck_leduc_creates_the_expected_deck() {
596        let cards = Deck::leduc().into_iter().collect::<Vec<_>>();
597
598        assert_eq!(
599            cards,
600            vec![
601                Card::from((Rank::King, Suit::Clubs)),
602                Card::from((Rank::Queen, Suit::Clubs)),
603                Card::from((Rank::Jack, Suit::Clubs)),
604                Card::from((Rank::King, Suit::Diamonds)),
605                Card::from((Rank::Queen, Suit::Hearts)),
606                Card::from((Rank::Jack, Suit::Spades)),
607            ]
608        );
609    }
610
611    #[test]
612    fn deck_len_tracks_the_number_of_cards() {
613        let mut deck = Deck::new();
614
615        assert_eq!(deck.len(), 52);
616
617        let _ = deck.draw();
618
619        assert_eq!(deck.len(), 51);
620    }
621
622    #[test]
623    fn deck_new_creates_all_fifty_two_cards_in_encoding_order() {
624        let deck = Deck::new();
625
626        assert_eq!(deck.len(), 52);
627        assert_eq!(deck.iter().copied().collect::<Vec<_>>(), all_cards());
628    }
629
630    #[test]
631    fn deck_remove_returns_and_removes_an_existing_card() {
632        let target = Card::from((Rank::Queen, Suit::Hearts));
633        let mut deck = Deck::new();
634
635        assert_eq!(deck.remove(&target), Some(target));
636        assert_eq!(deck.len(), 51);
637        assert!(!deck.iter().any(|card| *card == target));
638        assert_eq!(deck.remove(&target), None);
639        assert_eq!(deck.len(), 51);
640    }
641
642    #[test]
643    fn deck_shuffle_preserves_every_card() {
644        let mut deck = Deck::new();
645        let mut expected = deck.iter().copied().collect::<Vec<_>>();
646
647        deck.shuffle();
648
649        let mut actual = deck.iter().copied().collect::<Vec<_>>();
650
651        expected.sort_unstable();
652        actual.sort_unstable();
653
654        assert_eq!(actual, expected);
655        assert_eq!(deck.len(), 52);
656    }
657
658    #[test]
659    fn deck_combinations() {
660        let deck = Deck::new();
661        let choose_0: Vec<_> = deck.combinations::<0>().collect();
662        assert_eq!(choose_0.len(), 1);
663        assert_eq!(choose_0.first().expect("Len is positive").len(), 0);
664        let choose_1: Vec<_> = deck.combinations::<1>().collect();
665        assert_eq!(choose_1.len(), 52);
666        assert_eq!(choose_1.first().expect("Len is positive").len(), 1);
667        let choose_2: Vec<_> = deck.combinations::<2>().collect();
668        assert_eq!(choose_2.len(), 1326);
669        assert_eq!(choose_2.first().expect("Len is positive").len(), 2);
670    }
671
672    #[test]
673    fn parse_card_error_display_describes_the_error() {
674        assert_eq!(format!("{ParseCardError}"), "invalid card");
675    }
676
677    #[test]
678    fn public_constants_match_a_standard_deck() {
679        assert_eq!(NUM_RANKS, 13);
680        assert_eq!(NUM_SUITS, 4);
681    }
682
683    #[test]
684    fn rank_display_returns_the_rank_symbol() {
685        let cases = [
686            (Rank::Two, "2"),
687            (Rank::Three, "3"),
688            (Rank::Four, "4"),
689            (Rank::Five, "5"),
690            (Rank::Six, "6"),
691            (Rank::Seven, "7"),
692            (Rank::Eight, "8"),
693            (Rank::Nine, "9"),
694            (Rank::Ten, "T"),
695            (Rank::Jack, "J"),
696            (Rank::Queen, "Q"),
697            (Rank::King, "K"),
698            (Rank::Ace, "A"),
699        ];
700
701        for (rank, expected) in cases {
702            assert_eq!(format!("{rank}"), expected);
703        }
704    }
705
706    #[test]
707    fn rank_from_str_accepts_valid_ranks_case_insensitively() {
708        let cases = [
709            ("2", Rank::Two),
710            ("3", Rank::Three),
711            ("4", Rank::Four),
712            ("5", Rank::Five),
713            ("6", Rank::Six),
714            ("7", Rank::Seven),
715            ("8", Rank::Eight),
716            ("9", Rank::Nine),
717            ("t", Rank::Ten),
718            ("J", Rank::Jack),
719            ("q", Rank::Queen),
720            ("K", Rank::King),
721            ("a", Rank::Ace),
722        ];
723
724        for (text, expected) in cases {
725            assert_eq!(Rank::from_str(text), Ok(expected));
726        }
727
728        assert_eq!(Rank::from_str(""), Err(ParseCardError));
729        assert_eq!(Rank::from_str("10"), Err(ParseCardError));
730        assert_eq!(Rank::from_str("X"), Err(ParseCardError));
731    }
732
733    #[test]
734    fn rank_primitive_conversions_cover_every_rank() {
735        for (value, rank) in [
736            (0, Rank::Two),
737            (1, Rank::Three),
738            (2, Rank::Four),
739            (3, Rank::Five),
740            (4, Rank::Six),
741            (5, Rank::Seven),
742            (6, Rank::Eight),
743            (7, Rank::Nine),
744            (8, Rank::Ten),
745            (9, Rank::Jack),
746            (10, Rank::Queen),
747            (11, Rank::King),
748            (12, Rank::Ace),
749        ] {
750            assert_eq!(u8::from(rank), value);
751            assert!(matches!(
752                Rank::try_from(value),
753                Ok(parsed) if parsed == rank
754            ));
755        }
756
757        _ = Rank::try_from(NUM_RANKS).expect_err("Out of bounds");
758    }
759
760    #[test]
761    fn suit_display_returns_the_suit_symbol() {
762        let cases = [
763            (Suit::Clubs, "c"),
764            (Suit::Diamonds, "d"),
765            (Suit::Hearts, "h"),
766            (Suit::Spades, "s"),
767        ];
768
769        for (suit, expected) in cases {
770            assert_eq!(format!("{suit}"), expected);
771        }
772    }
773
774    #[test]
775    fn suit_from_str_accepts_valid_suits_case_insensitively() {
776        let cases = [
777            ("c", Suit::Clubs),
778            ("D", Suit::Diamonds),
779            ("h", Suit::Hearts),
780            ("S", Suit::Spades),
781        ];
782
783        for (text, expected) in cases {
784            assert_eq!(Suit::from_str(text), Ok(expected));
785        }
786
787        assert_eq!(Suit::from_str(""), Err(ParseCardError));
788        assert_eq!(Suit::from_str("clubs"), Err(ParseCardError));
789        assert_eq!(Suit::from_str("x"), Err(ParseCardError));
790    }
791
792    #[test]
793    fn suit_primitive_conversions_cover_every_suit() {
794        for (value, suit) in [
795            (0, Suit::Clubs),
796            (1, Suit::Diamonds),
797            (2, Suit::Hearts),
798            (3, Suit::Spades),
799        ] {
800            assert_eq!(u8::from(suit), value);
801            assert!(matches!(
802                Suit::try_from(value),
803                Ok(parsed) if parsed == suit
804            ));
805        }
806
807        _ = Suit::try_from(NUM_SUITS).expect_err("Out of bounds");
808    }
809}