Holdem basic core structures from scratch.
This commit is contained in:
parent
f02c35bef3
commit
e94bf42dce
53 changed files with 6316 additions and 11906 deletions
27
holdem_core/Cargo.toml
Normal file
27
holdem_core/Cargo.toml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
[package]
|
||||
name = "holdem_core"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Jan-Peter v. Hunnius"]
|
||||
description = "Poker library for bot testing frameworks"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/jhunnius/rusty_marvin"
|
||||
documentation = "https://docs.rs/holdem_core"
|
||||
homepage = "https://github.com/your-org/holdem_core"
|
||||
keywords = ["poker", "bot-testing", "texas-holdem", "testbed"]
|
||||
categories = ["games", "api-bindings", "mathematics"]
|
||||
exclude = ["docs/*", "target/*", "*.md"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
rand = "0.9.0"
|
||||
toml = "0.9.8"
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "1.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
212
holdem_core/README.md
Normal file
212
holdem_core/README.md
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
# Poker API Core Library
|
||||
|
||||
## Overview
|
||||
|
||||
The Poker API Core Library (`poker-api`) is a high-performance poker hand evaluation library based on the Meerkat API algorithm. This library provides lightning-fast poker hand strength evaluation using precomputed lookup tables and perfect hashing.
|
||||
|
||||
## Recent Updates
|
||||
|
||||
- **Input Validation**: Added comprehensive validation to `Card::new()` ensuring rank (0-12) and suit (0-3) are within valid ranges
|
||||
- **Enhanced Documentation**: All public methods now include working code examples and panic condition documentation
|
||||
- **Error Handling**: Improved error handling throughout the codebase with proper `Result` types
|
||||
|
||||
## Features
|
||||
|
||||
- **Blazing Fast**: O(1) hand evaluation using perfect hash lookup tables
|
||||
- **Memory Efficient**: ~128MB for complete evaluation tables
|
||||
- **Standards Compliant**: Based on proven poker evaluation algorithms
|
||||
- **Multi-Hand Support**: Evaluate 5, 6, or 7-card poker hands
|
||||
- **Texas Hold'em Ready**: Optimized for Texas Hold'em hand evaluation
|
||||
- **Comprehensive Testing**: Extensive test suite with millions of verification cases
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
#### API Module (`src/api/`)
|
||||
The main application programming interface providing:
|
||||
- **Card Management**: `Card` struct for individual playing cards
|
||||
- **Hand Management**: `Hand` struct for collections of cards
|
||||
- **Deck Operations**: `Deck` struct for standard 52-card deck management
|
||||
- **Player Actions**: `Action` types for betting operations
|
||||
- **Game State**: `GameInfo` trait for accessing game information
|
||||
- **Bot Interface**: `Player` trait for poker bot implementations
|
||||
|
||||
#### Hand Evaluator (`src/hand_evaluator.rs`)
|
||||
High-performance poker hand evaluation using precomputed lookup tables:
|
||||
- **Perfect Hash Algorithm**: Maps card combinations to unique indices
|
||||
- **Table-Based Evaluation**: O(1) lookup time after table generation
|
||||
- **Multi-Hand Support**: Handles 5, 6, and 7-card evaluations
|
||||
- **Memory Management**: Efficient table loading and caching
|
||||
|
||||
#### Evaluator Generator (`src/evaluator_generator/`)
|
||||
Table generation system for poker hand evaluation:
|
||||
- **Flushes** (`flushes.rs`): Lookup table for flush hand evaluation
|
||||
- **Products** (`products.rs`): Prime product-based hand type detection
|
||||
- **Unique** (`unique.rs`): Special hand types (straight flushes, quads, etc.)
|
||||
- **Values** (`values.rs`): Standard hand ranking and comparison
|
||||
- **Pairs** (`pair.rs`): Two-card combination analysis and lookup
|
||||
|
||||
#### Supporting Modules
|
||||
- **Simple Player** (`src/simple_player.rs`): Basic bot implementation example
|
||||
- **TOML Preferences** (`src/toml_preferences.rs`): Configuration management
|
||||
- **Texas Hold'em** (`src/texas_holdem/`): Game-specific logic (planned)
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
| Hand Type | Evaluation Time | Combinations Evaluated |
|
||||
|-----------|----------------|----------------------|
|
||||
| 5-Card | ~10-20ns | Direct lookup |
|
||||
| 6-Card | ~50-80ns | 6 combinations |
|
||||
| 7-Card | ~150-250ns | 21 combinations |
|
||||
|
||||
### Memory Usage
|
||||
- **Table Generation**: ~2.4MB working memory during generation
|
||||
- **Runtime Tables**: ~128MB for complete hand ranking tables
|
||||
- **Loading Time**: ~100-200ms from disk (after generation)
|
||||
|
||||
## Algorithm Overview
|
||||
|
||||
### Perfect Hash Algorithm
|
||||
|
||||
The evaluator uses a perfect hash function that maps each possible card combination to a unique index:
|
||||
|
||||
```
|
||||
Hash = f(card₁, card₂, ..., cardₙ)
|
||||
Index = Hash % TABLE_SIZE
|
||||
Rank = TABLE[Index]
|
||||
```
|
||||
|
||||
### Card Encoding
|
||||
|
||||
Each card is encoded using multiple techniques:
|
||||
- **Prime Numbers**: Unique primes for rank identification (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41)
|
||||
- **Bit Patterns**: Suit detection using bit manipulation
|
||||
- **Binary Representation**: Compact 8-bit card encoding
|
||||
|
||||
### Hand Strength Calculation
|
||||
|
||||
Hand strength is calculated using:
|
||||
1. **Flush Detection**: Bitwise suit comparison
|
||||
2. **Rank Analysis**: Prime product uniqueness
|
||||
3. **Pattern Matching**: Specialized algorithms per hand type
|
||||
4. **Relative Ranking**: Comparison against all possible hands
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Hand Evaluation
|
||||
|
||||
```rust
|
||||
use holdem_core::hand_evaluator::LookupHandEvaluator;
|
||||
use holdem_core::api::hand::Hand;
|
||||
use holdem_core::api::card::Card;
|
||||
|
||||
// Create evaluator (generates tables on first use)
|
||||
let evaluator = LookupHandEvaluator::new().unwrap();
|
||||
|
||||
// Create a poker hand
|
||||
let mut hand = Hand::new();
|
||||
hand.add_card(Card::from_string("As").unwrap()).unwrap();
|
||||
hand.add_card(Card::from_string("Ks").unwrap()).unwrap();
|
||||
hand.add_card(Card::from_string("Qs").unwrap()).unwrap();
|
||||
hand.add_card(Card::from_string("Js").unwrap()).unwrap();
|
||||
hand.add_card(Card::from_string("Ts").unwrap()).unwrap();
|
||||
|
||||
// Evaluate hand strength
|
||||
let rank = evaluator.rank_hand(&hand);
|
||||
println!("Hand rank: {}", rank);
|
||||
```
|
||||
|
||||
### 7-Card Hand Evaluation (Texas Hold'em)
|
||||
|
||||
```rust
|
||||
use holdem_core::hand_evaluator::LookupHandEvaluator;
|
||||
use holdem_core::api::card::Card;
|
||||
|
||||
let evaluator = LookupHandEvaluator::new().unwrap();
|
||||
|
||||
// Texas Hold'em: 2 hole cards + 5 community cards
|
||||
let cards = [
|
||||
Card::from_string("As").unwrap(),
|
||||
Card::from_string("Ks").unwrap(),
|
||||
Card::from_string("Td").unwrap(),
|
||||
Card::from_string("Jc").unwrap(),
|
||||
Card::from_string("Qh").unwrap(),
|
||||
Card::from_string("9s").unwrap(),
|
||||
Card::from_string("2d").unwrap(),
|
||||
];
|
||||
|
||||
let rank = evaluator.rank_hand7(&cards);
|
||||
println!("Best 5-card hand rank: {}", rank);
|
||||
```
|
||||
|
||||
### Custom Table Generation
|
||||
|
||||
```rust
|
||||
use holdem_core::evaluator_generator::state_table_generator::StateTableGenerator;
|
||||
|
||||
let mut generator = StateTableGenerator::new();
|
||||
generator.generate_tables(); // ~1-2 seconds
|
||||
generator.save_tables().unwrap(); // Save to disk
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run the comprehensive test suite:
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
Run performance benchmarks:
|
||||
|
||||
```bash
|
||||
cargo test --release hand_eval_integration_test -- --nocapture
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# Build library
|
||||
cargo build
|
||||
|
||||
# Build with optimizations
|
||||
cargo build --release
|
||||
|
||||
# Generate documentation
|
||||
cargo doc --open
|
||||
```
|
||||
|
||||
### Code Organization
|
||||
|
||||
- **API Types**: `src/api/` - Core data types and traits
|
||||
- **Evaluation**: `src/hand_evaluator.rs` - Main evaluation logic
|
||||
- **Table Generation**: `src/evaluator_generator/` - Lookup table creation
|
||||
- **Examples**: `src/simple_player.rs` - Basic bot implementation
|
||||
- **Configuration**: `src/toml_preferences.rs` - Settings management
|
||||
|
||||
### Contributing
|
||||
|
||||
Areas for improvement:
|
||||
- Hardware acceleration (SIMD optimizations)
|
||||
- Additional poker variants (Omaha, Stud, etc.)
|
||||
- Memory-mapped table loading
|
||||
- Distributed table generation
|
||||
- Advanced benchmarking tools
|
||||
|
||||
## Integration
|
||||
|
||||
This core library serves as the foundation for:
|
||||
- **Poker Bots**: AI players in `bots/` directory
|
||||
- **Simulations**: Game simulation tools in `simulator/` directory
|
||||
- **Analysis Tools**: Log analysis and monitoring in `tools/` directory
|
||||
- **Online Play**: Networked poker applications in `online/` directory
|
||||
|
||||
## References
|
||||
|
||||
- **Meerkat API**: Original Java implementation by Ray Wotton
|
||||
- **C Implementation**: Paul Senzee's optimized C version
|
||||
- **Perfect Hash**: Kevin Suffecool's hashing algorithm
|
||||
- **Poker Standards**: 2+2 Poker hand ranking standards
|
||||
1383
holdem_core/src/board.rs
Normal file
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
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
495
holdem_core/src/deck.rs
Normal 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
202
holdem_core/src/errors.rs
Normal 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
1130
holdem_core/src/hand.rs
Normal file
File diff suppressed because it is too large
Load diff
746
holdem_core/src/hole_cards.rs
Normal file
746
holdem_core/src/hole_cards.rs
Normal 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(¬ation).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
77
holdem_core/src/lib.rs
Normal 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 {}
|
||||
400
holdem_core/tests/integration_tests.rs
Normal file
400
holdem_core/tests/integration_tests.rs
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
//! Integration tests for poker data structures
|
||||
//!
|
||||
//! These tests verify that the holdem_core poker structures work together correctly,
|
||||
//! including serialization round-trips and performance benchmarks.
|
||||
|
||||
use holdem_core::{board, card, deck, hand, hole_cards};
|
||||
use std::time::Instant;
|
||||
|
||||
#[test]
|
||||
fn test_integration_hand_from_hole_cards_and_board() {
|
||||
// Test creating hands from various hole card and board combinations
|
||||
|
||||
// Test with preflop (no board cards)
|
||||
let hole_cards = hole_cards::HoleCards::new(
|
||||
card::Card::new(12, 0).unwrap(), // As
|
||||
card::Card::new(11, 1).unwrap(), // Kh
|
||||
)
|
||||
.unwrap();
|
||||
let empty_board = board::Board::new();
|
||||
let preflop_hand = hand::Hand::from_hole_cards_and_board(&hole_cards, &empty_board).unwrap();
|
||||
assert_eq!(preflop_hand.len, 2);
|
||||
assert_eq!(preflop_hand.cards()[0], card::Card::new(12, 0).unwrap());
|
||||
assert_eq!(preflop_hand.cards()[1], card::Card::new(11, 1).unwrap());
|
||||
|
||||
// Test with flop
|
||||
let mut flop_board = board::Board::new();
|
||||
flop_board
|
||||
.deal_flop(vec![
|
||||
card::Card::new(10, 2).unwrap(), // Qd
|
||||
card::Card::new(9, 3).unwrap(), // Js
|
||||
card::Card::new(8, 0).unwrap(), // Tc
|
||||
])
|
||||
.unwrap();
|
||||
let flop_hand = hand::Hand::from_hole_cards_and_board(&hole_cards, &flop_board).unwrap();
|
||||
assert_eq!(flop_hand.len, 5);
|
||||
assert_eq!(flop_hand.cards()[0], card::Card::new(12, 0).unwrap()); // As
|
||||
assert_eq!(flop_hand.cards()[1], card::Card::new(11, 1).unwrap()); // Kh
|
||||
assert_eq!(flop_hand.cards()[2], card::Card::new(10, 2).unwrap()); // Qd
|
||||
assert_eq!(flop_hand.cards()[3], card::Card::new(9, 3).unwrap()); // Js
|
||||
assert_eq!(flop_hand.cards()[4], card::Card::new(8, 0).unwrap()); // Tc
|
||||
|
||||
// Test with turn
|
||||
let mut turn_board = flop_board.clone();
|
||||
turn_board
|
||||
.deal_turn(card::Card::new(7, 1).unwrap())
|
||||
.unwrap(); // 9h
|
||||
let turn_hand = hand::Hand::from_hole_cards_and_board(&hole_cards, &turn_board).unwrap();
|
||||
assert_eq!(turn_hand.len, 6);
|
||||
|
||||
// Test with river
|
||||
let mut river_board = turn_board.clone();
|
||||
river_board
|
||||
.deal_river(card::Card::new(6, 2).unwrap())
|
||||
.unwrap(); // 8d
|
||||
let river_hand = hand::Hand::from_hole_cards_and_board(&hole_cards, &river_board).unwrap();
|
||||
assert_eq!(river_hand.len, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_integration_hand_from_hole_cards_and_board_edge_cases() {
|
||||
// Test edge cases for hand creation from hole cards and board
|
||||
|
||||
// Test with pocket aces and board
|
||||
let pocket_aces = hole_cards::HoleCards::new(
|
||||
card::Card::new(12, 0).unwrap(), // As
|
||||
card::Card::new(12, 1).unwrap(), // Ah
|
||||
)
|
||||
.unwrap();
|
||||
let mut board = board::Board::new();
|
||||
board
|
||||
.deal_flop(vec![
|
||||
card::Card::new(10, 2).unwrap(), // Qd
|
||||
card::Card::new(9, 3).unwrap(), // Js
|
||||
card::Card::new(8, 0).unwrap(), // Tc
|
||||
])
|
||||
.unwrap();
|
||||
let hand = hand::Hand::from_hole_cards_and_board(&pocket_aces, &board).unwrap();
|
||||
assert_eq!(hand.len, 5);
|
||||
|
||||
// Test with suited connectors
|
||||
let suited_connectors = hole_cards::HoleCards::new(
|
||||
card::Card::new(11, 0).unwrap(), // Ks
|
||||
card::Card::new(10, 0).unwrap(), // Qs
|
||||
)
|
||||
.unwrap();
|
||||
let hand = hand::Hand::from_hole_cards_and_board(&suited_connectors, &board).unwrap();
|
||||
assert_eq!(hand.len, 5);
|
||||
|
||||
// Test with empty board (should work)
|
||||
let empty_board = board::Board::new();
|
||||
let hand = hand::Hand::from_hole_cards_and_board(&pocket_aces, &empty_board).unwrap();
|
||||
assert_eq!(hand.len, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_integration_serialization_round_trips() {
|
||||
// Test that all structures can be serialized and deserialized correctly
|
||||
|
||||
// Test Card serialization round-trip
|
||||
let original_card = card::Card::new(12, 3).unwrap(); // Ace of Spades
|
||||
let json = serde_json::to_string(&original_card).unwrap();
|
||||
let deserialized_card: card::Card = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(original_card, deserialized_card);
|
||||
|
||||
// Test HoleCards serialization round-trip
|
||||
let original_hole_cards = hole_cards::HoleCards::new(
|
||||
card::Card::new(12, 0).unwrap(),
|
||||
card::Card::new(11, 1).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let json = serde_json::to_string(&original_hole_cards).unwrap();
|
||||
let deserialized_hole_cards: hole_cards::HoleCards = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(original_hole_cards, deserialized_hole_cards);
|
||||
|
||||
// Test Board serialization round-trip
|
||||
let mut original_board = board::Board::new();
|
||||
original_board
|
||||
.deal_flop(vec![
|
||||
card::Card::new(10, 2).unwrap(),
|
||||
card::Card::new(9, 3).unwrap(),
|
||||
card::Card::new(8, 0).unwrap(),
|
||||
])
|
||||
.unwrap();
|
||||
original_board
|
||||
.deal_turn(card::Card::new(7, 1).unwrap())
|
||||
.unwrap();
|
||||
original_board
|
||||
.deal_river(card::Card::new(6, 2).unwrap())
|
||||
.unwrap();
|
||||
|
||||
let json = serde_json::to_string(&original_board).unwrap();
|
||||
let deserialized_board: board::Board = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(original_board, deserialized_board);
|
||||
|
||||
// Test Hand serialization round-trip
|
||||
let original_hand =
|
||||
hand::Hand::from_hole_cards_and_board(&original_hole_cards, &original_board).unwrap();
|
||||
let json = serde_json::to_string(&original_hand).unwrap();
|
||||
let deserialized_hand: hand::Hand = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(original_hand, deserialized_hand);
|
||||
|
||||
// Test Deck serialization round-trip
|
||||
let original_deck = deck::Deck::new();
|
||||
let json = serde_json::to_string(&original_deck).unwrap();
|
||||
let deserialized_deck: deck::Deck = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(original_deck.cards(), deserialized_deck.cards());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_integration_serialization_round_trips_comprehensive() {
|
||||
// Test serialization round-trips with various data formats and edge cases
|
||||
|
||||
// Test with TOML serialization
|
||||
let card = card::Card::new(12, 3).unwrap();
|
||||
let toml = toml::to_string(&card).unwrap();
|
||||
let deserialized: card::Card = toml::from_str(&toml).unwrap();
|
||||
assert_eq!(card, deserialized);
|
||||
|
||||
// Test complex hand serialization
|
||||
let hole_cards = hole_cards::HoleCards::new(
|
||||
card::Card::new(12, 0).unwrap(),
|
||||
card::Card::new(11, 1).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut board = board::Board::new();
|
||||
board
|
||||
.deal_flop(vec![
|
||||
card::Card::new(10, 2).unwrap(),
|
||||
card::Card::new(9, 3).unwrap(),
|
||||
card::Card::new(8, 0).unwrap(),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let hand = hand::Hand::from_hole_cards_and_board(&hole_cards, &board).unwrap();
|
||||
|
||||
// JSON round-trip
|
||||
let json = serde_json::to_string(&hand).unwrap();
|
||||
let deserialized: hand::Hand = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(hand, deserialized);
|
||||
|
||||
// TOML round-trip
|
||||
let toml = toml::to_string(&hand).unwrap();
|
||||
let deserialized: hand::Hand = toml::from_str(&toml).unwrap();
|
||||
assert_eq!(hand, deserialized);
|
||||
|
||||
// Test empty structures
|
||||
let empty_hand = hand::Hand::new(vec![]).unwrap();
|
||||
let json = serde_json::to_string(&empty_hand).unwrap();
|
||||
let deserialized: hand::Hand = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(empty_hand, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_integration_performance_benchmarks() {
|
||||
// Performance benchmarks for common operations
|
||||
|
||||
let iterations = 1000;
|
||||
|
||||
// Benchmark hand creation from hole cards and board
|
||||
let hole_cards = hole_cards::HoleCards::new(
|
||||
card::Card::new(12, 0).unwrap(),
|
||||
card::Card::new(11, 1).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut board = board::Board::new();
|
||||
board
|
||||
.deal_flop(vec![
|
||||
card::Card::new(10, 2).unwrap(),
|
||||
card::Card::new(9, 3).unwrap(),
|
||||
card::Card::new(8, 0).unwrap(),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let start = Instant::now();
|
||||
for _ in 0..iterations {
|
||||
let _hand = hand::Hand::from_hole_cards_and_board(&hole_cards, &board).unwrap();
|
||||
}
|
||||
let duration = start.elapsed();
|
||||
println!("Hand creation ({} iterations): {:?}", iterations, duration);
|
||||
assert!(
|
||||
duration.as_millis() < 100,
|
||||
"Hand creation too slow: {:?}",
|
||||
duration
|
||||
);
|
||||
|
||||
// Benchmark serialization round-trips
|
||||
let hand = hand::Hand::from_hole_cards_and_board(&hole_cards, &board).unwrap();
|
||||
let start = Instant::now();
|
||||
for _ in 0..iterations {
|
||||
let json = serde_json::to_string(&hand).unwrap();
|
||||
let _: hand::Hand = serde_json::from_str(&json).unwrap();
|
||||
}
|
||||
let duration = start.elapsed();
|
||||
println!(
|
||||
"Serialization round-trip ({} iterations): {:?}",
|
||||
iterations, duration
|
||||
);
|
||||
assert!(
|
||||
duration.as_millis() < 500,
|
||||
"Serialization too slow: {:?}",
|
||||
duration
|
||||
);
|
||||
|
||||
// Benchmark card creation and operations
|
||||
let start = Instant::now();
|
||||
let mut cards = Vec::new();
|
||||
for i in 0..iterations {
|
||||
cards.push(card::Card::new((i % 13) as u8, (i % 4) as u8));
|
||||
}
|
||||
let duration = start.elapsed();
|
||||
println!("Card creation ({} iterations): {:?}", iterations, duration);
|
||||
assert!(
|
||||
duration.as_millis() < 50,
|
||||
"Card creation too slow: {:?}",
|
||||
duration
|
||||
);
|
||||
|
||||
// Benchmark deck operations
|
||||
let start = Instant::now();
|
||||
for _ in 0..(iterations / 10) {
|
||||
// Fewer iterations for deck operations
|
||||
let mut deck = deck::Deck::new();
|
||||
let _ = deck.shuffle(&mut rand::rng());
|
||||
let _ = deck.deal(5);
|
||||
}
|
||||
let duration = start.elapsed();
|
||||
println!(
|
||||
"Deck shuffle and deal ({} iterations): {:?}",
|
||||
iterations / 10,
|
||||
duration
|
||||
);
|
||||
assert!(
|
||||
duration.as_millis() < 200,
|
||||
"Deck operations too slow: {:?}",
|
||||
duration
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_integration_complex_scenarios() {
|
||||
// Test complex real-world scenarios
|
||||
|
||||
// Test multiple hands from same board
|
||||
let mut board = board::Board::new();
|
||||
board
|
||||
.deal_flop(vec![
|
||||
card::Card::new(12, 2).unwrap(), // Ad
|
||||
card::Card::new(11, 3).unwrap(), // Ks
|
||||
card::Card::new(10, 0).unwrap(), // Qc
|
||||
])
|
||||
.unwrap();
|
||||
board.deal_turn(card::Card::new(9, 1).unwrap()).unwrap(); // Jh
|
||||
board.deal_river(card::Card::new(8, 2).unwrap()).unwrap(); // Td
|
||||
|
||||
let hole_cards1 = hole_cards::HoleCards::new(
|
||||
card::Card::new(12, 0).unwrap(), // As
|
||||
card::Card::new(12, 1).unwrap(), // Ah
|
||||
)
|
||||
.unwrap(); // Pocket aces
|
||||
|
||||
let hole_cards2 = hole_cards::HoleCards::new(
|
||||
card::Card::new(11, 0).unwrap(), // Kc
|
||||
card::Card::new(11, 1).unwrap(), // Kh
|
||||
)
|
||||
.unwrap(); // Pocket kings
|
||||
|
||||
let hand1 = hand::Hand::from_hole_cards_and_board(&hole_cards1, &board).unwrap();
|
||||
let hand2 = hand::Hand::from_hole_cards_and_board(&hole_cards2, &board).unwrap();
|
||||
|
||||
assert_eq!(hand1.len, 7);
|
||||
assert_eq!(hand2.len, 7);
|
||||
|
||||
// Both hands should contain all board cards plus their hole cards
|
||||
assert!(hand1.cards().contains(&card::Card::new(12, 0).unwrap())); // As in hand1
|
||||
assert!(hand1.cards().contains(&card::Card::new(12, 1).unwrap())); // Ah in hand1
|
||||
assert!(hand2.cards().contains(&card::Card::new(11, 0).unwrap())); // Kc in hand2
|
||||
assert!(hand2.cards().contains(&card::Card::new(11, 1).unwrap())); // Kh in hand2
|
||||
|
||||
// Both hands should contain all board cards
|
||||
for card in board.visible_cards() {
|
||||
assert!(hand1.cards().contains(card));
|
||||
assert!(hand2.cards().contains(card));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_integration_error_handling() {
|
||||
// Test error handling in integration scenarios
|
||||
|
||||
// Test duplicate cards between hole cards and board
|
||||
let hole_cards = hole_cards::HoleCards::new(
|
||||
card::Card::new(12, 0).unwrap(), // As
|
||||
card::Card::new(11, 1).unwrap(), // Kh
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut board = board::Board::new();
|
||||
board
|
||||
.deal_flop(vec![
|
||||
card::Card::new(12, 0).unwrap(), // Duplicate As
|
||||
card::Card::new(9, 3).unwrap(),
|
||||
card::Card::new(8, 0).unwrap(),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
// This should fail due to duplicate cards
|
||||
assert!(hand::Hand::from_hole_cards_and_board(&hole_cards, &board).is_err());
|
||||
|
||||
// Test serialization of invalid states (should still work)
|
||||
let invalid_hand = hand::Hand::new(vec![
|
||||
card::Card::new(12, 0).unwrap(),
|
||||
card::Card::new(12, 0).unwrap(), // Duplicate
|
||||
]);
|
||||
assert!(invalid_hand.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_integration_data_consistency() {
|
||||
// Test that data remains consistent across operations
|
||||
|
||||
// Create a complex scenario and verify all data stays consistent
|
||||
let hole_cards = hole_cards::HoleCards::new(
|
||||
card::Card::new(12, 0).unwrap(),
|
||||
card::Card::new(11, 1).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut board = board::Board::new();
|
||||
let flop_cards = vec![
|
||||
card::Card::new(10, 2).unwrap(),
|
||||
card::Card::new(9, 3).unwrap(),
|
||||
card::Card::new(8, 0).unwrap(),
|
||||
];
|
||||
board.deal_flop(flop_cards.clone()).unwrap();
|
||||
|
||||
let hand = hand::Hand::from_hole_cards_and_board(&hole_cards, &board).unwrap();
|
||||
|
||||
// Verify hand contains exactly the expected cards
|
||||
let expected_cards = vec![
|
||||
card::Card::new(12, 0).unwrap(), // hole
|
||||
card::Card::new(11, 1).unwrap(), // hole
|
||||
card::Card::new(10, 2).unwrap(), // flop
|
||||
card::Card::new(9, 3).unwrap(), // flop
|
||||
card::Card::new(8, 0).unwrap(), // flop
|
||||
];
|
||||
|
||||
assert_eq!(hand.len, 5);
|
||||
for expected in expected_cards {
|
||||
assert!(hand.cards().contains(&expected));
|
||||
}
|
||||
|
||||
// Test serialization preserves data
|
||||
let json = serde_json::to_string(&hand).unwrap();
|
||||
let deserialized: hand::Hand = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(hand, deserialized);
|
||||
assert_eq!(hand.len, deserialized.len);
|
||||
for i in 0..hand.len {
|
||||
assert_eq!(hand.cards()[i], deserialized.cards()[i]);
|
||||
}
|
||||
}
|
||||
484
holdem_core/tests/property_tests.rs
Normal file
484
holdem_core/tests/property_tests.rs
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
//! Property-based tests for poker data structures
|
||||
//!
|
||||
//! These tests verify fundamental properties of poker structures like cards,
|
||||
//! hands, and decks through comprehensive testing of edge cases and invariants.
|
||||
|
||||
use holdem_core::{card::Card, deck::Deck, hand::Hand};
|
||||
use std::collections::HashSet;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[cfg(test)]
|
||||
mod card_properties {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_card_properties_comprehensive() {
|
||||
// Test all possible card combinations for key properties
|
||||
for rank in 0..=12 {
|
||||
for suit in 0..=3 {
|
||||
let card = Card::new(rank, suit).unwrap();
|
||||
|
||||
// Property: Valid cards are always within valid rank/suit bounds
|
||||
assert!(card.rank() <= 12);
|
||||
assert!(card.suit() <= 3);
|
||||
|
||||
// Property: Card creation is deterministic (same inputs = same card)
|
||||
let card2 = Card::new(rank, suit).unwrap();
|
||||
assert_eq!(card, card2);
|
||||
assert_eq!(card.rank(), rank);
|
||||
assert_eq!(card.suit(), suit);
|
||||
|
||||
// Property: String parsing round-trips correctly
|
||||
let string_repr = format!("{}", card);
|
||||
let parsed = Card::from_str(&string_repr).unwrap();
|
||||
assert_eq!(card, parsed);
|
||||
|
||||
// Property: Card serialization round-trips correctly
|
||||
let json = serde_json::to_string(&card).unwrap();
|
||||
let deserialized: Card = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(card, deserialized);
|
||||
|
||||
let toml = toml::to_string(&card).unwrap();
|
||||
let deserialized: Card = toml::from_str(&toml).unwrap();
|
||||
assert_eq!(card, deserialized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_card_ordering_properties() {
|
||||
// Test card ordering with various combinations
|
||||
let test_cases = vec![
|
||||
((12, 3), (11, 3)), // Ace > King, same suit
|
||||
((12, 3), (12, 0)), // Ace spades > Ace hearts (Spades=4 > Hearts=3)
|
||||
((12, 0), (12, 1)), // Ace hearts > Ace diamonds (Hearts=3 > Diamonds=2)
|
||||
((12, 1), (12, 2)), // Ace diamonds > Ace clubs (Diamonds=2 > Clubs=1)
|
||||
((5, 3), (5, 0)), // Five spades > Five hearts
|
||||
((5, 0), (5, 1)), // Five hearts > Five diamonds
|
||||
((5, 1), (5, 2)), // Five diamonds > Five clubs
|
||||
];
|
||||
|
||||
for ((rank1, suit1), (rank2, suit2)) in test_cases {
|
||||
let card1 = Card::new(rank1, suit1).unwrap();
|
||||
let card2 = Card::new(rank2, suit2).unwrap();
|
||||
|
||||
// Property: Cards are properly ordered (higher rank first, then higher suit)
|
||||
if rank1 != rank2 {
|
||||
if rank1 > rank2 {
|
||||
assert!(
|
||||
card1 > card2,
|
||||
"Card {:?} should be > Card {:?}",
|
||||
card1,
|
||||
card2
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
card1 < card2,
|
||||
"Card {:?} should be < Card {:?}",
|
||||
card1,
|
||||
card2
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If ranks are equal, higher suit order should be greater
|
||||
// Suit order: Spades(4) > Hearts(3) > Diamonds(2) > Clubs(1)
|
||||
let suit_order1 = match suit1 {
|
||||
3 => 4, // Spades
|
||||
0 => 3, // Hearts
|
||||
1 => 2, // Diamonds
|
||||
2 => 1, // Clubs
|
||||
_ => 0,
|
||||
};
|
||||
let suit_order2 = match suit2 {
|
||||
3 => 4, // Spades
|
||||
0 => 3, // Hearts
|
||||
1 => 2, // Diamonds
|
||||
2 => 1, // Clubs
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
if suit_order1 > suit_order2 {
|
||||
assert!(
|
||||
card1 > card2,
|
||||
"Card {:?} should be > Card {:?}",
|
||||
card1,
|
||||
card2
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
card1 < card2,
|
||||
"Card {:?} should be < Card {:?}",
|
||||
card1,
|
||||
card2
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_cards_unique_property() {
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
// Property: All 52 possible cards are unique
|
||||
for rank in 0..=12 {
|
||||
for suit in 0..=3 {
|
||||
let card = Card::new(rank, suit).unwrap();
|
||||
assert!(seen.insert(card), "Card {} was not unique", card);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(seen.len(), 52, "Should have exactly 52 unique cards");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod hand_properties {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hand_properties_comprehensive() {
|
||||
// Test hands of various sizes for key properties
|
||||
for size in 0..=7 {
|
||||
if size < 2 {
|
||||
continue; // Skip very small hands for some tests
|
||||
}
|
||||
|
||||
// Generate distinct cards for this hand size
|
||||
let mut cards = Vec::new();
|
||||
let mut card_set = HashSet::new();
|
||||
|
||||
for i in 0..size {
|
||||
let rank = (i % 13) as u8;
|
||||
let suit = (i % 4) as u8;
|
||||
let card = Card::new(rank, suit).unwrap();
|
||||
|
||||
// Ensure uniqueness
|
||||
if card_set.insert(card) {
|
||||
cards.push(card);
|
||||
}
|
||||
|
||||
if cards.len() == size {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if cards.len() == size {
|
||||
let hand = Hand::new(cards.clone()).unwrap();
|
||||
|
||||
// Property: Hands never contain duplicate cards
|
||||
let hand_cards = hand.cards();
|
||||
for i in 0..hand_cards.len() {
|
||||
for j in (i + 1)..hand_cards.len() {
|
||||
assert_ne!(hand_cards[i], hand_cards[j]);
|
||||
}
|
||||
}
|
||||
|
||||
// Property: Hands are always sorted correctly (rank descending)
|
||||
for i in 1..hand_cards.len() {
|
||||
assert!(hand_cards[i].rank() <= hand_cards[i - 1].rank());
|
||||
}
|
||||
|
||||
// Property: All cards in hand are valid cards
|
||||
for card in hand.iter() {
|
||||
assert!(card.rank() <= 12);
|
||||
assert!(card.suit() <= 3);
|
||||
}
|
||||
|
||||
// Property: Hand serialization round-trips correctly
|
||||
let json = serde_json::to_string(&hand).unwrap();
|
||||
let deserialized: Hand = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(hand, deserialized);
|
||||
|
||||
let toml = toml::to_string(&hand).unwrap();
|
||||
let deserialized: Hand = toml::from_str(&toml).unwrap();
|
||||
assert_eq!(hand, deserialized);
|
||||
|
||||
// Property: Hand length matches actual card count
|
||||
assert_eq!(hand.len, cards.len());
|
||||
assert_eq!(hand_cards.len(), cards.len());
|
||||
|
||||
// Property: Best five cards returns exactly 5 cards (if hand large enough)
|
||||
if hand.len >= 5 {
|
||||
let best_five = hand.best_five_cards();
|
||||
assert_eq!(best_five.len(), 5);
|
||||
|
||||
// Property: Best five cards are subset of hand cards
|
||||
for &card in &best_five {
|
||||
assert!(hand_cards.contains(&card));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hand_length_properties() {
|
||||
// Property: Hand length is always <= 7
|
||||
for size in 0..=10 {
|
||||
let mut cards = Vec::new();
|
||||
for i in 0..size {
|
||||
let rank = (i % 13) as u8;
|
||||
let suit = (i % 4) as u8;
|
||||
if let Ok(card) = Card::new(rank, suit) {
|
||||
cards.push(card);
|
||||
}
|
||||
}
|
||||
|
||||
if size <= 7 {
|
||||
let hand = Hand::new(cards);
|
||||
assert!(hand.is_ok(), "Hand of size {} should be valid", size);
|
||||
} else {
|
||||
let hand = Hand::new(cards);
|
||||
assert!(hand.is_err(), "Hand of size {} should be invalid", size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hand_display_properties() {
|
||||
// Test various hand sizes for display format consistency
|
||||
for size in 0..=7 {
|
||||
let mut cards = Vec::new();
|
||||
for i in 0..size {
|
||||
let rank = (i % 13) as u8;
|
||||
let suit = (i % 4) as u8;
|
||||
if let Ok(card) = Card::new(rank, suit) {
|
||||
cards.push(card);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(hand) = Hand::new(cards) {
|
||||
let display = format!("{}", hand);
|
||||
|
||||
// Property: Hand display format is consistent
|
||||
assert!(display.starts_with("Hand("));
|
||||
assert!(display.ends_with(")"));
|
||||
assert!(display.contains("cards:"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod deck_properties {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deck_properties_comprehensive() {
|
||||
let deck = Deck::new();
|
||||
|
||||
// Property: Fresh decks always contain exactly 52 cards
|
||||
assert_eq!(deck.remaining(), 52);
|
||||
assert!(!deck.is_empty());
|
||||
|
||||
// Property: All cards in deck are unique
|
||||
let cards = deck.cards();
|
||||
let mut seen = HashSet::new();
|
||||
for &card in cards {
|
||||
assert!(seen.insert(card), "Duplicate card in deck: {}", card);
|
||||
}
|
||||
assert_eq!(seen.len(), 52);
|
||||
|
||||
// Property: All cards in deck are valid cards
|
||||
for &card in cards {
|
||||
assert!(card.rank() <= 12);
|
||||
assert!(card.suit() <= 3);
|
||||
}
|
||||
|
||||
// Property: Deck serialization round-trips correctly
|
||||
let json = serde_json::to_string(&deck).unwrap();
|
||||
let deserialized: Deck = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(deck.cards(), deserialized.cards());
|
||||
|
||||
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_dealing_properties() {
|
||||
let mut deck = Deck::new();
|
||||
let initial_remaining = deck.remaining();
|
||||
|
||||
// Property: Dealing cards reduces remaining count
|
||||
if initial_remaining > 0 {
|
||||
let dealt = deck.deal(1);
|
||||
assert_eq!(dealt.len(), 1);
|
||||
assert_eq!(deck.remaining(), initial_remaining - 1);
|
||||
}
|
||||
|
||||
// Property: Dealing more cards than available returns empty
|
||||
let mut empty_deck = Deck::new();
|
||||
while empty_deck.remaining() > 0 {
|
||||
empty_deck.deal(1);
|
||||
}
|
||||
|
||||
assert_eq!(empty_deck.remaining(), 0);
|
||||
let dealt_none = empty_deck.deal(1);
|
||||
assert!(dealt_none.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod integration_properties {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hand_from_hole_cards_and_board_properties() {
|
||||
// Test various combinations of hole cards and board cards
|
||||
for num_hole in 2..=2 {
|
||||
for num_board in 0..=5 {
|
||||
let mut hole_cards = Vec::new();
|
||||
let mut board_cards = Vec::new();
|
||||
let mut all_cards_set = HashSet::new();
|
||||
|
||||
// Generate unique hole cards
|
||||
for i in 0..num_hole {
|
||||
let mut attempts = 0;
|
||||
loop {
|
||||
let rank = (i % 13) as u8;
|
||||
let suit = (i % 4) as u8;
|
||||
let card = Card::new(rank, suit).unwrap();
|
||||
if all_cards_set.insert(card) {
|
||||
hole_cards.push(card);
|
||||
break;
|
||||
}
|
||||
attempts += 1;
|
||||
if attempts > 50 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate unique board cards
|
||||
for i in 0..num_board {
|
||||
let mut attempts = 0;
|
||||
loop {
|
||||
let rank = ((i + 2) % 13) as u8;
|
||||
let suit = ((i + 1) % 4) as u8;
|
||||
let card = Card::new(rank, suit).unwrap();
|
||||
if all_cards_set.insert(card) {
|
||||
board_cards.push(card);
|
||||
break;
|
||||
}
|
||||
attempts += 1;
|
||||
if attempts > 50 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hole_cards.len() == num_hole && board_cards.len() == num_board {
|
||||
use holdem_core::{board::Board, hole_cards::HoleCards};
|
||||
|
||||
let hole_cards_struct = HoleCards::new(hole_cards[0], hole_cards[1]).unwrap();
|
||||
let mut board = Board::new();
|
||||
|
||||
if board_cards.len() >= 3 {
|
||||
// For testing purposes, just add the first 3 cards as flop
|
||||
board
|
||||
.deal_flop(board_cards.iter().take(3).cloned().collect())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Property: Hand creation from hole cards and board never exceeds 7 cards
|
||||
let hand = Hand::from_hole_cards_and_board(&hole_cards_struct, &board);
|
||||
assert!(hand.is_ok());
|
||||
|
||||
let hand = hand.unwrap();
|
||||
assert!(hand.len <= 7);
|
||||
|
||||
// The hand should contain hole cards + any successfully added board cards
|
||||
let expected_min_len = num_hole; // At least hole cards
|
||||
let expected_max_len = num_hole + num_board; // At most hole cards + all board cards
|
||||
assert!(hand.len >= expected_min_len);
|
||||
assert!(hand.len <= expected_max_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hand_notation_parsing_properties() {
|
||||
// Test hand notation parsing with various valid combinations
|
||||
for size in 2..=7 {
|
||||
let mut cards = Vec::new();
|
||||
let mut card_set = HashSet::new();
|
||||
|
||||
for i in 0..size {
|
||||
let mut attempts = 0;
|
||||
loop {
|
||||
let rank = (i % 13) as u8;
|
||||
let suit = (i % 4) as u8;
|
||||
let card = Card::new(rank, suit).unwrap();
|
||||
if card_set.insert(card) {
|
||||
cards.push(card);
|
||||
break;
|
||||
}
|
||||
attempts += 1;
|
||||
if attempts > 50 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cards.len() == size {
|
||||
let mut notation_parts = Vec::new();
|
||||
for card in &cards {
|
||||
notation_parts.push(format!("{}", card));
|
||||
}
|
||||
let notation = notation_parts.join(" ");
|
||||
|
||||
// Property: Hand notation parsing works for valid hands
|
||||
let parsed = Hand::from_notation(¬ation);
|
||||
if size <= 7 {
|
||||
assert!(parsed.is_ok(), "Failed to parse notation: {}", notation);
|
||||
let parsed_hand = parsed.unwrap();
|
||||
assert_eq!(parsed_hand.len, cards.len());
|
||||
} else {
|
||||
assert!(
|
||||
parsed.is_err(),
|
||||
"Should fail to parse hand with {} cards",
|
||||
size
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hand_strength_properties() {
|
||||
// Test that hand strength is deterministic
|
||||
for size in 5..=7 {
|
||||
let mut cards = Vec::new();
|
||||
let mut card_set = HashSet::new();
|
||||
|
||||
for i in 0..size {
|
||||
let mut attempts = 0;
|
||||
loop {
|
||||
let rank = (i % 13) as u8;
|
||||
let suit = (i % 4) as u8;
|
||||
let card = Card::new(rank, suit).unwrap();
|
||||
if card_set.insert(card) {
|
||||
cards.push(card);
|
||||
break;
|
||||
}
|
||||
attempts += 1;
|
||||
if attempts > 50 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cards.len() == size {
|
||||
// Property: Hand strength is deterministic (same hand = same strength)
|
||||
let hand1 = Hand::new(cards.clone()).unwrap();
|
||||
let hand2 = Hand::new(cards).unwrap();
|
||||
|
||||
// For now, strength is always 0 (placeholder), but should be consistent
|
||||
assert_eq!(hand1.strength(), hand2.strength());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue