Implemented hand and deck

This commit is contained in:
Jan-Peter von Hunnius 2025-02-13 17:31:39 +01:00
parent 77315c79b0
commit ac25a1caea
8 changed files with 451 additions and 59 deletions

View file

@ -5,3 +5,4 @@ edition = "2021"
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
rand = "0.9.0"

View file

@ -35,27 +35,35 @@ impl Card {
} }
// Constructor from rank and suit // Constructor from rank and suit
pub fn from_rank_suit(rank: u8, suit: u8) -> Self { pub fn from_rank_suit(rank: u8, suit: u8) -> Result<Self, &'static str> {
assert!(rank >= Self::TWO && rank <= Self::ACE, "Invalid rank"); if rank < Self::TWO || rank > Self::ACE {
assert!(suit < Self::NUM_SUITS, "Invalid suit"); return Err("Invalid rank");
Self((suit << 4) | (rank & 0xF)) }
if suit >= Self::NUM_SUITS {
return Err("Invalid suit");
}
Ok(Self((suit << 4) | (rank & 0xF)))
} }
// Constructor from index (0..51) // Constructor from index (0..51)
pub fn from_index(index: u8) -> Self { pub fn from_index(index: u8) -> Result<Self, &'static str> {
assert!(index < Self::NUM_CARDS, "Invalid index"); if index >= Self::NUM_CARDS {
return Err("Invalid index");
}
let rank = (index % Self::NUM_RANKS) + Self::TWO; let rank = (index % Self::NUM_RANKS) + Self::TWO;
let suit = index / Self::NUM_RANKS; let suit = index / Self::NUM_RANKS;
Self::from_rank_suit(rank, suit) Self::from_rank_suit(rank, suit)
} }
// Constructor from string (e.g., "As" for Ace of Spades) // Constructor from string (e.g., "As" for Ace of Spades)
pub fn from_string(s: &str) -> Self { pub fn from_string(s: &str) -> Result<Self, &'static str> {
assert!(s.len() == 2, "Invalid card string"); if s.len() != 2 {
return Err("Invalid card string");
}
let rank_char = s.chars().nth(0).unwrap(); let rank_char = s.chars().nth(0).unwrap();
let suit_char = s.chars().nth(1).unwrap(); let suit_char = s.chars().nth(1).unwrap();
let rank = Self::rank_from_char(rank_char); let rank = Self::rank_from_char(rank_char)?;
let suit = Self::suit_from_char(suit_char); let suit = Self::suit_from_char(suit_char)?;
Self::from_rank_suit(rank, suit) Self::from_rank_suit(rank, suit)
} }
@ -80,64 +88,64 @@ impl Card {
} }
// Convert rank to character (e.g., 14 -> 'A') // Convert rank to character (e.g., 14 -> 'A')
pub fn rank_to_char(rank: u8) -> char { pub fn rank_to_char(rank: u8) -> Result<char, &'static str> {
match rank { match rank {
Self::TWO => '2', Self::TWO => Ok('2'),
Self::THREE => '3', Self::THREE => Ok('3'),
Self::FOUR => '4', Self::FOUR => Ok('4'),
Self::FIVE => '5', Self::FIVE => Ok('5'),
Self::SIX => '6', Self::SIX => Ok('6'),
Self::SEVEN => '7', Self::SEVEN => Ok('7'),
Self::EIGHT => '8', Self::EIGHT => Ok('8'),
Self::NINE => '9', Self::NINE => Ok('9'),
Self::TEN => 'T', Self::TEN => Ok('T'),
Self::JACK => 'J', Self::JACK => Ok('J'),
Self::QUEEN => 'Q', Self::QUEEN => Ok('Q'),
Self::KING => 'K', Self::KING => Ok('K'),
Self::ACE => 'A', Self::ACE => Ok('A'),
_ => panic!("Invalid rank"), _ => Err("Invalid rank"),
} }
} }
// Convert suit to character (e.g., 0 -> 's') // Convert suit to character (e.g., 0 -> 's')
pub fn suit_to_char(suit: u8) -> char { pub fn suit_to_char(suit: u8) -> Result<char, &'static str> {
match suit { match suit {
Self::SPADES => 's', Self::SPADES => Ok('s'),
Self::HEARTS => 'h', Self::HEARTS => Ok('h'),
Self::DIAMONDS => 'd', Self::DIAMONDS => Ok('d'),
Self::CLUBS => 'c', Self::CLUBS => Ok('c'),
_ => panic!("Invalid suit"), _ => Err("Invalid suit"),
} }
} }
// Convert rank character to rank (e.g., 'A' -> 14) // Convert rank character to rank (e.g., 'A' -> 14)
pub fn rank_from_char(rank_char: char) -> u8 { pub fn rank_from_char(rank_char: char) -> Result<u8, &'static str> {
match rank_char { match rank_char {
'2' => Self::TWO, '2' => Ok(Self::TWO),
'3' => Self::THREE, '3' => Ok(Self::THREE),
'4' => Self::FOUR, '4' => Ok(Self::FOUR),
'5' => Self::FIVE, '5' => Ok(Self::FIVE),
'6' => Self::SIX, '6' => Ok(Self::SIX),
'7' => Self::SEVEN, '7' => Ok(Self::SEVEN),
'8' => Self::EIGHT, '8' => Ok(Self::EIGHT),
'9' => Self::NINE, '9' => Ok(Self::NINE),
'T' => Self::TEN, 'T' => Ok(Self::TEN),
'J' => Self::JACK, 'J' => Ok(Self::JACK),
'Q' => Self::QUEEN, 'Q' => Ok(Self::QUEEN),
'K' => Self::KING, 'K' => Ok(Self::KING),
'A' => Self::ACE, 'A' => Ok(Self::ACE),
_ => panic!("Invalid rank character"), _ => Err("Invalid rank character"),
} }
} }
// Convert suit character to suit (e.g., 's' -> 0) // Convert suit character to suit (e.g., 's' -> 0)
pub fn suit_from_char(suit_char: char) -> u8 { pub fn suit_from_char(suit_char: char) -> Result<u8, &'static str> {
match suit_char { match suit_char {
's' => Self::SPADES, 's' => Ok(Self::SPADES),
'h' => Self::HEARTS, 'h' => Ok(Self::HEARTS),
'd' => Self::DIAMONDS, 'd' => Ok(Self::DIAMONDS),
'c' => Self::CLUBS, 'c' => Ok(Self::CLUBS),
_ => panic!("Invalid suit character"), _ => Err("Invalid suit character"),
} }
} }
} }
@ -147,8 +155,8 @@ impl std::fmt::Display for Card {
if !self.is_valid() { if !self.is_valid() {
write!(f, "Invalid Card") write!(f, "Invalid Card")
} else { } else {
let rank_char = Self::rank_to_char(self.rank()); let rank_char = Self::rank_to_char(self.rank()).unwrap();
let suit_char = Self::suit_to_char(self.suit()); let suit_char = Self::suit_to_char(self.suit()).unwrap();
write!(f, "{}{}", rank_char, suit_char) write!(f, "{}{}", rank_char, suit_char)
} }
} }
@ -160,7 +168,7 @@ mod tests {
#[test] #[test]
fn test_card_creation() { fn test_card_creation() {
let card = Card::from_rank_suit(Card::ACE, Card::SPADES); let card = Card::from_rank_suit(Card::ACE, Card::SPADES).unwrap();
assert_eq!(card.rank(), Card::ACE); assert_eq!(card.rank(), Card::ACE);
assert_eq!(card.suit(), Card::SPADES); assert_eq!(card.suit(), Card::SPADES);
assert_eq!(card.index(), 12); // Ace of Spades is index 12 assert_eq!(card.index(), 12); // Ace of Spades is index 12
@ -169,7 +177,7 @@ mod tests {
#[test] #[test]
fn test_card_from_string() { fn test_card_from_string() {
let card = Card::from_string("As"); let card = Card::from_string("As").unwrap();
assert_eq!(card.rank(), Card::ACE); assert_eq!(card.rank(), Card::ACE);
assert_eq!(card.suit(), Card::SPADES); assert_eq!(card.suit(), Card::SPADES);
assert_eq!(card.to_string(), "As"); assert_eq!(card.to_string(), "As");
@ -177,7 +185,7 @@ mod tests {
#[test] #[test]
fn test_card_from_index() { fn test_card_from_index() {
let card = Card::from_index(0); let card = Card::from_index(0).unwrap();
assert_eq!(card.rank(), Card::TWO); assert_eq!(card.rank(), Card::TWO);
assert_eq!(card.suit(), Card::SPADES); assert_eq!(card.suit(), Card::SPADES);
assert_eq!(card.to_string(), "2s"); assert_eq!(card.to_string(), "2s");

175
api/src/deck.rs Normal file
View file

@ -0,0 +1,175 @@
use rand::seq::SliceRandom;
use rand::SeedableRng;
use std::fmt;
use crate::card::Card;
use crate::hand::Hand;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Deck {
cards: Vec<Card>,
dealt: Vec<Card>, // Cards that have been dealt
}
impl Deck {
pub const NUM_CARDS: usize = 52;
// Create a new, unshuffled deck
pub fn new() -> Self {
let mut cards = Vec::with_capacity(Self::NUM_CARDS);
for suit in 0..4 {
for rank in Card::TWO..=Card::ACE {
cards.push(Card::from_rank_suit(rank, suit).unwrap());
}
}
Self {
cards,
dealt: Vec::new(),
}
}
// Create a new deck with a specific shuffle seed
pub fn with_seed(seed: u64) -> Self {
let mut deck = Self::new();
deck.shuffle_with_seed(seed);
deck
}
// Shuffle the deck using a random seed
pub fn shuffle(&mut self) {
self.cards.shuffle(&mut rand::rng());
}
// Shuffle the deck using a specific seed
pub fn shuffle_with_seed(&mut self, seed: u64) {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
self.cards.shuffle(&mut rng);
}
// Deal the next card from the deck
pub fn deal(&mut self) -> Option<Card> {
if self.cards.is_empty() {
None
} else {
let card = self.cards.pop().unwrap();
self.dealt.push(card);
Some(card)
}
}
// Deal a random card from the deck
pub fn deal_random(&mut self) -> Option<Card> {
if self.cards.is_empty() {
None
} else {
let index = rand::random::<u8>() as usize % self.cards.len();
let card = self.cards.remove(index);
self.dealt.push(card);
Some(card)
}
}
// Reset the deck (put all cards back)
pub fn reset(&mut self) {
self.cards.extend(self.dealt.drain(..));
}
// Check if a card is still in the deck
pub fn in_deck(&self, card: Card) -> bool {
self.cards.contains(&card)
}
// Remove a specific card from the deck
pub fn extract_card(&mut self, card: Card) -> Option<Card> {
if let Some(index) = self.cards.iter().position(|&c| c == card) {
Some(self.cards.remove(index))
} else {
None
}
}
// Remove a specific card by index from the deck
pub fn extract_card_by_index(&mut self, index: u8) -> Option<Card> {
if let Some(pos) = self.cards.iter().position(|&c| c.index() == index) {
Some(self.cards.remove(pos))
} else {
None
}
}
// Remove all cards in a hand from the deck
pub fn extract_hand(&mut self, hand: &Hand) {
for card in hand.get_card_array() {
self.extract_card_by_index(card);
}
}
// Get the number of cards left in the deck
pub fn cards_left(&self) -> usize {
self.cards.len()
}
// Get the top card (next card to be dealt)
pub fn get_top_card(&self) -> Option<Card> {
self.cards.last().copied()
}
// Get the index of the top card
pub fn get_top_card_index(&self) -> Option<u8> {
self.get_top_card().map(|card| card.index())
}
// Get a card at a specific index in the deck
pub fn get_card(&self, index: usize) -> Option<Card> {
self.cards.get(index).copied()
}
}
impl fmt::Display for Deck {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Deck ({} cards left)", self.cards_left())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deck_creation() {
let deck = Deck::new();
assert_eq!(deck.cards_left(), Deck::NUM_CARDS);
}
#[test]
fn test_deck_shuffle() {
let mut deck = Deck::new();
deck.shuffle();
assert_eq!(deck.cards_left(), Deck::NUM_CARDS);
}
#[test]
fn test_deck_deal() {
let mut deck = Deck::new();
let card = deck.deal().unwrap();
assert_eq!(deck.cards_left(), Deck::NUM_CARDS - 1);
assert!(!deck.in_deck(card));
}
#[test]
fn test_deck_reset() {
let mut deck = Deck::new();
let card = deck.deal().unwrap();
deck.reset();
assert_eq!(deck.cards_left(), Deck::NUM_CARDS);
assert!(deck.in_deck(card));
}
#[test]
fn test_deck_extract_card() {
let mut deck = Deck::new();
let card = Card::from_string("As").unwrap();
deck.extract_card(card).unwrap();
assert_eq!(deck.cards_left(), Deck::NUM_CARDS - 1);
assert!(!deck.in_deck(card));
}
}

180
api/src/hand.rs Normal file
View file

@ -0,0 +1,180 @@
use crate::card::Card;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Hand {
cards: [Option<Card>; 7], // Up to 7 cards
size: usize, // Number of cards in the hand
}
impl Hand {
pub const MAX_CARDS: usize = 7;
// Create a new empty hand
pub fn new() -> Self {
Self {
cards: [None; Self::MAX_CARDS],
size: 0,
}
}
// Create a hand from a string representation (e.g., "As Ks Qs")
pub fn from_string(s: &str) -> Result<Self, &'static str> {
let mut hand = Self::new();
for card_str in s.split_whitespace() {
let card = Card::from_string(card_str)?;
hand.add_card(card)?;
}
Ok(hand)
}
// Add a card to the hand
pub fn add_card(&mut self, card: Card) -> Result<(), &'static str> {
if self.size >= Self::MAX_CARDS {
return Err("Hand is full");
}
self.cards[self.size] = Some(card);
self.size += 1;
Ok(())
}
// Remove the last card in the hand
pub fn remove_card(&mut self) {
if self.size > 0 {
self.size -= 1;
self.cards[self.size] = None;
}
}
// Get the card at a specific position (1-based indexing)
pub fn get_card(&self, pos: usize) -> Option<Card> {
if pos == 0 || pos > self.size {
return None;
}
self.cards[pos - 1]
}
// Get the card index at a specific position (1-based indexing)
pub fn get_card_index(&self, pos: usize) -> Option<u8> {
self.get_card(pos).map(|card| card.index())
}
// Get the last card in the hand
pub fn get_last_card(&self) -> Option<Card> {
if self.size == 0 {
None
} else {
self.cards[self.size - 1]
}
}
// Get the last card index in the hand
pub fn get_last_card_index(&self) -> Option<u8> {
self.get_last_card().map(|card| card.index())
}
// Clear the hand (remove all cards)
pub fn clear(&mut self) {
self.cards = [None; Self::MAX_CARDS];
self.size = 0;
}
// Sort the hand in descending order by card index
pub fn sort(&mut self) {
self.cards[..self.size].sort_by(|a, b| {
let a_index = a.map(|card| card.index()).unwrap_or(0);
let b_index = b.map(|card| card.index()).unwrap_or(0);
b_index.cmp(&a_index) // Sort in descending order
}); }
// Check if the hand contains a specific card
pub fn contains(&self, card: Card) -> bool {
self.cards[..self.size].contains(&Some(card))
}
// Get a string representation of the hand (e.g., "As Ks Qs")
pub fn to_string(&self) -> String {
self.cards[..self.size]
.iter()
.filter_map(|&card| card.map(|c| c.to_string()))
.collect::<Vec<String>>()
.join(" ")
}
// Get a string representation for flashing purposes
pub fn flashing_string(&self) -> String {
self.to_string()
}
// Get the size of the hand
pub fn size(&self) -> usize {
self.size
}
// Get the array of card indexes (for LUT-based evaluators)
pub fn get_card_array(&self) -> Vec<u8> {
self.cards[..self.size]
.iter()
.filter_map(|&card| card.map(|c| c.index()))
.collect()
}
}
#[cfg(test)]
mod tests {
use crate::card::Card;
use super::*;
#[test]
fn test_hand_creation() {
let hand = Hand::new();
assert_eq!(hand.size(), 0);
}
#[test]
fn test_add_card() {
let mut hand = Hand::new();
let card = Card::from_string("As").unwrap();
hand.add_card(card).unwrap();
assert_eq!(hand.size(), 1);
assert_eq!(hand.get_card(1), Some(card));
}
#[test]
fn test_remove_card() {
let mut hand = Hand::new();
let card = Card::from_string("As").unwrap();
hand.add_card(card).unwrap();
hand.remove_card();
assert_eq!(hand.size(), 0);
}
#[test]
fn test_hand_from_string() {
let hand = Hand::from_string("As Ks Qs").unwrap();
assert_eq!(hand.size(), 3);
assert_eq!(hand.get_card(1), Some(Card::from_string("As").unwrap()));
assert_eq!(hand.get_card(2), Some(Card::from_string("Ks").unwrap()));
assert_eq!(hand.get_card(3), Some(Card::from_string("Qs").unwrap()));
}
#[test]
fn test_hand_sort() {
let mut hand = Hand::from_string("2s As Ks").unwrap();
hand.sort();
assert_eq!(hand.get_card(1), Some(Card::from_string("As").unwrap()));
assert_eq!(hand.get_card(2), Some(Card::from_string("Ks").unwrap()));
assert_eq!(hand.get_card(3), Some(Card::from_string("2s").unwrap()));
}
#[test]
fn test_hand_sort_with_empty_slots() {
let mut hand = Hand::new();
hand.add_card(Card::from_string("2s").unwrap()).unwrap();
hand.add_card(Card::from_string("As").unwrap()).unwrap();
hand.add_card(Card::from_string("Ks").unwrap()).unwrap();
hand.sort();
assert_eq!(hand.get_card(1), Some(Card::from_string("As").unwrap()));
assert_eq!(hand.get_card(2), Some(Card::from_string("Ks").unwrap()));
assert_eq!(hand.get_card(3), Some(Card::from_string("2s").unwrap()));
}
}

View file

@ -1 +1,8 @@
pub mod card; pub mod card;
pub mod hand;
pub mod deck;
#[cfg(test)]
mod tests {
}

View file

@ -2,6 +2,7 @@ use poker_api::card::Card;
#[test] #[test]
fn test_card_integration() { fn test_card_integration() {
let card = Card::from_string("As"); let card = Card::from_string("As").unwrap();
assert_eq!(card.to_string(), "As"); assert_eq!(card.to_string(), "As");
} }

9
api/tests/deck_tests.rs Normal file
View file

@ -0,0 +1,9 @@
use poker_api::deck::Deck;
#[test]
fn test_deck_integration() {
let mut deck = Deck::new();
let card = deck.deal().unwrap();
assert_eq!(deck.cards_left(), 51);
assert!(!deck.in_deck(card));
}

11
api/tests/hand_test.rs Normal file
View file

@ -0,0 +1,11 @@
use poker_api::card::Card;
use poker_api::hand::Hand;
#[test]
fn test_hand_integration() {
let mut hand = Hand::new();
hand.add_card(Card::from_string("As").unwrap()).unwrap();
hand.add_card(Card::from_string("Ks").unwrap()).unwrap();
assert_eq!(hand.size(), 2);
assert_eq!(hand.to_string(), "As Ks");
}