1#![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
16pub const NUM_RANKS: u8 = 13;
18pub const NUM_SUITS: u8 = 4;
20
21const RANKS: &str = "23456789TJQKA";
22const SUITS: &str = "cdhs";
23
24#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
26#[error("invalid card")]
27#[non_exhaustive]
28pub struct ParseCardError;
29
30#[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 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#[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#[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 Two = 0,
170 Three = 1,
172 Four = 2,
174 Five = 3,
176 Six = 4,
178 Seven = 5,
180 Eight = 6,
182 Nine = 7,
184 Ten = 8,
186 Jack = 9,
188 Queen = 10,
190 King = 11,
192 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#[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 Clubs = 0,
255 Diamonds = 1,
257 Hearts = 2,
259 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#[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 #[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 #[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}