Holdem basic core structures from scratch.

This commit is contained in:
Jan-Peter von Hunnius 2025-10-10 19:48:24 +02:00
parent f02c35bef3
commit e94bf42dce
53 changed files with 6316 additions and 11906 deletions

1383
holdem_core/src/board.rs Normal file

File diff suppressed because it is too large Load diff

1172
holdem_core/src/card.rs Normal file

File diff suppressed because it is too large Load diff

495
holdem_core/src/deck.rs Normal file
View file

@ -0,0 +1,495 @@
//! # Deck Module
//!
//! This module provides the `Deck` struct for representing a deck of playing cards
//! in the poker AI system. Supports shuffling, dealing, and serialization.
//!
//! ## Deck Representation
//!
//! A Deck contains a vector of cards not yet dealt, starting with all 52 cards.
//!
//! ## Examples
//!
//! ### Creating and Using a Deck
//!
//! ```rust
//! use holdem_core::{Deck, Card};
//!
//! let mut deck = Deck::new();
//! assert_eq!(deck.remaining(), 52);
//!
//! // Shuffle the deck
//! use rand::SeedableRng;
//! let mut rng = rand::rngs::StdRng::from_seed([1; 32]);
//! deck.shuffle(&mut rng);
//!
//! // Deal some cards
//! let hole_cards = deck.deal(2);
//! let flop = deck.deal(3);
//! let turn = deck.deal(1);
//! let river = deck.deal(1);
//!
//! assert_eq!(hole_cards.len(), 2);
//! assert_eq!(flop.len(), 3);
//! assert_eq!(turn.len(), 1);
//! assert_eq!(river.len(), 1);
//! assert_eq!(deck.remaining(), 52 - 7);
//! ```
//!
//! ## Design Decisions
//!
//! - **Full Deck**: Always starts with 52 cards in standard poker order
//! - **Efficient Dealing**: Pop from end for O(1) deal operations
//! - **Serialization Ready**: Full serde support for persistence
//! - **Flexible Shuffling**: Uses rand crate for high-quality randomization
use crate::card::Card;
use serde::{Deserialize, Serialize};
/// Represents a deck of cards not yet dealt
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Deck {
cards: Vec<Card>,
}
impl Deck {
/// Creates a new full deck of 52 cards
///
/// # Examples
///
/// ```rust
/// use holdem_core::Deck;
///
/// let deck = Deck::new();
/// assert_eq!(deck.remaining(), 52);
/// ```
///
/// # Panics
///
/// This method does not panic.
pub fn new() -> Self {
let mut cards = Vec::with_capacity(52);
for suit in 0..4 {
for rank in 0..13 {
cards.push(Card::new(rank, suit).unwrap());
}
}
Self { cards }
}
/// Shuffles the deck using the provided random number generator
///
/// # Examples
///
/// ```rust
/// use holdem_core::Deck;
/// use rand::SeedableRng;
///
/// let mut deck = Deck::new();
/// let mut rng = rand::rngs::StdRng::from_seed([1; 32]);
/// deck.shuffle(&mut rng);
/// ```
///
/// # Panics
///
/// This method does not panic.
pub fn shuffle<R: rand::Rng>(&mut self, rng: &mut R) {
use rand::seq::SliceRandom;
self.cards.shuffle(rng);
}
/// Deals a single card from the top of the deck
///
/// Returns `None` if the deck is empty.
///
/// # Examples
///
/// ```rust
/// use holdem_core::Deck;
///
/// let mut deck = Deck::new();
/// let card = deck.deal_one().unwrap();
/// assert_eq!(deck.remaining(), 51);
/// ```
///
/// # Panics
///
/// This method does not panic, but returns `None` if the deck is empty.
pub fn deal_one(&mut self) -> Option<Card> {
self.cards.pop()
}
/// Deals multiple cards from the top of the deck
///
/// Returns a vector containing up to `count` cards. If fewer than `count` cards
/// remain in the deck, returns all remaining cards.
///
/// # Examples
///
/// ```rust
/// use holdem_core::Deck;
///
/// let mut deck = Deck::new();
/// let cards = deck.deal(5);
/// assert_eq!(cards.len(), 5);
/// assert_eq!(deck.remaining(), 47);
/// ```
///
/// # Panics
///
/// This method does not panic.
pub fn deal(&mut self, count: usize) -> Vec<Card> {
let mut dealt = Vec::new();
for _ in 0..count {
if let Some(card) = self.deal_one() {
dealt.push(card);
} else {
break;
}
}
dealt
}
/// Returns the number of cards remaining in the deck
///
/// # Examples
///
/// ```rust
/// use holdem_core::Deck;
///
/// let mut deck = Deck::new();
/// assert_eq!(deck.remaining(), 52);
///
/// deck.deal(5);
/// assert_eq!(deck.remaining(), 47);
/// ```
///
/// # Panics
///
/// This method does not panic.
pub fn remaining(&self) -> usize {
self.cards.len()
}
/// Returns true if the deck is empty
///
/// # Examples
///
/// ```rust
/// use holdem_core::Deck;
///
/// let mut deck = Deck::new();
/// assert!(!deck.is_empty());
///
/// // Deal all cards
/// deck.deal(52);
/// assert!(deck.is_empty());
/// ```
///
/// # Panics
///
/// This method does not panic.
pub fn is_empty(&self) -> bool {
self.cards.is_empty()
}
/// Returns a reference to the remaining cards
///
/// # Examples
///
/// ```rust
/// use holdem_core::Deck;
///
/// let deck = Deck::new();
/// let cards = deck.cards();
/// assert_eq!(cards.len(), 52);
/// ```
///
/// # Panics
///
/// This method does not panic.
pub fn cards(&self) -> &[Card] {
&self.cards
}
}
impl Default for Deck {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn test_deck_new() {
let deck = Deck::new();
assert_eq!(deck.remaining(), 52);
assert!(!deck.is_empty());
// Check all cards are present
let cards = deck.cards();
let mut card_set = HashSet::new();
for &card in cards {
assert!(card_set.insert(card), "Duplicate card in deck: {}", card);
}
assert_eq!(card_set.len(), 52);
}
#[test]
fn test_deck_shuffle() {
let mut deck1 = Deck::new();
let deck2 = Deck::new();
// Decks should be identical initially
assert_eq!(deck1.cards(), deck2.cards());
// Shuffle one deck
use rand::SeedableRng;
let mut rng = rand::rngs::StdRng::from_seed([1; 32]);
deck1.shuffle(&mut rng);
// After shuffle, order should be different (with very high probability)
assert_ne!(deck1.cards(), deck2.cards());
assert_eq!(deck1.remaining(), 52);
assert_eq!(deck2.remaining(), 52);
}
#[test]
fn test_deck_deal_one() {
let mut deck = Deck::new();
// Deal one card
let card = deck.deal_one().unwrap();
assert_eq!(deck.remaining(), 51);
// Deal another
let card2 = deck.deal_one().unwrap();
assert_eq!(deck.remaining(), 50);
// Cards should be different
assert_ne!(card, card2);
}
#[test]
fn test_deck_deal_multiple() {
let mut deck = Deck::new();
// Deal 5 cards
let cards = deck.deal(5);
assert_eq!(cards.len(), 5);
assert_eq!(deck.remaining(), 47);
// All cards should be unique
let mut card_set = HashSet::new();
for &card in &cards {
assert!(card_set.insert(card), "Duplicate card dealt: {}", card);
}
}
#[test]
fn test_deck_deal_empty() {
let mut deck = Deck::new();
// Deal all cards
let all_cards = deck.deal(52);
assert_eq!(all_cards.len(), 52);
assert_eq!(deck.remaining(), 0);
assert!(deck.is_empty());
// Try to deal one more
assert!(deck.deal_one().is_none());
// Try to deal multiple more
let empty_deal = deck.deal(5);
assert!(empty_deal.is_empty());
}
#[test]
fn test_deck_serialization() {
let deck = Deck::new();
// Test JSON serialization
let json = serde_json::to_string(&deck).unwrap();
let deserialized: Deck = serde_json::from_str(&json).unwrap();
assert_eq!(deck.cards(), deserialized.cards());
// Test TOML serialization
let toml = toml::to_string(&deck).unwrap();
let deserialized: Deck = toml::from_str(&toml).unwrap();
assert_eq!(deck.cards(), deserialized.cards());
}
#[test]
fn test_deck_shuffle_randomness() {
use rand::SeedableRng;
let mut deck = Deck::new();
let original_order: Vec<Card> = deck.cards().to_vec();
// Shuffle with different seeds and verify randomness
let mut rng1 = rand::rngs::StdRng::from_seed([1; 32]);
deck.shuffle(&mut rng1);
let shuffled1 = deck.cards().to_vec();
// Reset deck
deck = Deck::new();
let mut rng2 = rand::rngs::StdRng::from_seed([2; 32]);
deck.shuffle(&mut rng2);
let shuffled2 = deck.cards().to_vec();
// Different seeds should produce different shuffles
assert_ne!(shuffled1, shuffled2);
assert_ne!(shuffled1, original_order);
assert_ne!(shuffled2, original_order);
// All cards should still be present
let mut set1 = HashSet::new();
let mut set2 = HashSet::new();
for &card in &shuffled1 {
set1.insert(card);
}
for &card in &shuffled2 {
set2.insert(card);
}
assert_eq!(set1.len(), 52);
assert_eq!(set2.len(), 52);
}
#[test]
fn test_deck_deal_edge_cases() {
let mut deck = Deck::new();
// Deal more cards than available
let cards = deck.deal(60);
assert_eq!(cards.len(), 52);
assert_eq!(deck.remaining(), 0);
assert!(deck.is_empty());
// Try dealing from empty deck
let empty_deal = deck.deal(5);
assert!(empty_deal.is_empty());
// Deal one from empty deck
assert!(deck.deal_one().is_none());
}
#[test]
fn test_deck_remaining_cards() {
let mut deck = Deck::new();
assert_eq!(deck.remaining(), 52);
// Deal various amounts and check remaining
deck.deal(5);
assert_eq!(deck.remaining(), 47);
deck.deal_one();
assert_eq!(deck.remaining(), 46);
deck.deal(10);
assert_eq!(deck.remaining(), 36);
// Deal all remaining
deck.deal(36);
assert_eq!(deck.remaining(), 0);
assert!(deck.is_empty());
}
#[test]
fn test_deck_deal_zero() {
let mut deck = Deck::new();
// Deal zero cards
let empty_deal = deck.deal(0);
assert!(empty_deal.is_empty());
assert_eq!(deck.remaining(), 52);
}
#[test]
fn test_deck_default() {
let deck = Deck::default();
assert_eq!(deck.remaining(), 52);
assert!(!deck.is_empty());
}
#[test]
fn test_deck_clone() {
let mut deck1 = Deck::new();
deck1.deal(5);
let deck2 = deck1.clone();
assert_eq!(deck1.remaining(), deck2.remaining());
assert_eq!(deck1.cards(), deck2.cards());
}
#[test]
fn test_deck_serialization_partial() {
let mut deck = Deck::new();
deck.deal(10); // Remove 10 cards
// Serialize partial deck
let json = serde_json::to_string(&deck).unwrap();
let deserialized: Deck = serde_json::from_str(&json).unwrap();
assert_eq!(deck.remaining(), deserialized.remaining());
assert_eq!(deck.cards(), deserialized.cards());
// Should be able to continue dealing from deserialized deck
let mut deserialized = deserialized;
let more_cards = deserialized.deal(5);
assert_eq!(more_cards.len(), 5);
}
#[test]
fn test_deck_deal_uniqueness() {
let mut deck = Deck::new();
// Deal all cards and ensure uniqueness
let mut all_cards = Vec::new();
while let Some(card) = deck.deal_one() {
all_cards.push(card);
}
assert_eq!(all_cards.len(), 52);
let mut card_set = HashSet::new();
for card in all_cards {
assert!(card_set.insert(card), "Duplicate card found: {}", card);
}
assert_eq!(card_set.len(), 52);
}
#[test]
fn test_deck_performance() {
use std::time::Instant;
// Test creation performance
let start = Instant::now();
for _ in 0..1000 {
let _deck = Deck::new();
}
let creation_time = start.elapsed();
// Test dealing performance
let mut deck = Deck::new();
let start = Instant::now();
for _ in 0..52 {
deck.deal_one();
}
let dealing_time = start.elapsed();
// Should be very fast
assert!(
creation_time.as_millis() < 100,
"Deck creation too slow: {:?}",
creation_time
);
assert!(
dealing_time.as_millis() < 10,
"Dealing too slow: {:?}",
dealing_time
);
}
}

202
holdem_core/src/errors.rs Normal file
View file

@ -0,0 +1,202 @@
//! # Poker Error Types
//!
//! This module defines comprehensive error types for the poker library,
//! replacing string-based errors with structured, type-safe error handling.
use crate::card::Card;
use std::fmt;
/// Comprehensive error type for all poker-related operations
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PokerError {
/// Invalid card rank (must be 0-12)
InvalidCardRank { rank: u8 },
/// Invalid card suit (must be 0-3)
InvalidCardSuit { suit: u8 },
/// Invalid card string format
InvalidCardString { string: String },
/// Invalid rank character in card string
InvalidRankCharacter { character: char },
/// Invalid suit character in card string
InvalidSuitCharacter { character: char },
/// Duplicate card found
DuplicateCard(Card),
/// Invalid hand size (must be 0-7 cards)
InvalidHandSize { size: usize },
/// Invalid hole cards notation length
InvalidHoleCardsNotationLength { length: usize },
/// Invalid suitedness indicator (must be 's' or 'o')
InvalidSuitednessIndicator { indicator: char },
/// Pairs cannot have suitedness indicators
PairsCannotHaveSuitedness,
/// Non-pair hole cards must specify suitedness
NonPairMustSpecifySuitedness,
/// Invalid street transition in board dealing
InvalidStreetTransition,
/// Flop must consist of exactly 3 cards
FlopMustBeThreeCards { actual: usize },
/// Must have exactly 3 cards before dealing turn
MustHaveThreeCardsForTurn { actual: usize },
/// Must have exactly 4 cards before dealing river
MustHaveFourCardsForRiver { actual: usize },
/// Cannot deal from current street
CannotDealFromStreet { current_street: String },
/// Combined hole cards and board exceed 7 cards
CombinedCardsExceedLimit { total: usize },
/// Duplicate cards in new deal
DuplicateCardsInDeal,
/// New card duplicates existing board card
DuplicateWithExistingBoardCard(Card),
}
impl fmt::Display for PokerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PokerError::InvalidCardRank { rank } => {
write!(f, "Invalid card rank: {}. Rank must be 0-12", rank)
}
PokerError::InvalidCardSuit { suit } => {
write!(f, "Invalid card suit: {}. Suit must be 0-3", suit)
}
PokerError::InvalidCardString { string } => {
write!(
f,
"Invalid card string: '{}'. Must be exactly 2 characters (rank + suit)",
string
)
}
PokerError::InvalidRankCharacter { character } => {
write!(
f,
"Invalid rank character: '{}'. Must be 2-9, T, J, Q, K, or A",
character
)
}
PokerError::InvalidSuitCharacter { character } => {
write!(
f,
"Invalid suit character: '{}'. Must be h, d, c, or s",
character
)
}
PokerError::DuplicateCard(card) => {
write!(f, "Duplicate card found: {}", card)
}
PokerError::InvalidHandSize { size } => {
write!(
f,
"Invalid hand size: {}. Hand must contain 0-7 cards",
size
)
}
PokerError::InvalidHoleCardsNotationLength { length } => {
write!(
f,
"Invalid hole cards notation length: {}. Must be 2 or 3 characters",
length
)
}
PokerError::InvalidSuitednessIndicator { indicator } => {
write!(
f,
"Invalid suitedness indicator: '{}'. Must be 's' or 'o'",
indicator
)
}
PokerError::PairsCannotHaveSuitedness => {
write!(f, "Pairs cannot have suitedness indicators")
}
PokerError::NonPairMustSpecifySuitedness => {
write!(
f,
"Non-pair hole cards must specify suitedness with 's' or 'o'"
)
}
PokerError::InvalidStreetTransition => {
write!(f, "Invalid street transition")
}
PokerError::FlopMustBeThreeCards { actual } => {
write!(f, "Flop must consist of exactly 3 cards, got {}", actual)
}
PokerError::MustHaveThreeCardsForTurn { actual } => {
write!(
f,
"Must have exactly 3 cards before dealing turn, got {}",
actual
)
}
PokerError::MustHaveFourCardsForRiver { actual } => {
write!(
f,
"Must have exactly 4 cards before dealing river, got {}",
actual
)
}
PokerError::CannotDealFromStreet { current_street } => {
write!(f, "Cannot deal from street: {}", current_street)
}
PokerError::CombinedCardsExceedLimit { total } => {
write!(
f,
"Combined hole cards and board must result in at most 7 cards, got {}",
total
)
}
PokerError::DuplicateCardsInDeal => {
write!(f, "Duplicate cards in new deal")
}
PokerError::DuplicateWithExistingBoardCard(card) => {
write!(f, "New card duplicates existing board card: {}", card)
}
}
}
}
impl std::error::Error for PokerError {}
/// Convert PokerError to String for backward compatibility
impl From<PokerError> for String {
fn from(error: PokerError) -> String {
error.to_string()
}
}
/// Convert &str to PokerError for backward compatibility
impl From<&str> for PokerError {
fn from(s: &str) -> PokerError {
PokerError::InvalidCardString {
string: s.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::card::Card;
#[test]
fn test_error_display() {
let error = PokerError::InvalidCardRank { rank: 13 };
assert_eq!(
error.to_string(),
"Invalid card rank: 13. Rank must be 0-12"
);
let error = PokerError::InvalidCardSuit { suit: 4 };
assert_eq!(error.to_string(), "Invalid card suit: 4. Suit must be 0-3");
let error = PokerError::DuplicateCard(Card::new(12, 3).unwrap());
assert!(error.to_string().contains("Duplicate card found"));
}
#[test]
fn test_error_conversion() {
let error = PokerError::InvalidCardRank { rank: 13 };
let string: String = error.into();
assert!(string.contains("Invalid card rank"));
let error: PokerError = "test string".into();
assert!(matches!(error, PokerError::InvalidCardString { .. }));
}
}

1130
holdem_core/src/hand.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,746 @@
//! # Hole Cards Module
//!
//! This module provides the `HoleCards` struct for representing a player's two private cards
//! in poker. Hole cards are distinct from complete hands and provide specialized methods
//! for analyzing preflop hand strength, suitedness, connectivity, and standard notation.
//!
//! ## Hole Cards Structure
//!
//! A `HoleCards` contains:
//! - An array of exactly two `Card` instances
//! - Methods for analyzing card relationships and properties
//! - Standard poker notation generation
//! - Efficient comparison and hashing
//!
//! ## Key Features
//!
//! - **Validation**: Ensures no duplicate cards during construction
//! - **Properties**: Pair detection, suitedness, connectivity analysis
//! - **Notation**: Standard poker abbreviations ("AKs", "QQ", "T9o")
//! - **Serialization**: Full serde support for persistence
//! - **Performance**: Optimized comparison and hashing for lookups
//! - **Access**: Methods for retrieving individual cards
//!
//! ## Examples
//!
//! ### Creating and Analyzing Hole Cards
//!
//! ```rust
//! use holdem_core::hole_cards::HoleCards;
//! use holdem_core::card::Card;
//!
//! // Create hole cards from strings
//! let hole_cards = HoleCards::from_notation("AKs").unwrap();
//!
//! // Check properties
//! assert!(hole_cards.is_suited());
//! assert!(!hole_cards.is_pair());
//! assert_eq!(hole_cards.connectivity(), 0); // No gap between A and K
//!
//! // Get standard notation
//! assert_eq!(hole_cards.notation(), "AKs");
//! ```
//!
//! ### Working with Individual Cards
//!
//! ```rust
//! use holdem_core::hole_cards::HoleCards;
//!
//! let hole_cards = HoleCards::from_notation("QQ").unwrap();
//!
//! // Access individual cards
//! let card1 = hole_cards.first_card();
//! let card2 = hole_cards.second_card();
//!
//! // Both cards are Queens
//! assert_eq!(card1.rank(), card2.rank());
//! assert!(hole_cards.is_pair());
//! ```
//!
//! ## Design Decisions
//!
//! - **Fixed Size**: Always exactly two cards for hole cards
//! - **Validation**: Constructor validates no duplicates
//! - **Ordering**: Cards stored in rank-descending order for consistency
//! - **Notation**: Follows standard poker conventions (high-low, s/o for suited/offsuit)
//! - **Performance**: Implements Hash and efficient comparison
//! - **Safety**: Comprehensive error handling and validation
use crate::card::Card;
use crate::errors::PokerError;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt;
/// Represents a player's two private hole cards in poker
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct HoleCards {
/// The two cards, stored in rank-descending order
pub cards: [Card; 2],
}
impl HoleCards {
/// Create new hole cards from two individual cards
///
/// # Errors
/// Returns an error if the cards are identical (same rank and suit)
///
/// # Examples
/// ```
/// use holdem_core::hole_cards::HoleCards;
/// use holdem_core::card::Card;
/// use std::str::FromStr;
///
/// let card1 = Card::from_str("As").unwrap();
/// let card2 = Card::from_str("Ks").unwrap();
/// let hole_cards = HoleCards::new(card1, card2).unwrap();
/// ```
///
/// # Panics
///
/// This method does not panic, but returns an error if cards are identical.
pub fn new(card1: Card, card2: Card) -> Result<Self, PokerError> {
if card1 == card2 {
return Err(PokerError::DuplicateCard(card1));
}
// Store cards in rank-descending order
let (high, low) = if card1.rank() >= card2.rank() {
(card1, card2)
} else {
(card2, card1)
};
Ok(Self { cards: [high, low] })
}
/// Create hole cards from standard poker notation
///
/// # Supported Formats
/// - "AKs" - suited Ace-King
/// - "QQ" - pocket Queens
/// - "T9o" - offsuit Ten-Nine
///
/// # Errors
/// Returns an error for invalid notation or duplicate ranks with same suit
///
/// # Examples
/// ```
/// use holdem_core::hole_cards::HoleCards;
///
/// let pocket_aces = HoleCards::from_notation("AA").unwrap();
/// let pocket_kings = HoleCards::from_notation("KK").unwrap();
/// let offsuit_jack_ten = HoleCards::from_notation("JTo").unwrap();
/// ```
///
/// # Panics
///
/// This method does not panic, but returns an error for invalid notation.
pub fn from_notation(s: &str) -> Result<Self, PokerError> {
if s.len() < 2 || s.len() > 3 {
return Err(PokerError::InvalidHoleCardsNotationLength { length: s.len() });
}
let chars: Vec<char> = s.chars().collect();
// Parse ranks
let rank1 = Card::rank_from_char(chars[0]).ok_or(PokerError::InvalidRankCharacter {
character: chars[0],
})?;
let rank2 = Card::rank_from_char(chars[1]).ok_or(PokerError::InvalidRankCharacter {
character: chars[1],
})?;
// Determine suitedness
let suited = if s.len() == 3 {
// Pairs cannot have suitedness indicators
if rank1 == rank2 {
return Err(PokerError::PairsCannotHaveSuitedness);
}
match chars[2] {
's' => true,
'o' => false,
_ => {
return Err(PokerError::InvalidSuitednessIndicator {
indicator: chars[2],
})
}
}
} else {
// For pairs, always use different suits
if rank1 != rank2 {
return Err(PokerError::NonPairMustSpecifySuitedness);
}
false // pairs are neither suited nor offsuit in notation
};
// Create cards - for suited hands, use spades; for offsuit, different suits
let suit1 = 3; // Spades
let suit2 = if suited { 3 } else { 2 }; // Spades or hearts
let card1 = Card::new(rank1, suit1).unwrap();
let card2 = Card::new(rank2, suit2).unwrap();
Self::new(card1, card2)
}
/// Get the first card (higher rank)
///
/// # Examples
///
/// ```rust
/// use holdem_core::{HoleCards, Card};
///
/// let hole_cards = HoleCards::new(Card::new(12, 0).unwrap(), Card::new(11, 1).unwrap()).unwrap();
/// let first = hole_cards.first_card();
/// assert_eq!(first.rank(), 12); // Ace
/// ```
///
/// # Panics
///
/// This method does not panic.
pub fn first_card(&self) -> Card {
self.cards[0]
}
/// Get the second card (lower rank)
///
/// # Examples
///
/// ```rust
/// use holdem_core::{HoleCards, Card};
///
/// let hole_cards = HoleCards::new(Card::new(12, 0).unwrap(), Card::new(11, 1).unwrap()).unwrap();
/// let second = hole_cards.second_card();
/// assert_eq!(second.rank(), 11); // King
/// ```
///
/// # Panics
///
/// This method does not panic.
pub fn second_card(&self) -> Card {
self.cards[1]
}
/// Check if the hole cards form a pocket pair
///
/// # Examples
/// ```
/// use holdem_core::hole_cards::HoleCards;
///
/// let pocket_aces = HoleCards::from_notation("AA").unwrap();
/// assert!(pocket_aces.is_pair());
///
/// let suited_king_queen = HoleCards::from_notation("KQs").unwrap();
/// assert!(!suited_king_queen.is_pair());
/// ```
///
/// # Panics
///
/// This method does not panic.
pub fn is_pair(&self) -> bool {
self.cards[0].rank() == self.cards[1].rank()
}
/// Check if both cards have the same suit (suited)
///
/// # Examples
/// ```
/// use holdem_core::hole_cards::HoleCards;
///
/// let suited_ace_king = HoleCards::from_notation("AKs").unwrap();
/// assert!(suited_ace_king.is_suited());
///
/// let offsuit_queen_jack = HoleCards::from_notation("QJo").unwrap();
/// assert!(!offsuit_queen_jack.is_suited());
/// ```
///
/// # Panics
///
/// This method does not panic.
pub fn is_suited(&self) -> bool {
self.cards[0].suit() == self.cards[1].suit()
}
/// Calculate the connectivity between the two cards
///
/// Returns the number of ranks between the cards (0 = connected, 1 = one gap, etc.)
/// For pairs, returns 0 (perfectly connected).
///
/// # Examples
/// ```
/// use holdem_core::hole_cards::HoleCards;
///
/// let connected = HoleCards::from_notation("KQs").unwrap();
/// assert_eq!(connected.connectivity(), 0); // K and Q are adjacent
///
/// let one_gapper = HoleCards::from_notation("J9s").unwrap();
/// assert_eq!(one_gapper.connectivity(), 1); // J and 9 have one rank between (T)
///
/// let pocket_pair = HoleCards::from_notation("77").unwrap();
/// assert_eq!(pocket_pair.connectivity(), 0); // Pairs are perfectly connected
/// ```
///
/// # Panics
///
/// This method does not panic.
pub fn connectivity(&self) -> u8 {
if self.is_pair() {
0
} else {
let rank1 = self.cards[0].rank() as i8;
let rank2 = self.cards[1].rank() as i8;
(rank1 - rank2 - 1) as u8
}
}
/// Generate standard poker notation for the hole cards
///
/// # Format
/// - Pairs: "AA", "KK", "22"
/// - Suited hands: "AKs", "QJs", "T9s"
/// - Offsuit hands: "AKo", "QJo", "T9o"
///
/// # Examples
/// ```
/// use holdem_core::hole_cards::HoleCards;
///
/// let pocket_aces = HoleCards::from_notation("AA").unwrap();
/// assert_eq!(pocket_aces.notation(), "AA");
///
/// let suited_king_queen = HoleCards::from_notation("KQs").unwrap();
/// assert_eq!(suited_king_queen.notation(), "KQs");
///
/// let offsuit_jack_ten = HoleCards::from_notation("JTo").unwrap();
/// assert_eq!(offsuit_jack_ten.notation(), "JTo");
/// ```
///
/// # Panics
///
/// This method does not panic.
pub fn notation(&self) -> String {
let rank1_char = Card::rank_to_char(self.cards[0].rank());
let rank2_char = Card::rank_to_char(self.cards[1].rank());
if self.is_pair() {
format!("{}{}", rank1_char, rank2_char)
} else if self.is_suited() {
format!("{}{}s", rank1_char, rank2_char)
} else {
format!("{}{}o", rank1_char, rank2_char)
}
}
/// Get a display-friendly string representation
///
/// # Examples
///
/// ```rust
/// use holdem_core::hole_cards::HoleCards;
///
/// let aks = HoleCards::from_notation("AKs").unwrap();
/// let string_repr = aks.to_string();
/// assert!(string_repr.contains("A"));
/// assert!(string_repr.contains("K"));
/// ```
///
/// # Panics
///
/// This method does not panic.
pub fn to_string(&self) -> String {
format!(
"{} {}",
self.cards[0].to_string(),
self.cards[1].to_string()
)
}
}
impl fmt::Display for HoleCards {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.notation())
}
}
impl PartialOrd for HoleCards {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for HoleCards {
fn cmp(&self, other: &Self) -> Ordering {
// Compare by first card rank, then second card rank, then suitedness
match self.cards[0].rank().cmp(&other.cards[0].rank()) {
Ordering::Equal => match self.cards[1].rank().cmp(&other.cards[1].rank()) {
Ordering::Equal => self.is_suited().cmp(&other.is_suited()),
ord => ord,
},
ord => ord,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_hole_cards_creation() {
let card1 = Card::from_str("As").unwrap();
let card2 = Card::from_str("Ks").unwrap();
let hole_cards = HoleCards::new(card1, card2).unwrap();
assert_eq!(hole_cards.first_card(), card1);
assert_eq!(hole_cards.second_card(), card2);
}
#[test]
fn test_duplicate_cards_error() {
let card = Card::from_str("As").unwrap();
let result = HoleCards::new(card, card);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), PokerError::DuplicateCard(_)));
}
#[test]
fn test_from_notation_pairs() {
let aa = HoleCards::from_notation("AA").unwrap();
assert!(aa.is_pair());
assert_eq!(aa.notation(), "AA");
let kk = HoleCards::from_notation("KK").unwrap();
assert!(kk.is_pair());
assert_eq!(kk.notation(), "KK");
}
#[test]
fn test_from_notation_suited() {
let aks = HoleCards::from_notation("AKs").unwrap();
assert!(aks.is_suited());
assert!(!aks.is_pair());
assert_eq!(aks.notation(), "AKs");
assert_eq!(aks.connectivity(), 0);
}
#[test]
fn test_from_notation_offsuit() {
let ako = HoleCards::from_notation("AKo").unwrap();
assert!(!ako.is_suited());
assert!(!ako.is_pair());
assert_eq!(ako.notation(), "AKo");
assert_eq!(ako.connectivity(), 0);
}
#[test]
fn test_connectivity() {
let connected = HoleCards::from_notation("KQs").unwrap();
assert_eq!(connected.connectivity(), 0);
let one_gapper = HoleCards::from_notation("J9s").unwrap();
assert_eq!(one_gapper.connectivity(), 1);
let two_gapper = HoleCards::from_notation("K8s").unwrap();
assert_eq!(two_gapper.connectivity(), 4); // K to 8 has Q, J, T, 9 between - 4 gaps
let pair = HoleCards::from_notation("77").unwrap();
assert_eq!(pair.connectivity(), 0);
}
#[test]
fn test_invalid_notation() {
assert!(HoleCards::from_notation("").is_err());
assert!(HoleCards::from_notation("A").is_err());
assert!(HoleCards::from_notation("AKx").is_err());
assert!(HoleCards::from_notation("AK").is_err()); // Missing suitedness for non-pair
assert!(HoleCards::from_notation("1K").is_err()); // Invalid rank
}
#[test]
fn test_ordering() {
let aks = HoleCards::from_notation("AKs").unwrap();
let ako = HoleCards::from_notation("AKo").unwrap();
let aqs = HoleCards::from_notation("AQs").unwrap();
assert!(aks > ako); // Suited beats offsuit for same ranks
assert!(aqs < aks); // AK beats AQ
}
#[test]
fn test_display() {
let aks = HoleCards::from_notation("AKs").unwrap();
assert_eq!(format!("{}", aks), "AKs");
let qq = HoleCards::from_notation("QQ").unwrap();
assert_eq!(format!("{}", qq), "QQ");
}
#[test]
fn test_serialization() {
let hole_cards = HoleCards::from_notation("AKs").unwrap();
let serialized = serde_json::to_string(&hole_cards).unwrap();
let deserialized: HoleCards = serde_json::from_str(&serialized).unwrap();
assert_eq!(hole_cards, deserialized);
}
#[test]
fn test_hole_cards_validation_edge_cases() {
// Test with same rank different suits (should work)
let card1 = Card::new(12, 3).unwrap(); // Ace of spades
let card2 = Card::new(12, 0).unwrap(); // Ace of hearts
let hole_cards = HoleCards::new(card1, card2).unwrap();
assert!(hole_cards.is_pair());
// Test with different ranks same suit (should work)
let card1 = Card::new(12, 3).unwrap(); // Ace of spades
let card2 = Card::new(11, 3).unwrap(); // King of spades
let hole_cards = HoleCards::new(card1, card2).unwrap();
assert!(hole_cards.is_suited());
// Test duplicate cards (should fail)
let card = Card::new(12, 3).unwrap();
assert!(HoleCards::new(card, card).is_err());
}
#[test]
fn test_hole_cards_properties_comprehensive() {
// Test all possible pair combinations
for rank in 0..13 {
let notation = format!("{}{}", Card::rank_to_char(rank), Card::rank_to_char(rank));
let hole_cards = HoleCards::from_notation(&notation).unwrap();
assert!(hole_cards.is_pair());
assert!(!hole_cards.is_suited()); // Pairs are neither suited nor offsuit
assert_eq!(hole_cards.connectivity(), 0);
assert_eq!(hole_cards.notation(), notation);
}
// Test suited hands
let suited_hands = [
"AKs", "AQs", "AJs", "ATs", "A9s", "A8s", "A7s", "A6s", "A5s", "A4s", "A3s", "A2s",
"KQs", "KJs", "KTs", "K9s", "K8s", "K7s", "K6s", "K5s", "K4s", "K3s", "K2s", "QJs",
"QTs", "Q9s", "Q8s", "Q7s", "Q6s", "Q5s", "Q4s", "Q3s", "Q2s",
];
for hand in suited_hands.iter() {
let hole_cards = HoleCards::from_notation(hand).unwrap();
assert!(hole_cards.is_suited());
assert!(!hole_cards.is_pair());
assert_eq!(hole_cards.notation(), *hand);
}
// Test offsuit hands
let offsuit_hands = [
"AKo", "AQo", "AJo", "ATo", "A9o", "A8o", "A7o", "A6o", "A5o", "A4o", "A3o", "A2o",
];
for hand in offsuit_hands.iter() {
let hole_cards = HoleCards::from_notation(hand).unwrap();
assert!(!hole_cards.is_suited());
assert!(!hole_cards.is_pair());
assert_eq!(hole_cards.notation(), *hand);
}
}
#[test]
fn test_hole_cards_connectivity_edge_cases() {
// Test maximum gap (A2)
let a2s = HoleCards::from_notation("A2s").unwrap();
assert_eq!(a2s.connectivity(), 11); // A to 2 has K,Q,J,T,9,8,7,6,5,4,3 between - 11 gaps
// Test minimum gap (AK, KQ, etc.)
let aks = HoleCards::from_notation("AKs").unwrap();
assert_eq!(aks.connectivity(), 0);
let kqs = HoleCards::from_notation("KQs").unwrap();
assert_eq!(kqs.connectivity(), 0);
// Test various gaps
let j9s = HoleCards::from_notation("J9s").unwrap();
assert_eq!(j9s.connectivity(), 1); // J to 9 has T between
let t7s = HoleCards::from_notation("T7s").unwrap();
assert_eq!(t7s.connectivity(), 2); // T to 7 has 9,8 between
let k8s = HoleCards::from_notation("K8s").unwrap();
assert_eq!(k8s.connectivity(), 4); // K to 8 has Q,J,T,9 between
}
#[test]
fn test_hole_cards_notation_comprehensive() {
// Test all possible notations generate correctly
let test_cases = [
("AA", "AA"),
("AKs", "AKs"),
("AKo", "AKo"),
("22", "22"),
("T9s", "T9s"),
("J8o", "J8o"),
];
for (input, expected) in test_cases.iter() {
let hole_cards = HoleCards::from_notation(input).unwrap();
assert_eq!(hole_cards.notation(), *expected);
}
// Test round-trip: notation -> HoleCards -> notation
let all_notations = [
"AA", "KK", "QQ", "JJ", "TT", "99", "88", "77", "66", "55", "44", "33", "22", "AKs",
"AQs", "AJs", "ATs", "A9s", "A8s", "A7s", "A6s", "A5s", "A4s", "A3s", "A2s", "AKo",
"AQo", "AJo", "ATo", "A9o", "A8o", "A7o", "A6o", "A5o", "A4o", "A3o", "A2o", "KQs",
"KJs", "KTs", "K9s", "K8s", "K7s", "K6s", "K5s", "K4s", "K3s", "K2s", "QJs", "QTs",
"Q9s", "Q8s", "Q7s", "Q6s", "Q5s", "Q4s", "Q3s", "Q2s", "JTs", "J9s", "J8s", "J7s",
"J6s", "J5s", "J4s", "J3s", "J2s", "T9s", "T8s", "T7s", "T6s", "T5s", "T4s", "T3s",
"T2s", "98s", "97s", "96s", "95s", "94s", "93s", "92s", "87s", "86s", "85s", "84s",
"83s", "82s", "76s", "75s", "74s", "73s", "72s", "65s", "64s", "63s", "62s", "54s",
"53s", "52s", "43s", "42s", "32s",
];
for notation in all_notations.iter() {
let hole_cards = HoleCards::from_notation(notation).unwrap();
assert_eq!(hole_cards.notation(), *notation);
}
}
#[test]
fn test_hole_cards_notation_invalid() {
// Test various invalid notations
let invalid_cases = [
"", "A", "ABCD", "AKx", "AK", "AAs", "AAo", "AK", "1K", "AK1", "AKz", "ak", "AKS",
"ako", "A K s", "AKss", "AA s",
];
for invalid in invalid_cases.iter() {
assert!(
HoleCards::from_notation(invalid).is_err(),
"Expected '{}' to be invalid",
invalid
);
}
// Test non-pair without suitedness indicator
assert!(HoleCards::from_notation("AK").is_err());
assert!(HoleCards::from_notation("QJ").is_err());
}
#[test]
fn test_hole_cards_ordering_comprehensive() {
// Test that ordering is consistent and transitive
let hands = [
HoleCards::from_notation("AA").unwrap(),
HoleCards::from_notation("AKs").unwrap(),
HoleCards::from_notation("AKo").unwrap(),
HoleCards::from_notation("AQs").unwrap(),
HoleCards::from_notation("AQo").unwrap(),
HoleCards::from_notation("22").unwrap(),
];
// Test transitivity
for i in 0..hands.len() {
for j in 0..hands.len() {
for k in 0..hands.len() {
let cmp_ij = hands[i].cmp(&hands[j]);
let cmp_jk = hands[j].cmp(&hands[k]);
let cmp_ik = hands[i].cmp(&hands[k]);
// If i >= j and j >= k, then i >= k
if cmp_ij != Ordering::Less && cmp_jk != Ordering::Less {
assert!(
cmp_ik != Ordering::Less,
"Transitivity violated: {} >= {} >= {} but {} < {}",
hands[i],
hands[j],
hands[k],
hands[i],
hands[k]
);
}
}
}
}
// Test specific ordering expectations
assert!(HoleCards::from_notation("AA").unwrap() > HoleCards::from_notation("AKs").unwrap());
assert!(
HoleCards::from_notation("AKs").unwrap() > HoleCards::from_notation("AKo").unwrap()
);
assert!(
HoleCards::from_notation("AKs").unwrap() > HoleCards::from_notation("AQs").unwrap()
);
}
#[test]
fn test_hole_cards_serialization_comprehensive() {
// Test JSON serialization round-trip for various hands
let test_hands = ["AA", "AKs", "AKo", "22", "T9s", "J8o"];
for hand in test_hands.iter() {
let hole_cards = HoleCards::from_notation(hand).unwrap();
// JSON round-trip
let json = serde_json::to_string(&hole_cards).unwrap();
let deserialized: HoleCards = serde_json::from_str(&json).unwrap();
assert_eq!(hole_cards, deserialized);
// TOML round-trip
let toml = toml::to_string(&hole_cards).unwrap();
let deserialized: HoleCards = toml::from_str(&toml).unwrap();
assert_eq!(hole_cards, deserialized);
}
// Test serialization of edge cases
let max_gap = HoleCards::from_notation("A2s").unwrap();
let json = serde_json::to_string(&max_gap).unwrap();
let deserialized: HoleCards = serde_json::from_str(&json).unwrap();
assert_eq!(max_gap, deserialized);
}
#[test]
fn test_hole_cards_hash_consistency() {
use std::collections::HashSet;
// Test that equal hole cards have equal hashes
let hc1 = HoleCards::from_notation("AKs").unwrap();
let hc2 = HoleCards::from_notation("AKs").unwrap();
assert_eq!(hc1, hc2);
// Test hash distribution (no major collisions for common hands)
let mut hashes = HashSet::new();
let common_hands = ["AA", "AKs", "AKo", "AQs", "AQo", "AJs", "AJo"];
for hand in common_hands.iter() {
let hc = HoleCards::from_notation(hand).unwrap();
let hash = {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
hc.hash(&mut hasher);
hasher.finish()
};
hashes.insert(hash);
}
// Should have no collisions for these distinct hands
assert_eq!(hashes.len(), common_hands.len());
}
#[test]
fn test_hole_cards_to_string() {
let aks = HoleCards::from_notation("AKs").unwrap();
let string_repr = aks.to_string();
assert!(string_repr.contains("A"));
assert!(string_repr.contains("K"));
assert!(string_repr.len() > 3); // Should be more than just "AKs"
}
#[test]
fn test_hole_cards_card_access() {
let aks = HoleCards::from_notation("AKs").unwrap();
let first = aks.first_card();
let second = aks.second_card();
assert_eq!(first.rank(), 12); // Ace
assert_eq!(second.rank(), 11); // King
assert_eq!(first.suit(), second.suit()); // Both spades
}
}

77
holdem_core/src/lib.rs Normal file
View file

@ -0,0 +1,77 @@
//! # Poker Core Library
//!
//! Core data structures for poker AI systems. This library provides the fundamental
//! representations for cards, decks, hole cards, hands, and community boards used
//! in poker games, optimized for performance with zero-based enumerations.
//!
//! ## Quick Start
//!
//! ```rust
//! use holdem_core::{Card, Deck, HoleCards, Hand, Board};
//! use std::str::FromStr;
//!
//! // Create a card
//! let card = Card::from_str("As").unwrap();
//!
//! // Create hole cards
//! let hole_cards = HoleCards::from_notation("AKs").unwrap();
//!
//! // Create a board using the fluent builder pattern
//! let board = Board::new()
//! .with_flop([
//! Card::from_str("Kd").unwrap(),
//! Card::from_str("Qh").unwrap(),
//! Card::from_str("Jc").unwrap(),
//! ])
//! .unwrap()
//! .with_turn(Card::from_str("Ts").unwrap()).unwrap()
//! .with_river(Card::from_str("9h").unwrap()).unwrap();
//!
//! // Create a complete hand
//! let hand = Hand::from_hole_cards_and_board(&hole_cards, &board).unwrap();
//!
//! // Or create hands from notation strings
//! let hand = Hand::from_notation("As Ks Qs Js Ts").unwrap();
//! ```
//!
//! ## Features
//!
//! - **Performance Optimized**: Zero-based enums for fast bit manipulation and lookup tables
//! - **Memory Efficient**: Compact data structures for mass simulations
//! - **Serialization Ready**: TOML and JSON support for configuration and networking
//! - **Texas Hold'em Support**: Complete representation of hole cards, boards, and hands
//! - **Type Safe**: Strong typing prevents invalid poker states
/// Core poker card representation with zero-based rank/suit enums
pub mod card;
/// Deck of cards representation with shuffle and deal functionality
pub mod deck;
/// Complete poker hand representation for 5-7 card evaluation
pub mod hand;
/// Hole cards representation for player's private cards
pub mod hole_cards;
/// Community cards (board) representation with betting street management
pub mod board;
/// Comprehensive error types for poker operations
pub mod errors;
/// Re-export holdem_core types for convenience
pub use board::Board;
pub use card::Card;
pub use deck::Deck;
pub use hand::Hand;
pub use hole_cards::HoleCards;
/// Re-export Street enum for convenience
pub use board::Street;
/// Re-export error types for convenience
pub use errors::PokerError;
#[cfg(test)]
mod tests {}