#![allow(
clippy::exhaustive_enums,
reason = "Nutype generates exhaustive validation error enums."
)]
use crate::traits::Card as _;
use core::fmt::Display;
use core::{ops::Div as _, str::FromStr};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use nutype::nutype;
use rand::seq::SliceRandom as _;
use thiserror::Error;
pub const NUM_RANKS: u8 = 13;
pub const NUM_SUITS: u8 = 4;
const RANKS: &str = "23456789TJQKA";
const SUITS: &str = "cdhs";
#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
#[error("invalid card")]
#[non_exhaustive]
pub struct ParseCardError;
#[nutype(
new_unchecked,
validate(less = (NUM_RANKS * NUM_SUITS)),
derive(Clone, Copy, Debug, Eq, Hash, Into, PartialEq, PartialOrd, Ord),
)]
pub struct Card(u8);
impl Display for Card {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let rank = self.rank();
let suit = self.suit();
write!(f, "{rank}{suit}")
}
}
impl TryFrom<&str> for Card {
type Error = ParseCardError;
#[inline]
fn try_from(value: &str) -> Result<Self, Self::Error> {
if value.len() != 2 || !value.is_ascii() {
return Err(ParseCardError);
}
let (rank, suit) = value.split_at(1);
let rank = Rank::from_str(rank)?;
let suit = Suit::from_str(suit)?;
Ok((rank, suit).into())
}
}
impl From<(Rank, Suit)> for Card {
#[inline]
fn from(value: (Rank, Suit)) -> Self {
let (rank, suit) = value;
unsafe { Self::new_unchecked(rank as u8 * NUM_SUITS + suit as u8) }
}
}
impl crate::traits::Card for Card {
type Rank = Rank;
type Suit = Suit;
#[inline]
fn rank(self) -> Self::Rank {
Self::Rank::try_from(self.into_inner().div(NUM_SUITS))
.expect("A valid card should have a valid rank")
}
#[inline]
fn suit(self) -> Self::Suit {
Self::Suit::try_from(self.into_inner() % NUM_SUITS)
.expect("A valid card should have a valid suit")
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd, Eq, Ord)]
pub struct PrettyCard(Card);
impl crate::traits::PrettyCard for PrettyCard {
type Card = Card;
}
impl crate::traits::Card for PrettyCard {
type Rank = Rank;
type Suit = Suit;
#[inline]
fn rank(self) -> Self::Rank {
self.0.rank()
}
#[inline]
fn suit(self) -> Self::Suit {
self.0.suit()
}
}
impl From<Card> for PrettyCard {
#[inline]
fn from(value: Card) -> Self {
Self(value)
}
}
impl From<(Rank, Suit)> for PrettyCard {
#[inline]
fn from(value: (Rank, Suit)) -> Self {
Card::from(value).into()
}
}
impl From<PrettyCard> for u8 {
#[inline]
fn from(value: PrettyCard) -> Self {
value.0.into()
}
}
impl TryFrom<&str> for PrettyCard {
type Error = ParseCardError;
#[inline]
fn try_from(value: &str) -> Result<Self, Self::Error> {
let card = Card::try_from(value)?;
Ok(Self(card))
}
}
impl Display for PrettyCard {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let suit = self.suit();
write!(f, "{}{}{}", suit.color(), self.0, Suit::clear_color())
}
}
#[allow(
clippy::arbitrary_source_item_ordering,
reason = "Ranks should be ordered by their semantic value."
)]
#[allow(
clippy::exhaustive_enums,
reason = "The set of ranks in a standard 52-card deck is fixed and complete"
)]
#[derive(
Clone, Copy, Debug, Eq, Hash, IntoPrimitive, Ord, PartialEq, PartialOrd, TryFromPrimitive,
)]
#[repr(u8)]
pub enum Rank {
Two = 0,
Three = 1,
Four = 2,
Five = 3,
Six = 4,
Seven = 5,
Eight = 6,
Nine = 7,
Ten = 8,
Jack = 9,
Queen = 10,
King = 11,
Ace = 12,
}
impl Display for Rank {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let index = usize::from(u8::from(*self));
let rank = char::from(
RANKS
.as_bytes()
.get(index)
.expect("Index should be valid")
.to_owned(),
);
f.write_fmt(format_args!("{rank}"))
}
}
impl FromStr for Rank {
type Err = ParseCardError;
#[inline]
fn from_str(value: &str) -> Result<Self, Self::Err> {
let [rank] = value.as_bytes() else {
return Err(ParseCardError);
};
match rank.to_ascii_uppercase() {
b'2' => Ok(Self::Two),
b'3' => Ok(Self::Three),
b'4' => Ok(Self::Four),
b'5' => Ok(Self::Five),
b'6' => Ok(Self::Six),
b'7' => Ok(Self::Seven),
b'8' => Ok(Self::Eight),
b'9' => Ok(Self::Nine),
b'T' => Ok(Self::Ten),
b'J' => Ok(Self::Jack),
b'Q' => Ok(Self::Queen),
b'K' => Ok(Self::King),
b'A' => Ok(Self::Ace),
_ => Err(ParseCardError),
}
}
}
#[allow(
clippy::arbitrary_source_item_ordering,
reason = "Suits should be ordered by their semantic value."
)]
#[allow(
clippy::exhaustive_enums,
reason = "The set of suits in a standard 52-card deck is fixed and complete"
)]
#[derive(
Clone, Copy, Debug, Eq, Hash, IntoPrimitive, Ord, PartialEq, PartialOrd, TryFromPrimitive,
)]
#[repr(u8)]
pub enum Suit {
Clubs = 0,
Diamonds = 1,
Hearts = 2,
Spades = 3,
}
impl Display for Suit {
#[inline]
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let index = usize::from(u8::from(*self));
let suit = char::from(
SUITS
.as_bytes()
.get(index)
.expect("Index should be valid")
.to_owned(),
);
f.write_fmt(format_args!("{suit}"))
}
}
impl FromStr for Suit {
type Err = ParseCardError;
#[inline]
fn from_str(value: &str) -> Result<Self, Self::Err> {
let [suit] = value.as_bytes() else {
return Err(ParseCardError);
};
match suit.to_ascii_lowercase() {
b'c' => Ok(Self::Clubs),
b'd' => Ok(Self::Diamonds),
b'h' => Ok(Self::Hearts),
b's' => Ok(Self::Spades),
_ => Err(ParseCardError),
}
}
}
impl Suit {
#[must_use]
const fn clear_color() -> &'static str {
"\x1b[0m"
}
#[must_use]
const fn color(self) -> &'static str {
match self {
Self::Clubs => "\x1b[32m",
Self::Diamonds => "\x1b[34m",
Self::Hearts => "\x1b[31m",
Self::Spades => "\x1b[33m",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Deck(Vec<Card>);
impl IntoIterator for Deck {
type IntoIter = alloc::vec::IntoIter<Card>;
type Item = Card;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl crate::traits::Deck for Deck {
type Card = Card;
#[inline]
fn as_slice(&self) -> &[Self::Card] {
&self.0
}
#[inline]
fn draw(&mut self) -> Option<Self::Card> {
self.0.pop()
}
#[inline]
fn new() -> Self {
Self(
(0..(NUM_RANKS * NUM_SUITS))
.map(|value| Card::try_new(value).expect("all deck indices are valid cards"))
.collect(),
)
}
#[inline]
fn remove(&mut self, card: &Self::Card) -> Option<Self::Card> {
let index = self.0.iter().position(|candidate| candidate == card)?;
Some(self.0.swap_remove(index))
}
#[inline]
fn shuffle(&mut self) {
self.0.shuffle(&mut rand::rng());
}
}
impl Default for Deck {
#[inline]
fn default() -> Self {
crate::traits::Deck::new()
}
}
impl Deck {
#[inline]
#[must_use]
pub fn kuhn() -> Self {
let inner = vec![
Card::from((Rank::King, Suit::Clubs)),
Card::from((Rank::Queen, Suit::Clubs)),
Card::from((Rank::Jack, Suit::Clubs)),
];
Self(inner)
}
#[inline]
#[must_use]
pub fn leduc() -> Self {
let inner = vec![
Card::from((Rank::King, Suit::Clubs)),
Card::from((Rank::Queen, Suit::Clubs)),
Card::from((Rank::Jack, Suit::Clubs)),
Card::from((Rank::King, Suit::Diamonds)),
Card::from((Rank::Queen, Suit::Hearts)),
Card::from((Rank::Jack, Suit::Spades)),
];
Self(inner)
}
}
#[cfg(test)]
#[allow(clippy::inline_modules, reason = "Unit tests.")]
mod tests {
use super::*;
use crate::traits::Deck as _;
use alloc::{format, vec};
const ALL_RANKS: [Rank; 13] = [
Rank::Two,
Rank::Three,
Rank::Four,
Rank::Five,
Rank::Six,
Rank::Seven,
Rank::Eight,
Rank::Nine,
Rank::Ten,
Rank::Jack,
Rank::Queen,
Rank::King,
Rank::Ace,
];
const ALL_SUITS: [Suit; 4] = [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades];
fn all_cards() -> Vec<Card> {
ALL_RANKS
.into_iter()
.flat_map(|rank| {
ALL_SUITS
.into_iter()
.map(move |suit| Card::from((rank, suit)))
})
.collect()
}
#[test]
fn card_display_uses_rank_suit_and_color() {
let cases = [
(
PrettyCard::from((Rank::Two, Suit::Clubs)),
"\x1b[32m2c\x1b[0m",
),
(
PrettyCard::from((Rank::Ten, Suit::Diamonds)),
"\x1b[34mTd\x1b[0m",
),
(
PrettyCard::from((Rank::Queen, Suit::Hearts)),
"\x1b[31mQh\x1b[0m",
),
(
PrettyCard::from((Rank::Ace, Suit::Spades)),
"\x1b[33mAs\x1b[0m",
),
];
for (card, expected) in cases {
assert_eq!(format!("{card}"), expected);
}
}
#[test]
fn card_from_rank_and_suit_encodes_the_card() {
let cases = [
((Rank::Two, Suit::Clubs), 0),
((Rank::Two, Suit::Spades), 3),
((Rank::Ten, Suit::Diamonds), 33),
((Rank::Ace, Suit::Clubs), 48),
((Rank::Ace, Suit::Spades), 51),
];
for ((rank, suit), expected) in cases {
assert_eq!(Card::from((rank, suit)).into_inner(), expected);
}
}
#[test]
fn card_into_u8_returns_the_inner_value() {
let card = Card::from((Rank::Ace, Suit::Spades));
assert_eq!(u8::from(card), 51);
}
#[test]
fn card_rank_returns_the_original_rank() {
for rank in ALL_RANKS {
for suit in ALL_SUITS {
let card = Card::from((rank, suit));
assert_eq!(card.rank(), rank);
}
}
}
#[test]
fn card_suit_returns_the_original_suit() {
for rank in ALL_RANKS {
for suit in ALL_SUITS {
let card = Card::from((rank, suit));
assert_eq!(card.suit(), suit);
}
}
}
#[test]
fn card_try_from_accepts_valid_cards() {
let cases = [
("2c", Card::from((Rank::Two, Suit::Clubs))),
("Td", Card::from((Rank::Ten, Suit::Diamonds))),
("qh", Card::from((Rank::Queen, Suit::Hearts))),
("aS", Card::from((Rank::Ace, Suit::Spades))),
];
for (text, expected) in cases {
assert_eq!(Card::try_from(text), Ok(expected));
}
}
#[test]
fn card_try_from_rejects_invalid_cards() {
let invalid_cards = ["", "A", "Ahh", "10h", "1h", "Ax", "\u{00e9}"];
for text in invalid_cards {
assert_eq!(Card::try_from(text), Err(ParseCardError));
}
}
#[test]
fn card_try_new_validates_the_inner_value() {
assert!(matches!(
Card::try_new(0),
Ok(card) if card.into_inner() == 0
));
assert!(matches!(
Card::try_new(51),
Ok(card) if card.into_inner() == 51
));
_ = Card::try_new(52).expect_err("Out of bounds");
_ = Card::try_new(u8::MAX).expect_err("Out of bounds");
}
#[test]
fn deck_default_creates_a_standard_deck() {
assert_eq!(Deck::default(), Deck::new());
}
#[test]
fn deck_draw_removes_cards_from_the_end() {
let mut deck = Deck::kuhn();
assert_eq!(deck.draw(), Some(Card::from((Rank::Jack, Suit::Clubs))));
assert_eq!(deck.draw(), Some(Card::from((Rank::Queen, Suit::Clubs))));
assert_eq!(deck.draw(), Some(Card::from((Rank::King, Suit::Clubs))));
assert_eq!(deck.draw(), None);
}
#[test]
fn deck_into_iterator_returns_every_card() {
let cards = Deck::new().into_iter().collect::<Vec<_>>();
assert_eq!(cards, all_cards());
}
#[test]
fn deck_iter_borrows_every_card() {
let deck = Deck::kuhn();
let cards = deck.iter().copied().collect::<Vec<_>>();
assert_eq!(
cards,
vec![
Card::from((Rank::King, Suit::Clubs)),
Card::from((Rank::Queen, Suit::Clubs)),
Card::from((Rank::Jack, Suit::Clubs)),
]
);
assert_eq!(deck.len(), 3);
}
#[test]
fn deck_kuhn_creates_the_expected_deck() {
let cards = Deck::kuhn().into_iter().collect::<Vec<_>>();
assert_eq!(
cards,
vec![
Card::from((Rank::King, Suit::Clubs)),
Card::from((Rank::Queen, Suit::Clubs)),
Card::from((Rank::Jack, Suit::Clubs)),
]
);
}
#[test]
fn deck_leduc_creates_the_expected_deck() {
let cards = Deck::leduc().into_iter().collect::<Vec<_>>();
assert_eq!(
cards,
vec![
Card::from((Rank::King, Suit::Clubs)),
Card::from((Rank::Queen, Suit::Clubs)),
Card::from((Rank::Jack, Suit::Clubs)),
Card::from((Rank::King, Suit::Diamonds)),
Card::from((Rank::Queen, Suit::Hearts)),
Card::from((Rank::Jack, Suit::Spades)),
]
);
}
#[test]
fn deck_len_tracks_the_number_of_cards() {
let mut deck = Deck::new();
assert_eq!(deck.len(), 52);
let _ = deck.draw();
assert_eq!(deck.len(), 51);
}
#[test]
fn deck_new_creates_all_fifty_two_cards_in_encoding_order() {
let deck = Deck::new();
assert_eq!(deck.len(), 52);
assert_eq!(deck.iter().copied().collect::<Vec<_>>(), all_cards());
}
#[test]
fn deck_remove_returns_and_removes_an_existing_card() {
let target = Card::from((Rank::Queen, Suit::Hearts));
let mut deck = Deck::new();
assert_eq!(deck.remove(&target), Some(target));
assert_eq!(deck.len(), 51);
assert!(!deck.iter().any(|card| *card == target));
assert_eq!(deck.remove(&target), None);
assert_eq!(deck.len(), 51);
}
#[test]
fn deck_shuffle_preserves_every_card() {
let mut deck = Deck::new();
let mut expected = deck.iter().copied().collect::<Vec<_>>();
deck.shuffle();
let mut actual = deck.iter().copied().collect::<Vec<_>>();
expected.sort_unstable();
actual.sort_unstable();
assert_eq!(actual, expected);
assert_eq!(deck.len(), 52);
}
#[test]
fn deck_combinations() {
let deck = Deck::new();
let choose_0: Vec<_> = deck.combinations::<0>().collect();
assert_eq!(choose_0.len(), 1);
assert_eq!(choose_0.first().expect("Len is positive").len(), 0);
let choose_1: Vec<_> = deck.combinations::<1>().collect();
assert_eq!(choose_1.len(), 52);
assert_eq!(choose_1.first().expect("Len is positive").len(), 1);
let choose_2: Vec<_> = deck.combinations::<2>().collect();
assert_eq!(choose_2.len(), 1326);
assert_eq!(choose_2.first().expect("Len is positive").len(), 2);
}
#[test]
fn parse_card_error_display_describes_the_error() {
assert_eq!(format!("{ParseCardError}"), "invalid card");
}
#[test]
fn public_constants_match_a_standard_deck() {
assert_eq!(NUM_RANKS, 13);
assert_eq!(NUM_SUITS, 4);
}
#[test]
fn rank_display_returns_the_rank_symbol() {
let cases = [
(Rank::Two, "2"),
(Rank::Three, "3"),
(Rank::Four, "4"),
(Rank::Five, "5"),
(Rank::Six, "6"),
(Rank::Seven, "7"),
(Rank::Eight, "8"),
(Rank::Nine, "9"),
(Rank::Ten, "T"),
(Rank::Jack, "J"),
(Rank::Queen, "Q"),
(Rank::King, "K"),
(Rank::Ace, "A"),
];
for (rank, expected) in cases {
assert_eq!(format!("{rank}"), expected);
}
}
#[test]
fn rank_from_str_accepts_valid_ranks_case_insensitively() {
let cases = [
("2", Rank::Two),
("3", Rank::Three),
("4", Rank::Four),
("5", Rank::Five),
("6", Rank::Six),
("7", Rank::Seven),
("8", Rank::Eight),
("9", Rank::Nine),
("t", Rank::Ten),
("J", Rank::Jack),
("q", Rank::Queen),
("K", Rank::King),
("a", Rank::Ace),
];
for (text, expected) in cases {
assert_eq!(Rank::from_str(text), Ok(expected));
}
assert_eq!(Rank::from_str(""), Err(ParseCardError));
assert_eq!(Rank::from_str("10"), Err(ParseCardError));
assert_eq!(Rank::from_str("X"), Err(ParseCardError));
}
#[test]
fn rank_primitive_conversions_cover_every_rank() {
for (value, rank) in [
(0, Rank::Two),
(1, Rank::Three),
(2, Rank::Four),
(3, Rank::Five),
(4, Rank::Six),
(5, Rank::Seven),
(6, Rank::Eight),
(7, Rank::Nine),
(8, Rank::Ten),
(9, Rank::Jack),
(10, Rank::Queen),
(11, Rank::King),
(12, Rank::Ace),
] {
assert_eq!(u8::from(rank), value);
assert!(matches!(
Rank::try_from(value),
Ok(parsed) if parsed == rank
));
}
_ = Rank::try_from(NUM_RANKS).expect_err("Out of bounds");
}
#[test]
fn suit_display_returns_the_suit_symbol() {
let cases = [
(Suit::Clubs, "c"),
(Suit::Diamonds, "d"),
(Suit::Hearts, "h"),
(Suit::Spades, "s"),
];
for (suit, expected) in cases {
assert_eq!(format!("{suit}"), expected);
}
}
#[test]
fn suit_from_str_accepts_valid_suits_case_insensitively() {
let cases = [
("c", Suit::Clubs),
("D", Suit::Diamonds),
("h", Suit::Hearts),
("S", Suit::Spades),
];
for (text, expected) in cases {
assert_eq!(Suit::from_str(text), Ok(expected));
}
assert_eq!(Suit::from_str(""), Err(ParseCardError));
assert_eq!(Suit::from_str("clubs"), Err(ParseCardError));
assert_eq!(Suit::from_str("x"), Err(ParseCardError));
}
#[test]
fn suit_primitive_conversions_cover_every_suit() {
for (value, suit) in [
(0, Suit::Clubs),
(1, Suit::Diamonds),
(2, Suit::Hearts),
(3, Suit::Spades),
] {
assert_eq!(u8::from(suit), value);
assert!(matches!(
Suit::try_from(value),
Ok(parsed) if parsed == suit
));
}
_ = Suit::try_from(NUM_SUITS).expect_err("Out of bounds");
}
}