chironaut 0.3.5

A poker game library for Texas Hold'em and other poker variants
Documentation
use std::cmp::Ordering;
use std::collections::HashMap;

use crate::card::{Card, Rank};

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum HandRank {
    HighCard,
    Pair,
    TwoPair,
    ThreeOfAKind,
    Straight,
    Flush,
    FullHouse,
    FourOfAKind,
    StraightFlush,
    RoyalFlush,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HandEvaluation {
    pub rank: HandRank,
    pub hand_rank_value: u32,
    pub kickers: Vec<Rank>,
}

impl PartialOrd for HandEvaluation {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for HandEvaluation {
    fn cmp(&self, other: &Self) -> Ordering {
        match self.rank.cmp(&other.rank) {
            Ordering::Equal => {
                match self.hand_rank_value.cmp(&other.hand_rank_value) {
                    Ordering::Equal => {
                        // Compare kickers in order
                        for (s, o) in self.kickers.iter().zip(other.kickers.iter()) {
                            let cmp = s.cmp(o);
                            if cmp != Ordering::Equal {
                                return cmp;
                            }
                        }
                        Ordering::Equal
                    }
                    ordering => ordering,
                }
            }
            ordering => ordering,
        }
    }
}

pub fn evaluate_hand(cards: &[Card]) -> HandEvaluation {
    if cards.len() < 5 {
        return HandEvaluation {
            rank: HandRank::HighCard,
            hand_rank_value: 0,
            kickers: cards.iter().map(|c| c.rank).collect(),
        };
    }

    // Check for flush
    let is_flush = cards.iter().all(|c| c.suit == cards[0].suit);
    
    // Group by rank
    let mut rank_counts: HashMap<Rank, usize> = HashMap::new();
    for card in cards {
        *rank_counts.entry(card.rank).or_insert(0) += 1;
    }
    
    // Sort ranks by count, then by rank value for tie-breaking
    let mut rank_groups: Vec<(Rank, usize)> = rank_counts.into_iter().collect();
    rank_groups.sort_by(|(rank_a, count_a), (rank_b, count_b)| {
        count_b.cmp(count_a).then_with(|| rank_b.cmp(rank_a))
    });
    
    // Check for straight
    let mut ranks: Vec<Rank> = cards.iter().map(|c| c.rank).collect();
    ranks.sort_by(|a, b| b.cmp(a)); // Sort descending
    ranks.dedup(); // Remove duplicates
    
    let is_straight = check_straight(&ranks);
    
    // Check for special case: A-5 straight
    let is_wheel = ranks.contains(&Rank::Ace) && 
                  ranks.contains(&Rank::Five) && 
                  ranks.contains(&Rank::Four) && 
                  ranks.contains(&Rank::Three) && 
                  ranks.contains(&Rank::Two);
    
    // Determine hand rank
    let (hand_rank, hand_rank_value, kickers) = if is_flush && is_straight {
        // Check for royal flush (A-K-Q-J-T of same suit)
        if ranks[0] == Rank::Ace && ranks[1] == Rank::King {
            (HandRank::RoyalFlush, ranks[0].value() as u32, Vec::new())
        } else {
            (HandRank::StraightFlush, ranks[0].value() as u32, Vec::new())
        }
    } else if rank_groups[0].1 == 4 {
        // Four of a kind
        let quad_rank = rank_groups[0].0;
        let kicker = if rank_groups.len() > 1 { vec![rank_groups[1].0] } else { Vec::new() };
        (HandRank::FourOfAKind, quad_rank.value() as u32, kicker)
    } else if rank_groups[0].1 == 3 && rank_groups.get(1).map_or(false, |&(_, count)| count == 2) {
        // Full house
        let trip_rank = rank_groups[0].0;
        let pair_rank = rank_groups[1].0;
        (HandRank::FullHouse, (trip_rank.value() as u32) * 100 + pair_rank.value() as u32, Vec::new())
    } else if is_flush {
        // Flush
        let kickers = ranks.iter().take(5).cloned().collect();
        (HandRank::Flush, ranks[0].value() as u32, kickers)
    } else if is_straight || is_wheel {
        // Straight
        let high_card = if is_wheel { Rank::Five } else { ranks[0] };
        (HandRank::Straight, high_card.value() as u32, Vec::new())
    } else if rank_groups[0].1 == 3 {
        // Three of a kind
        let trip_rank = rank_groups[0].0;
        let kickers: Vec<Rank> = rank_groups.iter()
            .skip(1)
            .take(2)
            .map(|&(rank, _)| rank)
            .collect();
        (HandRank::ThreeOfAKind, trip_rank.value() as u32, kickers)
    } else if rank_groups[0].1 == 2 && rank_groups.get(1).map_or(false, |&(_, count)| count == 2) {
        // Two pair
        let high_pair = rank_groups[0].0;
        let low_pair = rank_groups[1].0;
        let kicker = if rank_groups.len() > 2 { vec![rank_groups[2].0] } else { Vec::new() };
        (HandRank::TwoPair, (high_pair.value() as u32) * 100 + low_pair.value() as u32, kicker)
    } else if rank_groups[0].1 == 2 {
        // One pair
        let pair_rank = rank_groups[0].0;
        let kickers: Vec<Rank> = rank_groups.iter()
            .skip(1)
            .take(3)
            .map(|&(rank, _)| rank)
            .collect();
        (HandRank::Pair, pair_rank.value() as u32, kickers)
    } else {
        // High card
        let kickers = ranks.iter().take(5).cloned().collect();
        (HandRank::HighCard, ranks[0].value() as u32, kickers)
    };
    
    HandEvaluation {
        rank: hand_rank,
        hand_rank_value,
        kickers,
    }
}

fn check_straight(ranks: &[Rank]) -> bool {
    if ranks.len() < 5 {
        return false;
    }
    
    // Find the longest consecutive sequence
    let mut consecutive = 1;
    let mut max_consecutive = 1;
    
    for i in 1..ranks.len() {
        if ranks[i].value() == ranks[i-1].value() - 1 {
            consecutive += 1;
            max_consecutive = max_consecutive.max(consecutive);
        } else if ranks[i].value() != ranks[i-1].value() {
            consecutive = 1;
        }
    }
    
    // Special case for A-5-4-3-2 (wheel)
    if ranks.contains(&Rank::Ace) && 
       ranks.contains(&Rank::Five) && 
       ranks.contains(&Rank::Four) && 
       ranks.contains(&Rank::Three) && 
       ranks.contains(&Rank::Two) {
        return true;
    }
    
    max_consecutive >= 5
}

pub fn find_best_hand(hole_cards: &[Card], community_cards: &[Card]) -> (Vec<Card>, HandEvaluation) {
    let all_cards: Vec<Card> = hole_cards.iter().chain(community_cards.iter()).cloned().collect();
    
    // For NLHE, we need to find the best 5-card hand out of 7 cards
    let mut best_hand = Vec::new();
    let mut best_eval = HandEvaluation {
        rank: HandRank::HighCard,
        hand_rank_value: 0,
        kickers: Vec::new(),
    };
    
    // This is a simple but inefficient approach for small n
    // We're finding all 5-card combinations from the 7 available cards
    for i in 0..all_cards.len() {
        for j in i+1..all_cards.len() {
            for k in j+1..all_cards.len() {
                for l in k+1..all_cards.len() {
                    for m in l+1..all_cards.len() {
                        let hand = vec![
                            all_cards[i], all_cards[j], all_cards[k], 
                            all_cards[l], all_cards[m]
                        ];
                        let eval = evaluate_hand(&hand);
                        
                        if eval > best_eval {
                            best_hand = hand;
                            best_eval = eval;
                        }
                    }
                }
            }
        }
    }
    
    (best_hand, best_eval)
}

/// Find the best hand for Omaha variants
/// In Omaha, player must use exactly 2 hole cards and 3 community cards
pub fn find_best_omaha_hand(hole_cards: &[Card], community_cards: &[Card]) -> (Vec<Card>, HandEvaluation) {
    let mut best_hand = Vec::new();
    let mut best_eval = HandEvaluation {
        rank: HandRank::HighCard,
        hand_rank_value: 0,
        kickers: Vec::new(),
    };
    
    // Generate all possible combinations of 2 cards from hole cards
    for i in 0..hole_cards.len() {
        for j in i+1..hole_cards.len() {
            let hole_combo = vec![hole_cards[i], hole_cards[j]];
            
            // Generate all possible combinations of 3 cards from community cards
            for a in 0..community_cards.len() {
                for b in a+1..community_cards.len() {
                    for c in b+1..community_cards.len() {
                        let comm_combo = vec![
                            community_cards[a], 
                            community_cards[b], 
                            community_cards[c]
                        ];
                        
                        // Create a 5-card hand with exactly 2 hole cards and 3 community cards
                        let hand = vec![
                            hole_combo[0], hole_combo[1],
                            comm_combo[0], comm_combo[1], comm_combo[2]
                        ];
                        
                        let eval = evaluate_hand(&hand);
                        
                        if eval > best_eval {
                            best_hand = hand;
                            best_eval = eval;
                        }
                    }
                }
            }
        }
    }
    
    (best_hand, best_eval)
}