espada 0.3.1

Texas Hold'em poker odds evaluator
Documentation
use super::showdown::Showdown;
use crate::card::{Card, RankRange, SuitRange};
use crate::hand_range::{CardPair, HandRange};
use fxhash::FxBuildHasher;
use std::collections::HashSet;

pub struct FlopExhaustiveEvaluator {
    board: [Option<Card>; 5],
    players: Vec<HandRange>,
    turn_from: u8,
    river_from: u8,
    turn_to: u8,
    river_to: u8,
}

impl FlopExhaustiveEvaluator {
    pub fn new(board: &[Option<Card>; 5], players: &Vec<HandRange>) -> Self {
        Self {
            board: board.clone(),
            players: players.clone(),
            turn_from: 0,
            river_from: 1,
            turn_to: 48,
            river_to: 49,
        }
    }

    pub fn scope(&mut self, turn_from: u8, river_from: u8, turn_to: u8, river_to: u8) {
        debug_assert!(turn_from <= turn_to);
        debug_assert!(turn_from < river_from);
        debug_assert!(turn_to < river_to);

        self.turn_from = turn_from;
        self.river_from = river_from;
        self.turn_to = turn_to;
        self.river_to = river_to;
    }
}

impl<'p> IntoIterator for FlopExhaustiveEvaluator {
    type Item = Showdown;
    type IntoIter = FlopExhaustiveEvaluatorIterator;

    fn into_iter(self) -> Self::IntoIter {
        FlopExhaustiveEvaluatorIterator::new(&self)
    }
}

pub struct FlopExhaustiveEvaluatorIterator {
    turn_to: u8,
    river_to: u8,
    player_entries: Vec<Vec<(CardPair, f32)>>,
    current_deck: [Card; 49],
    current_board: [Option<Card>; 5],
    current_used_cards: HashSet<Card, FxBuildHasher>,
    current_turn_index: u8,
    current_river_index: u8,
    current_player_indexes: Vec<u8>,
}

impl FlopExhaustiveEvaluatorIterator {
    fn new(evaluator: &FlopExhaustiveEvaluator) -> Self {
        let mut player_entries = vec![vec![]; evaluator.players.len()];

        for (player_index, player) in evaluator.players.iter().enumerate() {
            for (card_pair, probability) in player.card_pairs() {
                player_entries[player_index].push((*card_pair, *probability));
            }
        }

        let mut current_deck = Vec::with_capacity(52);

        for rank in RankRange::all() {
            for suit in SuitRange::all() {
                let card = Card::new(rank, suit);

                if evaluator
                    .board
                    .iter()
                    .filter(|c| c.is_some())
                    .all(|c| (*c).unwrap() != card)
                {
                    current_deck.push(card);
                }
            }
        }

        Self {
            turn_to: evaluator.turn_to,
            river_to: evaluator.river_to,
            player_entries,
            current_deck: current_deck.try_into().unwrap(),
            current_board: evaluator.board.clone(),
            current_used_cards: HashSet::with_capacity_and_hasher(
                2 + evaluator.players.len() * 2,
                FxBuildHasher::default(),
            ),
            current_turn_index: evaluator.turn_from,
            current_river_index: evaluator.river_from,
            current_player_indexes: vec![0; evaluator.players.len()],
        }
    }
}

impl Iterator for FlopExhaustiveEvaluatorIterator {
    type Item = Showdown;

    fn next(&mut self) -> Option<Showdown> {
        if self.current_turn_index >= self.turn_to && self.current_river_index >= self.river_to {
            return None;
        }

        let turn = self.current_deck[self.current_turn_index as usize];
        let river = self.current_deck[self.current_river_index as usize];

        self.current_board[3] = Some(turn);
        self.current_board[4] = Some(river);

        self.current_used_cards.insert(turn);
        self.current_used_cards.insert(river);

        let mut player_card_pairs = vec![];
        let mut probability: f32 = 1.0;

        let mut is_materialized = true;

        for (player_index, player_entry) in self.player_entries.iter().enumerate() {
            let entry = player_entry[self.current_player_indexes[player_index] as usize];

            if self.current_used_cards.contains(&entry.0[0])
                || self.current_used_cards.contains(&entry.0[1])
            {
                is_materialized = false;
            }

            player_card_pairs.push(entry.0);
            probability *= entry.1;
        }

        let mut showdown = None;

        if is_materialized {
            showdown = Showdown::new(
                player_card_pairs,
                [
                    self.current_board[0].unwrap(),
                    self.current_board[1].unwrap(),
                    self.current_board[2].unwrap(),
                    self.current_board[3].unwrap(),
                    self.current_board[4].unwrap(),
                ],
                probability,
            );
        }

        let mut player_index_to_increment = None;

        for i in 0..self.current_player_indexes.len() {
            let ri = self.current_player_indexes.len() - i - 1;

            if self.current_player_indexes[ri] < self.player_entries[ri].len() as u8 - 1 {
                player_index_to_increment = Some(ri);

                break;
            }
        }

        self.current_board[3] = None;
        self.current_board[4] = None;

        self.current_used_cards.clear();

        if let Some(player_index_to_increment) = player_index_to_increment {
            self.current_player_indexes[player_index_to_increment] += 1;

            for i in (player_index_to_increment + 1)..self.current_player_indexes.len() {
                self.current_player_indexes[i] = 0;
            }

            return showdown.or_else(|| self.next());
        }

        if self.current_river_index < 48 {
            self.current_river_index += 1;
            self.current_player_indexes.fill(0);

            return showdown.or_else(|| self.next());
        }

        self.current_turn_index += 1;
        self.current_river_index = self.current_turn_index + 1;
        self.current_player_indexes.fill(0);

        showdown.or_else(|| self.next())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    mod iterator {
        use super::*;
        use crate::card::{Rank, Suit};
        use insta::*;
        use std::str::FromStr;

        #[test]
        fn it_iterates_scoped_from_0_1_to_2_25() {
            let board = [
                Some(Card::new(Rank::Deuce, Suit::Heart)),
                Some(Card::new(Rank::Deuce, Suit::Diamond)),
                Some(Card::new(Rank::Deuce, Suit::Club)),
                None,
                None,
            ];
            let players = vec![
                HandRange::from_str("4s3h:1").unwrap(),
                HandRange::from_str("4d3c:1").unwrap(),
            ];

            let mut evaluator = FlopExhaustiveEvaluator::new(&board, &players);
            evaluator.scope(0, 1, 2, 25);

            let result: Vec<Showdown> = evaluator.into_iter().collect();

            assert_eq!(result.len(), (48 - 4) + (47 - 4) + 22);
            assert_debug_snapshot!(result);
        }

        #[test]
        fn it_iterates_scoped_from_10_43_to_14_18() {
            let board = [
                Some(Card::new(Rank::Jack, Suit::Heart)),
                Some(Card::new(Rank::Nine, Suit::Diamond)),
                Some(Card::new(Rank::Trey, Suit::Club)),
                None,
                None,
            ];
            let players = vec![
                HandRange::from_str("As4h:1").unwrap(),
                HandRange::from_str("Td8c:1").unwrap(),
            ];

            let mut evaluator = FlopExhaustiveEvaluator::new(&board, &players);
            evaluator.scope(10, 43, 14, 18);

            let result: Vec<Showdown> = evaluator.into_iter().collect();

            assert_eq!(result.len(), 5 + (37 - 3) + (36 - 3) + (35 - 3) + 3);
            assert_debug_snapshot!(result);
        }

        #[test]
        fn it_iterates_scoped_from_32_48_to_47_49() {
            let board = [
                Some(Card::new(Rank::King, Suit::Heart)),
                Some(Card::new(Rank::King, Suit::Diamond)),
                Some(Card::new(Rank::King, Suit::Club)),
                None,
                None,
            ];
            let players = vec![
                HandRange::from_str("As2s:1").unwrap(),
                HandRange::from_str("JdJc:1").unwrap(),
            ];

            let mut evaluator = FlopExhaustiveEvaluator::new(&board, &players);
            evaluator.scope(32, 48, 47, 49);

            let result: Vec<Showdown> = evaluator.into_iter().collect();

            assert_eq!(
                result.len(),
                1 + (15 - 1)
                    + (14 - 1)
                    + (13 - 1)
                    + (12 - 1)
                    + (11 - 1)
                    + (10 - 1)
                    + (9 - 1)
                    + (8 - 1)
                    + (7 - 1)
                    + (6 - 1)
                    + (5 - 1)
                    + 3
                    + 2
                    + 1
            );
            assert_debug_snapshot!(result);
        }
    }
}