refactor CardValue into Rank enum

This commit is contained in:
2026-05-18 11:01:48 -07:00
parent ab4348775b
commit 5f81f28160
3 changed files with 109 additions and 59 deletions
+95 -45
View File
@@ -16,6 +16,27 @@ pub trait Game {
fn is_win(&self) -> bool;
}
/// card_game supports up to 4 identifiably separate decks.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Deck {
Deck1,
Deck2,
Deck3,
Deck4,
}
impl Deck {
pub const fn new(deck: u8) -> Option<Self> {
use Deck::*;
Some(match deck {
1 => Deck1,
2 => Deck2,
3 => Deck3,
4 => Deck4,
_ => return None,
})
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Suit {
Spades = 0b00,
@@ -36,38 +57,66 @@ impl Suit {
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct CardValue(u8);
impl CardValue {
pub const ACE: Self = CardValue(1);
pub const TWO: Self = CardValue(2);
pub const THREE: Self = CardValue(3);
pub const FOUR: Self = CardValue(4);
pub const FIVE: Self = CardValue(5);
pub const SIX: Self = CardValue(6);
pub const SEVEN: Self = CardValue(7);
pub const EIGHT: Self = CardValue(8);
pub const NINE: Self = CardValue(9);
pub const TEN: Self = CardValue(10);
pub const JACK: Self = CardValue(11);
pub const QUEEN: Self = CardValue(12);
pub const KING: Self = CardValue(13);
pub fn get(self) -> u8 {
self.0
pub enum Rank {
Ace = 1,
Two = 2,
Three = 3,
Four = 4,
Five = 5,
Six = 6,
Seven = 7,
Eight = 8,
Nine = 9,
Ten = 10,
Jack = 11,
Queen = 12,
King = 13,
}
pub fn checked_add(self, offset: u8) -> Option<CardValue> {
let new_value = self.0.checked_add(offset)?;
if 13 < new_value {
None
} else {
Some(CardValue(new_value))
impl Rank {
pub const RANKS: [Self; 13] = [
Self::Ace,
Self::Two,
Self::Three,
Self::Four,
Self::Five,
Self::Six,
Self::Seven,
Self::Eight,
Self::Nine,
Self::Ten,
Self::Jack,
Self::Queen,
Self::King,
];
pub const fn new(rank: u8) -> Option<Self> {
use Rank::*;
Some(match rank {
1 => Ace,
2 => Two,
3 => Three,
4 => Four,
5 => Five,
6 => Six,
7 => Seven,
8 => Eight,
9 => Nine,
10 => Ten,
11 => Jack,
12 => Queen,
13 => King,
_ => return None,
})
}
pub const fn checked_add(self, offset: u8) -> Option<Rank> {
match (self as u8).checked_add(offset) {
Some(rank) => Self::new(rank),
None => None,
}
}
pub fn checked_sub(self, offset: u8) -> Option<CardValue> {
let new_value = self.0.checked_sub(offset)?;
if new_value < 1 {
None
} else {
Some(CardValue(new_value))
pub const fn checked_sub(self, offset: u8) -> Option<Rank> {
match (self as u8).checked_sub(offset) {
Some(rank) => Self::new(rank),
None => None,
}
}
}
@@ -77,16 +126,17 @@ impl CardValue {
/// 4 bits for card Value
/// TODO: better encoding for slightly more decks
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Card(u8);
pub struct Card(core::num::NonZeroU8);
impl Card {
pub fn new(deck: u8, suit: Suit, CardValue(value): CardValue) -> Self {
Self(deck << 6 | (suit as u8) << 4 | value)
pub const fn new(deck: Deck, suit: Suit, rank: Rank) -> Self {
let packed = (deck as u8) << 6 | (suit as u8) << 4 | (rank as u8);
Self(core::num::NonZeroU8::new(packed).unwrap())
}
pub fn value(&self) -> CardValue {
let masked = self.0 & 0b1111;
CardValue(masked)
pub const fn rank(&self) -> Rank {
let masked = self.0.get() & 0b1111;
Rank::new(masked).unwrap()
}
pub fn suit(&self) -> Suit {
pub const fn suit(&self) -> Suit {
let red = self.is_red();
let kiki = self.is_kiki();
match (kiki, red) {
@@ -97,15 +147,15 @@ impl Card {
}
}
/// Is the suit red.
pub fn is_red(&self) -> bool {
self.0 & 0b010000 != 0
pub const fn is_red(&self) -> bool {
self.0.get() & 0b010000 != 0
}
/// Is the suit shape spikey. (Bouba/kiki)
pub fn is_kiki(&self) -> bool {
self.0 & 0b100000 != 0
pub const fn is_kiki(&self) -> bool {
self.0.get() & 0b100000 != 0
}
pub fn deck(&self) -> u8 {
self.0 >> 6
pub const fn deck(&self) -> Deck {
Deck::new(self.0.get() >> 6).unwrap()
}
}
@@ -121,11 +171,11 @@ impl<const CAP: usize> Stack<CAP> {
}
impl Stack<52> {
/// Generate a full deck of cards with the specified deck id.
pub fn full_deck(deck: u8) -> Self {
pub fn full_deck(deck: Deck) -> Self {
let mut stack = arrayvec::ArrayVec::new();
for suit in Suit::SUITS {
for value in 1..=13 {
stack.push(Card::new(deck, suit, CardValue(value)));
for rank in Rank::RANKS {
stack.push(Card::new(deck, suit, rank));
}
}
Stack(stack)
+8 -8
View File
@@ -1,4 +1,4 @@
use card_game::{Card, CardValue, Game, Pile, Session, SessionStats, Suit};
use card_game::{Card, Game, Pile, Rank, Session, SessionStats, Suit};
use klondike::{
DstFoundation, DstTableau, Foundation, Klondike, KlondikeConfig, KlondikeInstruction,
KlondikePile, KlondikePileStack, KlondikeStats, SkipCards, Tableau, TableauStack,
@@ -9,12 +9,12 @@ struct Displayed<T>(T);
impl Display for Displayed<&Card> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0.value().get() {
1 => write!(f, "A"),
11 => write!(f, "J"),
12 => write!(f, "Q"),
13 => write!(f, "K"),
other => write!(f, "{other}"),
match self.0.rank() {
Rank::Ace => write!(f, "A"),
Rank::Jack => write!(f, "J"),
Rank::Queen => write!(f, "Q"),
Rank::King => write!(f, "K"),
other => write!(f, "{}", other as u8),
}?;
match self.0.suit() {
Suit::Spades => write!(f, ""),
@@ -236,7 +236,7 @@ fn get_good_move(state: &Klondike) -> Option<KlondikeInstruction> {
|| state
.state()
.stack_bottom_card(dst_tableau.src)
.is_some_and(|card| card.value() != CardValue::KING) =>
.is_some_and(|card| card.rank() != Rank::King) =>
{
2
}
+6 -6
View File
@@ -1,6 +1,6 @@
pub type Rng = rand::rngs::ThreadRng;
use card_game::{Card, CardValue, Game, Pile, Stack};
use card_game::{Card, Game, Pile, Rank, Stack};
#[cfg(test)]
mod test;
@@ -475,10 +475,10 @@ impl KlondikeState {
// suit matches?
src_card.suit() == dst_card.suit()
// value is +1?
&& dst_card.value().checked_add(1) == Some(src_card.value())
&& dst_card.rank().checked_add(1) == Some(src_card.rank())
}
// only ace is allowed to go onto empty foundation
None => src_card.value() == CardValue::ACE,
None => src_card.rank() == Rank::Ace,
}
} else {
false
@@ -494,10 +494,10 @@ impl KlondikeState {
// red-ness is opposite?
src_card.is_red() != dst_card.is_red()
// value is -1?
&& dst_card.value().checked_sub(1) == Some(src_card.value())
&& dst_card.rank().checked_sub(1) == Some(src_card.rank())
}
// only king is allowed to go onto empty tableau
None => src_card.value() == CardValue::KING,
None => src_card.rank() == Rank::King,
}
} else {
false
@@ -536,7 +536,7 @@ impl Klondike {
}
pub fn new(mut seed: Rng) -> Self {
// shuffle a new deck
let mut deck = Stack::full_deck(0);
let mut deck = Stack::full_deck(card_game::Deck::Deck1);
use rand::seq::SliceRandom;
deck.shuffle(&mut seed);
let mut deck = deck.into_iter();