diff --git a/api/Cargo.toml b/api/Cargo.toml index a50895c..c98a617 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -5,3 +5,4 @@ edition = "2021" [dependencies] serde = { version = "1.0", features = ["derive"] } +rand = "0.9.0" diff --git a/api/src/card.rs b/api/src/card.rs index 547dbab..a51f813 100644 --- a/api/src/card.rs +++ b/api/src/card.rs @@ -35,27 +35,35 @@ impl Card { } // Constructor from rank and suit - pub fn from_rank_suit(rank: u8, suit: u8) -> Self { - assert!(rank >= Self::TWO && rank <= Self::ACE, "Invalid rank"); - assert!(suit < Self::NUM_SUITS, "Invalid suit"); - Self((suit << 4) | (rank & 0xF)) + pub fn from_rank_suit(rank: u8, suit: u8) -> Result { + if rank < Self::TWO || rank > Self::ACE { + return Err("Invalid rank"); + } + if suit >= Self::NUM_SUITS { + return Err("Invalid suit"); + } + Ok(Self((suit << 4) | (rank & 0xF))) } // Constructor from index (0..51) - pub fn from_index(index: u8) -> Self { - assert!(index < Self::NUM_CARDS, "Invalid index"); + pub fn from_index(index: u8) -> Result { + if index >= Self::NUM_CARDS { + return Err("Invalid index"); + } let rank = (index % Self::NUM_RANKS) + Self::TWO; let suit = index / Self::NUM_RANKS; Self::from_rank_suit(rank, suit) } // Constructor from string (e.g., "As" for Ace of Spades) - pub fn from_string(s: &str) -> Self { - assert!(s.len() == 2, "Invalid card string"); + pub fn from_string(s: &str) -> Result { + if s.len() != 2 { + return Err("Invalid card string"); + } let rank_char = s.chars().nth(0).unwrap(); let suit_char = s.chars().nth(1).unwrap(); - let rank = Self::rank_from_char(rank_char); - let suit = Self::suit_from_char(suit_char); + let rank = Self::rank_from_char(rank_char)?; + let suit = Self::suit_from_char(suit_char)?; Self::from_rank_suit(rank, suit) } @@ -80,64 +88,64 @@ impl Card { } // Convert rank to character (e.g., 14 -> 'A') - pub fn rank_to_char(rank: u8) -> char { + pub fn rank_to_char(rank: u8) -> Result { match rank { - Self::TWO => '2', - Self::THREE => '3', - Self::FOUR => '4', - Self::FIVE => '5', - Self::SIX => '6', - Self::SEVEN => '7', - Self::EIGHT => '8', - Self::NINE => '9', - Self::TEN => 'T', - Self::JACK => 'J', - Self::QUEEN => 'Q', - Self::KING => 'K', - Self::ACE => 'A', - _ => panic!("Invalid rank"), + Self::TWO => Ok('2'), + Self::THREE => Ok('3'), + Self::FOUR => Ok('4'), + Self::FIVE => Ok('5'), + Self::SIX => Ok('6'), + Self::SEVEN => Ok('7'), + Self::EIGHT => Ok('8'), + Self::NINE => Ok('9'), + Self::TEN => Ok('T'), + Self::JACK => Ok('J'), + Self::QUEEN => Ok('Q'), + Self::KING => Ok('K'), + Self::ACE => Ok('A'), + _ => Err("Invalid rank"), } } // Convert suit to character (e.g., 0 -> 's') - pub fn suit_to_char(suit: u8) -> char { + pub fn suit_to_char(suit: u8) -> Result { match suit { - Self::SPADES => 's', - Self::HEARTS => 'h', - Self::DIAMONDS => 'd', - Self::CLUBS => 'c', - _ => panic!("Invalid suit"), + Self::SPADES => Ok('s'), + Self::HEARTS => Ok('h'), + Self::DIAMONDS => Ok('d'), + Self::CLUBS => Ok('c'), + _ => Err("Invalid suit"), } } // 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 { match rank_char { - '2' => Self::TWO, - '3' => Self::THREE, - '4' => Self::FOUR, - '5' => Self::FIVE, - '6' => Self::SIX, - '7' => Self::SEVEN, - '8' => Self::EIGHT, - '9' => Self::NINE, - 'T' => Self::TEN, - 'J' => Self::JACK, - 'Q' => Self::QUEEN, - 'K' => Self::KING, - 'A' => Self::ACE, - _ => panic!("Invalid rank character"), + '2' => Ok(Self::TWO), + '3' => Ok(Self::THREE), + '4' => Ok(Self::FOUR), + '5' => Ok(Self::FIVE), + '6' => Ok(Self::SIX), + '7' => Ok(Self::SEVEN), + '8' => Ok(Self::EIGHT), + '9' => Ok(Self::NINE), + 'T' => Ok(Self::TEN), + 'J' => Ok(Self::JACK), + 'Q' => Ok(Self::QUEEN), + 'K' => Ok(Self::KING), + 'A' => Ok(Self::ACE), + _ => Err("Invalid rank character"), } } // 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 { match suit_char { - 's' => Self::SPADES, - 'h' => Self::HEARTS, - 'd' => Self::DIAMONDS, - 'c' => Self::CLUBS, - _ => panic!("Invalid suit character"), + 's' => Ok(Self::SPADES), + 'h' => Ok(Self::HEARTS), + 'd' => Ok(Self::DIAMONDS), + 'c' => Ok(Self::CLUBS), + _ => Err("Invalid suit character"), } } } @@ -147,8 +155,8 @@ impl std::fmt::Display for Card { if !self.is_valid() { write!(f, "Invalid Card") } else { - let rank_char = Self::rank_to_char(self.rank()); - let suit_char = Self::suit_to_char(self.suit()); + let rank_char = Self::rank_to_char(self.rank()).unwrap(); + let suit_char = Self::suit_to_char(self.suit()).unwrap(); write!(f, "{}{}", rank_char, suit_char) } } @@ -160,7 +168,7 @@ mod tests { #[test] 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.suit(), Card::SPADES); assert_eq!(card.index(), 12); // Ace of Spades is index 12 @@ -169,7 +177,7 @@ mod tests { #[test] 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.suit(), Card::SPADES); assert_eq!(card.to_string(), "As"); @@ -177,7 +185,7 @@ mod tests { #[test] 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.suit(), Card::SPADES); assert_eq!(card.to_string(), "2s"); diff --git a/api/src/deck.rs b/api/src/deck.rs new file mode 100644 index 0000000..47d1c02 --- /dev/null +++ b/api/src/deck.rs @@ -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, + dealt: Vec, // 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 { + 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 { + if self.cards.is_empty() { + None + } else { + let index = rand::random::() 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 { + 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 { + 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 { + self.cards.last().copied() + } + + // Get the index of the top card + pub fn get_top_card_index(&self) -> Option { + 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 { + 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)); + } +} diff --git a/api/src/hand.rs b/api/src/hand.rs new file mode 100644 index 0000000..85f5207 --- /dev/null +++ b/api/src/hand.rs @@ -0,0 +1,180 @@ +use crate::card::Card; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Hand { + cards: [Option; 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 { + 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 { + 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 { + self.get_card(pos).map(|card| card.index()) + } + + // Get the last card in the hand + pub fn get_last_card(&self) -> Option { + 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 { + 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::>() + .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 { + 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())); + } +} diff --git a/api/src/lib.rs b/api/src/lib.rs index 2e8b5ec..1d244ee 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -1 +1,8 @@ -pub mod card; \ No newline at end of file +pub mod card; +pub mod hand; +pub mod deck; + +#[cfg(test)] +mod tests { + +} \ No newline at end of file diff --git a/api/tests/card_test.rs b/api/tests/card_test.rs index b09311a..14e9382 100644 --- a/api/tests/card_test.rs +++ b/api/tests/card_test.rs @@ -2,6 +2,7 @@ use poker_api::card::Card; #[test] fn test_card_integration() { - let card = Card::from_string("As"); + let card = Card::from_string("As").unwrap(); assert_eq!(card.to_string(), "As"); } + diff --git a/api/tests/deck_tests.rs b/api/tests/deck_tests.rs new file mode 100644 index 0000000..b8cd0c0 --- /dev/null +++ b/api/tests/deck_tests.rs @@ -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)); +} diff --git a/api/tests/hand_test.rs b/api/tests/hand_test.rs new file mode 100644 index 0000000..ec86141 --- /dev/null +++ b/api/tests/hand_test.rs @@ -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"); +}