card: a sufficient API for cards

The mathematical result that underpins this is a bijective map between
the positive integers and a unique playing card.
This commit is contained in:
2026-03-31 20:56:52 +01:00
committed by oreodave
parent f112f9ed0c
commit 3259448dca

154
src/card.rs Normal file
View File

@@ -0,0 +1,154 @@
use std::cmp::Ordering;
use std::convert::TryFrom;
use std::hash::{Hash, Hasher};
#[derive(Debug, Copy, Clone)]
pub enum Rank {
Three = 0,
Four,
Five,
Six,
Seven,
Eight,
Nine,
Ten,
Jack,
Queen,
King,
Ace,
Two,
}
#[derive(Debug, Copy, Clone)]
pub enum Suit {
Diamond = 0,
Club,
Heart,
Spade,
}
#[derive(Debug, Clone, Copy)]
pub enum Card {
Joker(i64),
PlayingCard(i64, Rank, Suit),
}
impl Card {
fn is_joker(&self) -> bool {
matches!(self, Card::Joker(_))
}
}
pub fn make_decks(number_of_decks: usize) -> Vec<Card> {
let number_of_decks: i64 = number_of_decks.try_into().unwrap();
(-(number_of_decks * 2)..(52 * number_of_decks))
.map(Card::from)
.collect::<Vec<Card>>()
}
mod impls {
use super::*;
impl TryFrom<i64> for Rank {
type Error = ();
fn try_from(value: i64) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::Three),
1 => Ok(Self::Four),
2 => Ok(Self::Five),
3 => Ok(Self::Six),
4 => Ok(Self::Seven),
5 => Ok(Self::Eight),
6 => Ok(Self::Nine),
7 => Ok(Self::Ten),
8 => Ok(Self::Jack),
9 => Ok(Self::Queen),
10 => Ok(Self::King),
11 => Ok(Self::Ace),
12 => Ok(Self::Two),
_ => Err(()),
}
}
}
impl TryFrom<i64> for Suit {
type Error = ();
fn try_from(value: i64) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::Diamond),
1 => Ok(Self::Club),
2 => Ok(Self::Heart),
3 => Ok(Self::Spade),
_ => Err(()),
}
}
}
impl From<i64> for Card {
fn from(n: i64) -> Card {
if n < 0 {
Card::Joker(n)
} else {
let deck = n / 52;
let n = n % 52;
// NOTE: If only Rust had Ada-like numeric contracts, this wouldn't
// be necessary; n >= 0 => n % 52 in [0, 51] so Rank::try_from and
// Suit::try_from will always succeed
let rank = Rank::try_from(n / 4).unwrap();
let suit = Suit::try_from(n % 4).unwrap();
Card::PlayingCard(deck, rank, suit)
}
}
}
impl From<Card> for i64 {
fn from(card: Card) -> i64 {
match card {
Card::Joker(x) => x,
Card::PlayingCard(deck, rank, suit) => {
(deck * 52) + ((rank as i64) * 4) + (suit as i64)
}
}
}
}
impl PartialEq for Card {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for Card {}
impl Ord for Card {
fn cmp(&self, other: &Self) -> Ordering {
if self.is_joker() && other.is_joker() {
Ordering::Equal
} else if self.is_joker() {
Ordering::Less
} else if other.is_joker() {
Ordering::Greater
} else {
let self_val = i64::from(*self) % 52;
let other_val = i64::from(*other) % 52;
self_val.cmp(&other_val)
}
}
}
impl Hash for Card {
fn hash<H: Hasher>(&self, state: &mut H) {
// NOTE: We're using the i64 conversion of card for the hash since that
// should generate unique numbers per card.
i64::from(*self).hash(state);
}
}
impl PartialOrd for Card {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
}