rs_poker 5.0.0

A library to help with any Rust code dealing with poker. This includes card values, suits, hands, hand ranks, 5 card hand strength calculation, 7 card hand strength calulcation, and monte carlo game simulation helpers.
Documentation
use clap::Parser;
use rs_poker::core::{CardBitSet, Hand, RSPokerError};
use rs_poker::holdem::OutsCalculator;

#[derive(Debug, thiserror::Error)]
pub enum OutsError {
    #[error(transparent)]
    Poker(#[from] RSPokerError),
    #[error("{0}")]
    InvalidInput(String),
}

#[derive(Parser, Debug)]
#[command(
    name = "outs",
    about = "Calculate outs and equity for Texas Hold'em hands",
    long_about = "Calculate win/tie probabilities and outs for Texas Hold'em hands.\n\
                  Provide player hands and optionally a board (flop/turn) to analyze all possible outcomes."
)]
pub struct OutsArgs {
    /// Player hands to analyze (e.g., "AcAd" "KsKh")
    #[arg(required = true, num_args = 2..)]
    hands: Vec<String>,

    /// Optional board cards (e.g., "AhKhQh" for flop, "AhKhQhTc" for turn)
    /// If not provided, calculates equity from preflop
    #[arg(short = 'b', long)]
    board: Option<String>,

    /// Show detailed winning hand types and out cards
    #[arg(long = "detailed")]
    detailed: bool,
}

pub fn run(args: OutsArgs) -> Result<(), OutsError> {
    println!("=== Texas Hold'em Outs Calculator ===\n");

    // Parse board (if provided)
    let board = if let Some(board_str) = &args.board {
        let hand = Hand::new_from_str(board_str).map_err(|e| {
            OutsError::InvalidInput(format!("Error parsing board '{}': {}", board_str, e))
        })?;
        let board_set: CardBitSet = hand.into();
        let board_count = board_set.count();
        if !matches!(board_count, 3..=5) {
            return Err(OutsError::InvalidInput(format!(
                "Board must have 3, 4, or 5 cards (flop/turn/river), got {}",
                board_count
            )));
        }
        println!("Board: {}", board_str);
        board_set
    } else {
        println!("Board: (empty - preflop)");
        CardBitSet::new()
    };

    // Parse player hands
    let mut hands = Vec::new();
    for (i, hand_str) in args.hands.iter().enumerate() {
        let hand = Hand::new_from_str(hand_str).map_err(|e| {
            OutsError::InvalidInput(format!("Error parsing hand '{}': {}", hand_str, e))
        })?;
        println!("Player {}: {}", i + 1, hand_str);
        hands.push(hand);
    }

    // Check for card conflicts between hands and board
    let mut all_cards = board;
    for hand in &hands {
        let hand_set: CardBitSet = (*hand).into();
        if !(all_cards & hand_set).is_empty() {
            return Err(OutsError::InvalidInput(
                "Duplicate cards detected between hands and/or board".into(),
            ));
        }
        all_cards |= hand_set;
    }

    println!();

    // Create calculator
    let calc = OutsCalculator::new(board, hands);

    // Calculate and display information
    let board_size = board.count();
    let cards_to_deal = 5 - board_size;

    match cards_to_deal {
        0 => println!("Board is complete. Analyzing final hand strengths..."),
        1 => println!(
            "Calculating all possible river cards ({} cards)...",
            cards_to_deal
        ),
        2 => println!(
            "Calculating all possible turn + river combinations ({} cards)...",
            cards_to_deal
        ),
        _ => {
            println!(
                "Calculating all possible board combinations ({} cards to deal)...",
                cards_to_deal
            );
            println!("(This will take a moment for preflop calculations...)");
        }
    }
    println!();

    // Calculate outs
    let player_outs = calc.calculate_outs();
    let results = player_outs.outcomes();

    // Get exclusive outs for each player
    let exclusive_outs = player_outs.get_outs();

    // Display results
    println!("Results:");
    println!("========\n");

    for (idx, result) in results.iter().enumerate() {
        println!("Player {} - {}:", idx + 1, args.hands[idx]);
        println!("  Wins:  {} ({:.2}%)", result.wins, result.win_percentage());
        println!("  Ties:  {} ({:.2}%)", result.ties, result.tie_percentage());

        // Display exclusive outs
        let outs_count = exclusive_outs[idx].count();
        if outs_count > 0 {
            println!("  Exclusive outs: {} cards", outs_count);
            if args.detailed {
                // Show the actual cards
                let outs_vec: Vec<_> = exclusive_outs[idx].into_iter().collect();
                let outs_str: Vec<String> = outs_vec.iter().map(|c| format!("{}", c)).collect();
                println!("    Cards: {}", outs_str.join(", "));
            }
        } else {
            println!("  Exclusive outs: None");
        }

        if args.detailed && !result.winning_boards.is_empty() {
            println!("\n  Winning hand types:");
            let grouped = result.count_wins_by_core_rank();
            let mut rank_counts: Vec<_> = grouped.iter().collect();

            // Sort by count (descending), then by rank (descending) as tiebreaker
            rank_counts.sort_by(|a, b| b.1.cmp(a.1).then_with(|| b.0.cmp(a.0)));

            for (rank, count) in rank_counts {
                println!("    {:?}: {} times", rank, count);
            }
        }
        println!();
    }

    // Summary
    println!("Summary:");
    println!("--------");
    println!(
        "Total possible outcomes analyzed: {}",
        results[0].total_combinations
    );

    // Find the favorite
    let favorite_idx = results
        .iter()
        .enumerate()
        .max_by(|(_, a), (_, b)| a.win_percentage().partial_cmp(&b.win_percentage()).unwrap())
        .map(|(idx, _)| idx)
        .unwrap();

    println!(
        "Favorite: Player {} ({}) with {:.2}% win rate",
        favorite_idx + 1,
        args.hands[favorite_idx],
        results[favorite_idx].win_percentage()
    );

    Ok(())
}