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 {
#[arg(required = true, num_args = 2..)]
hands: Vec<String>,
#[arg(short = 'b', long)]
board: Option<String>,
#[arg(long = "detailed")]
detailed: bool,
}
pub fn run(args: OutsArgs) -> Result<(), OutsError> {
println!("=== Texas Hold'em Outs Calculator ===\n");
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()
};
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);
}
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!();
let calc = OutsCalculator::new(board, hands);
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!();
let player_outs = calc.calculate_outs();
let results = player_outs.outcomes();
let exclusive_outs = player_outs.get_outs();
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());
let outs_count = exclusive_outs[idx].count();
if outs_count > 0 {
println!(" Exclusive outs: {} cards", outs_count);
if args.detailed {
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();
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!();
}
println!("Summary:");
println!("--------");
println!(
"Total possible outcomes analyzed: {}",
results[0].total_combinations
);
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(())
}