#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://codeberg.org/pezcore/playin-cards/raw/branch/main/icon.svg")]
#[allow(unused_imports)]
use std::fmt::{Display, UpperHex};
use itertools::Itertools;
use strum::EnumIter;
use strum::IntoEnumIterator;
pub const CARDS_PER_DECK: u8 = 52;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Card {
Regular { rank: Rank, suit: Suit },
Joker(Color),
}
#[repr(u8)]
#[derive(EnumIter, Debug, Ord, PartialOrd, PartialEq, Clone, Copy, Eq, Hash)]
pub enum Rank {
Two = 2,
Three,
Four,
Five,
Six,
Seven,
Eight,
Nine,
Ten,
Jack,
Queen,
King,
Ace,
}
#[derive(EnumIter, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Suit {
Hearts,
Diamonds,
Spades,
Clubs,
}
#[derive(Debug, PartialEq, Clone, Copy, Eq, Hash)]
pub enum Color {
Red,
Black,
}
mod format;
mod parse;
pub use parse::ParseError;
impl Suit {
pub const ALLOWED_CHARS: &'static str = "SsCcHhDd♠♣♥♦";
pub fn color(self) -> Color {
match self {
Suit::Hearts | Suit::Diamonds => Color::Red,
Suit::Spades | Suit::Clubs => Color::Black,
}
}
}
impl Card {
pub fn color(self) -> Color {
match self {
Self::Regular { rank: _, suit } => suit.color(),
Self::Joker(color) => color,
}
}
pub fn is_joker(self) -> bool {
match self {
Self::Regular { rank: _, suit: _ } => false,
Self::Joker(_) => true,
}
}
}
impl Rank {
pub const ALLOWED_CHARS: &'static str = "A23456789TJQK";
pub fn is_adjacent(self, other: Self) -> bool {
matches!((self as u8).abs_diff(other as u8), 1 | 12)
}
}
impl From<(Rank, Suit)> for Card {
fn from((rank, suit): (Rank, Suit)) -> Self {
Self::Regular { rank, suit }
}
}
pub fn gen_shoe(n_decks: u8, with_jokers: bool) -> Vec<Card> {
let extra_cards: u8 = if with_jokers { 2 } else { 0 };
let total_cards = n_decks as usize * (CARDS_PER_DECK + extra_cards) as usize;
let mut output = Vec::with_capacity(total_cards);
for _ in 0..n_decks {
let deck_iter = Rank::iter().cartesian_product(Suit::iter()).map(Card::from);
output.extend(deck_iter);
if with_jokers {
output.extend([Card::Joker(Color::Red), Card::Joker(Color::Black)]);
}
}
output
}
#[cfg(test)]
mod test {
use super::*;
use itertools::Itertools;
use std::collections::HashMap;
use strum::IntoEnumIterator;
#[rustfmt::skip]
pub const FULL_DECK_CARDS: [Card; 52] = [
Card::Regular{ suit: Suit::Hearts, rank: Rank::Ace },
Card::Regular{ suit: Suit::Hearts, rank: Rank::Two },
Card::Regular{ suit: Suit::Hearts, rank: Rank::Three },
Card::Regular{ suit: Suit::Hearts, rank: Rank::Four },
Card::Regular{ suit: Suit::Hearts, rank: Rank::Five },
Card::Regular{ suit: Suit::Hearts, rank: Rank::Six },
Card::Regular{ suit: Suit::Hearts, rank: Rank::Seven },
Card::Regular{ suit: Suit::Hearts, rank: Rank::Eight },
Card::Regular{ suit: Suit::Hearts, rank: Rank::Nine },
Card::Regular{ suit: Suit::Hearts, rank: Rank::Ten },
Card::Regular{ suit: Suit::Hearts, rank: Rank::Jack },
Card::Regular{ suit: Suit::Hearts, rank: Rank::Queen },
Card::Regular{ suit: Suit::Hearts, rank: Rank::King },
Card::Regular{ suit: Suit::Diamonds, rank: Rank::Ace },
Card::Regular{ suit: Suit::Diamonds, rank: Rank::Two },
Card::Regular{ suit: Suit::Diamonds, rank: Rank::Three },
Card::Regular{ suit: Suit::Diamonds, rank: Rank::Four },
Card::Regular{ suit: Suit::Diamonds, rank: Rank::Five },
Card::Regular{ suit: Suit::Diamonds, rank: Rank::Six },
Card::Regular{ suit: Suit::Diamonds, rank: Rank::Seven },
Card::Regular{ suit: Suit::Diamonds, rank: Rank::Eight },
Card::Regular{ suit: Suit::Diamonds, rank: Rank::Nine },
Card::Regular{ suit: Suit::Diamonds, rank: Rank::Ten },
Card::Regular{ suit: Suit::Diamonds, rank: Rank::Jack },
Card::Regular{ suit: Suit::Diamonds, rank: Rank::Queen },
Card::Regular{ suit: Suit::Diamonds, rank: Rank::King },
Card::Regular{ suit: Suit::Spades, rank: Rank::Ace },
Card::Regular{ suit: Suit::Spades, rank: Rank::Two },
Card::Regular{ suit: Suit::Spades, rank: Rank::Three },
Card::Regular{ suit: Suit::Spades, rank: Rank::Four },
Card::Regular{ suit: Suit::Spades, rank: Rank::Five },
Card::Regular{ suit: Suit::Spades, rank: Rank::Six },
Card::Regular{ suit: Suit::Spades, rank: Rank::Seven },
Card::Regular{ suit: Suit::Spades, rank: Rank::Eight },
Card::Regular{ suit: Suit::Spades, rank: Rank::Nine },
Card::Regular{ suit: Suit::Spades, rank: Rank::Ten },
Card::Regular{ suit: Suit::Spades, rank: Rank::Jack },
Card::Regular{ suit: Suit::Spades, rank: Rank::Queen },
Card::Regular{ suit: Suit::Spades, rank: Rank::King },
Card::Regular{ suit: Suit::Clubs, rank: Rank::Ace },
Card::Regular{ suit: Suit::Clubs, rank: Rank::Two },
Card::Regular{ suit: Suit::Clubs, rank: Rank::Three },
Card::Regular{ suit: Suit::Clubs, rank: Rank::Four },
Card::Regular{ suit: Suit::Clubs, rank: Rank::Five },
Card::Regular{ suit: Suit::Clubs, rank: Rank::Six },
Card::Regular{ suit: Suit::Clubs, rank: Rank::Seven },
Card::Regular{ suit: Suit::Clubs, rank: Rank::Eight },
Card::Regular{ suit: Suit::Clubs, rank: Rank::Nine },
Card::Regular{ suit: Suit::Clubs, rank: Rank::Ten },
Card::Regular{ suit: Suit::Clubs, rank: Rank::Jack },
Card::Regular{ suit: Suit::Clubs, rank: Rank::Queen },
Card::Regular{ suit: Suit::Clubs, rank: Rank::King },
];
#[test]
fn rank_cast_u8() {
assert_eq!(Rank::Ace as u8, 14, "Ace as u8 was not 14");
assert_eq!(Rank::Two as u8, 2, "Two as u8 was not 2");
assert_eq!(Rank::Three as u8, 3, "Three as u8 was not 3");
assert_eq!(Rank::Four as u8, 4, "Four as u8 was not 4");
assert_eq!(Rank::Five as u8, 5, "Five as u8 was not 5");
assert_eq!(Rank::Six as u8, 6, "Six as u8 was not 6");
assert_eq!(Rank::Seven as u8, 7, "Seven as u8 was not 7");
assert_eq!(Rank::Eight as u8, 8, "Eight as u8 was not 8");
assert_eq!(Rank::Nine as u8, 9, "Nine as u8 was not 9");
assert_eq!(Rank::Ten as u8, 10, "Ten as u8 was not 10");
assert_eq!(Rank::Jack as u8, 11, "Jack as u8 was not 11");
assert_eq!(Rank::Queen as u8, 12, "Queen as u8 was not 12");
assert_eq!(Rank::King as u8, 13, "King as u8 was not 13");
}
#[test]
fn suit_colors() {
assert_eq!(Suit::Spades.color(), Color::Black);
assert_eq!(Suit::Hearts.color(), Color::Red);
assert_eq!(Suit::Clubs.color(), Color::Black);
assert_eq!(Suit::Diamonds.color(), Color::Red);
}
#[test]
fn card_colors() {
for card in FULL_DECK_CARDS[..26].iter() {
assert_eq!(
card.color(),
Color::Red,
"{} incorrectly identified as Red",
card
);
}
for card in FULL_DECK_CARDS[26..].iter() {
assert_eq!(
card.color(),
Color::Black,
"{} incorrectly identified as Black",
card
);
}
assert_eq!(Card::Joker(Color::Black).color(), Color::Black);
assert_eq!(Card::Joker(Color::Red).color(), Color::Red);
}
#[test]
fn is_joker() {
for card in FULL_DECK_CARDS.iter() {
assert!(!card.is_joker(), "{} incorrectly identified as joker", card)
}
assert!(Card::Joker(Color::Black).is_joker());
assert!(Card::Joker(Color::Red).is_joker());
}
fn count(shoe: &[Card]) -> HashMap<Card, u16> {
let mut map = HashMap::new();
for &card in shoe {
*map.entry(card).or_insert(0) += 1;
}
map
}
fn test_shoe(n: u8, w_jokers: bool) {
let shoe = gen_shoe(n, w_jokers);
let n_jokers = if w_jokers { 2 * n } else { 0 } as usize;
assert_eq!(shoe.len(), CARDS_PER_DECK as usize * n as usize + n_jokers);
let map = count(&shoe);
for (rank, suit) in Rank::iter().cartesian_product(Suit::iter()) {
assert_eq!(map.get(&Card::Regular { rank, suit }), Some(&(n as u16)));
}
if w_jokers {
assert_eq!(map.get(&Card::Joker(Color::Black)), Some(&(n as u16)));
assert_eq!(map.get(&Card::Joker(Color::Red)), Some(&(n as u16)));
}
}
#[test]
fn gen_shoe_no_jokers_single() {
test_shoe(1, false);
}
#[test]
fn gen_shoe_no_jokers_multi() {
test_shoe(6, false);
}
#[test]
fn gen_shoe_w_jokers() {
test_shoe(1, true);
}
#[test]
fn gen_shoe_w_jokers_multi() {
test_shoe(4, true);
}
#[rustfmt::skip]
#[test]
fn is_adjacent() {
for (r1, r2) in Rank::iter().cartesian_product(Rank::iter()) {
match (r1, r2) {
(Rank::Ace, Rank::Two) | (Rank::Two, Rank::Ace) => assert!(r1.is_adjacent(r2)),
(Rank::Two, Rank::Three) | (Rank::Three, Rank::Two) => assert!(r1.is_adjacent(r2)),
(Rank::Three, Rank::Four) | (Rank::Four, Rank::Three) => assert!(r1.is_adjacent(r2)),
(Rank::Four, Rank::Five) | (Rank::Five, Rank::Four) => assert!(r1.is_adjacent(r2)),
(Rank::Five, Rank::Six) | (Rank::Six, Rank::Five) => assert!(r1.is_adjacent(r2)),
(Rank::Six, Rank::Seven) | (Rank::Seven, Rank::Six) => assert!(r1.is_adjacent(r2)),
(Rank::Seven, Rank::Eight) | (Rank::Eight, Rank::Seven) => assert!(r1.is_adjacent(r2)),
(Rank::Eight, Rank::Nine) | (Rank::Nine, Rank::Eight) => assert!(r1.is_adjacent(r2)),
(Rank::Nine, Rank::Ten) | (Rank::Ten, Rank::Nine) => assert!(r1.is_adjacent(r2)),
(Rank::Ten, Rank::Jack) | (Rank::Jack, Rank::Ten) => assert!(r1.is_adjacent(r2)),
(Rank::Jack, Rank::Queen) | (Rank::Queen, Rank::Jack) => assert!(r1.is_adjacent(r2)),
(Rank::Queen, Rank::King) | (Rank::King, Rank::Queen) => assert!(r1.is_adjacent(r2)),
(Rank::King, Rank::Ace) | (Rank::Ace, Rank::King) => assert!(r1.is_adjacent(r2)),
_ => assert!(!r1.is_adjacent(r2)),
}
}
}
}