cribbage-core 0.1.5

A (work-in-progress) library providing the core functionality of the game Cribbage.
Documentation
mod crib_cards;
mod crib_part;
mod dealt_cards;
mod kept_cards;

use crate::card::{Card, Rank};
use crate::deck::Deck;
use crate::CribbageCoreError;

pub use self::crib_cards::CribCards;
pub use self::crib_part::FourPlayerCribPart;
pub use self::crib_part::ThreePlayerCribPart;
pub use self::crib_part::TwoPlayerCribPart;
pub use self::dealt_cards::FourPlayerDeal;
pub use self::dealt_cards::ThreePlayerDeal;
pub use self::dealt_cards::TwoPlayerDeal;
pub use self::kept_cards::KeptCards;

pub fn deal_four_player_hand(deck: &mut Deck) -> Result<FourPlayerDeal, CribbageCoreError> {
    let cards = deck.draw_n(5)?;
    Ok(FourPlayerDeal::new([
        cards[0], cards[1], cards[2], cards[3], cards[4],
    ]))
}

pub fn deal_three_player_hand(deck: &mut Deck) -> Result<ThreePlayerDeal, CribbageCoreError> {
    let cards = deck.draw_n(5)?;
    Ok(ThreePlayerDeal::new([
        cards[0], cards[1], cards[2], cards[3], cards[4],
    ]))
}

pub fn deal_two_player_hand(deck: &mut Deck) -> Result<TwoPlayerDeal, CribbageCoreError> {
    let cards = deck.draw_n(6)?;
    Ok(TwoPlayerDeal::new([
        cards[0], cards[1], cards[2], cards[3], cards[4], cards[5],
    ]))
}

#[derive(Debug)]
pub struct Hand {
    cards: [Card; 4],
    cut: Card,
    is_crib: bool,
    score: Option<u8>,
}

impl Hand {
    pub fn new(cards: [Card; 4], cut: Card, is_crib: bool) -> Hand {
        Hand {
            cards,
            cut,
            is_crib,
            score: None,
        }
    }

    pub fn cards(&self) -> &[Card] {
        &self.cards
    }

    pub fn score(&mut self) -> u8 {
        if let Some(score) = self.score {
            return score;
        }

        let mut score = 0u8;
        score += self.score_fifteens();
        score += self.score_pairs();
        score += self.score_runs();
        score += self.score_flush();
        score += self.score_nobs();
        self.score = Some(score);
        score
    }

    fn score_fifteens(&self) -> u8 {
        let mut points = 0u8;
        let subsets = vec![
            self.sets_of_two(),
            self.sets_of_three(),
            self.sets_of_four(),
            self.sets_of_five(),
        ];

        for subset in subsets {
            for set in subset {
                let mut sum = 0u8;
                for card in &set {
                    sum += card.rank().value();
                }

                if sum == 15 {
                    points += 2;
                }
            }
        }

        points
    }

    fn score_flush(&self) -> u8 {
        let mut points = 0;
        let suit = self.cards[0].suit();
        if self.cards[1..4].iter().all(|c| c.suit() == suit) {
            points += 4;

            if self.cut.suit() == suit {
                points += 1;
            } else if self.is_crib {
                points = 0;
            }
        }

        points
    }

    fn score_nobs(&self) -> u8 {
        for card in &self.cards {
            if card.rank() == Rank::Jack && card.suit() == self.cut.suit() {
                return 1;
            }
        }

        0
    }

    fn score_pairs(&self) -> u8 {
        let mut points = 0u8;
        let sets_of_two = self.sets_of_two();
        for set in sets_of_two {
            if set[0].rank() == set[1].rank() {
                points += 2u8;
            }
        }

        points
    }

    fn score_runs(&self) -> u8 {
        let mut points = 0u8;
        let mut temp = 0u8;

        for mut set in self.sets_of_five() {
            set.sort();
            temp += Hand::score_run(&set);
        }

        points += temp;
        if points != 0u8 {
            return points;
        }

        for mut set in self.sets_of_four() {
            set.sort();
            temp += Hand::score_run(&set);
        }

        points += temp;
        if points != 0u8 {
            return points;
        }

        for mut set in self.sets_of_three() {
            set.sort();
            temp += Hand::score_run(&set);
        }

        points += temp;
        if points != 0u8 {
            return points;
        }

        points
    }

    fn score_run(cards: &[Card]) -> u8 {
        if cards.len() < 3usize {
            return 0u8;
        }

        let mut points = 1u8;
        for i in 1..cards.len() {
            if cards[i].rank().ordinal() == (cards[i - 1].rank().ordinal() + 1) {
                points += 1u8;
            } else {
                points = 0u8;
                break;
            }
        }

        points
    }

    fn sets_of_two(&self) -> Vec<Vec<Card>> {
        vec![
            vec![self.cards[0], self.cards[1]],
            vec![self.cards[0], self.cards[2]],
            vec![self.cards[0], self.cards[3]],
            vec![self.cards[0], self.cut],
            vec![self.cards[1], self.cards[2]],
            vec![self.cards[1], self.cards[3]],
            vec![self.cards[1], self.cut],
            vec![self.cards[2], self.cards[3]],
            vec![self.cards[2], self.cut],
            vec![self.cards[3], self.cut],
        ]
    }

    fn sets_of_three(&self) -> Vec<Vec<Card>> {
        vec![
            vec![self.cards[0], self.cards[1], self.cards[2]],
            vec![self.cards[0], self.cards[1], self.cards[3]],
            vec![self.cards[0], self.cards[2], self.cards[3]],
            vec![self.cards[1], self.cards[2], self.cards[3]],
            vec![self.cards[0], self.cards[1], self.cut],
            vec![self.cards[0], self.cards[2], self.cut],
            vec![self.cards[1], self.cards[2], self.cut],
            vec![self.cards[0], self.cards[3], self.cut],
            vec![self.cards[1], self.cards[3], self.cut],
            vec![self.cards[2], self.cards[3], self.cut],
        ]
    }

    fn sets_of_four(&self) -> Vec<Vec<Card>> {
        vec![
            vec![self.cards[0], self.cards[1], self.cards[2], self.cards[3]],
            vec![self.cards[0], self.cards[1], self.cards[2], self.cut],
            vec![self.cards[0], self.cards[1], self.cards[3], self.cut],
            vec![self.cards[0], self.cards[2], self.cards[3], self.cut],
            vec![self.cards[1], self.cards[2], self.cards[3], self.cut],
        ]
    }

    fn sets_of_five(&self) -> Vec<Vec<Card>> {
        vec![vec![
            self.cards[0],
            self.cards[1],
            self.cards[2],
            self.cards[3],
            self.cut,
        ]]
    }
}

#[cfg(test)]
mod tests {
    use crate::card::Card;
    use crate::hand::Hand;
    use std::str::FromStr;

    #[allow(dead_code)]
    #[cfg_attr(feature = "extensive-tests", test)]
    fn test_hands() {
        use std::env;
        use std::fs::File;
        use std::io::{BufRead, BufReader};
        use std::path::PathBuf;

        let key = "CRIBBAGE_CORE_RESOURCES_DIRECTORY";
        let resources = match env::var(key) {
            Ok(val) => val,
            Err(_) => panic!("Environment variable {} not set", key),
        };

        let mut path = PathBuf::new();
        path.push(resources);
        path.push("cribbage_hand_scores");

        let filenames = [
            "A.txt", "2.txt", "3.txt", "4.txt", "5.txt", "6.txt", "7.txt", "8.txt", "9.txt",
            "T.txt", "J.txt", "Q.txt", "K.txt",
        ];

        for filename in &filenames {
            path.push(filename);
            println!("Processing: {:?}", path);
            let reader = BufReader::new(File::open(&path).unwrap());
            for (line_number, line) in reader.lines().enumerate() {
                let line = line.unwrap().to_string();
                let tokens: Vec<&str> = line.split(' ').collect();
                if tokens.len() != 7 {
                    panic!("Line {} in {:?} is malformed", line_number + 1, path);
                }

                let cut = Card::from_str(tokens[0]).unwrap();
                let card1 = Card::from_str(tokens[1]).unwrap();
                let card2 = Card::from_str(tokens[2]).unwrap();
                let card3 = Card::from_str(tokens[3]).unwrap();
                let card4 = Card::from_str(tokens[4]).unwrap();
                let expected_non_crib_hand_score: u8 = tokens[5].parse().unwrap();
                let expected_crib_hand_score: u8 = tokens[6].parse().unwrap();

                let mut hand = Hand::new([card1, card2, card3, card4], cut, false);
                let non_crib_hand_score = hand.score();
                if non_crib_hand_score != expected_non_crib_hand_score {
                    panic!(
                        "Line {} in {:?}: {:?} scored {} points as a non-crib hand,\
                         but should have scored {} points",
                        line_number + 1,
                        path,
                        hand,
                        non_crib_hand_score,
                        expected_non_crib_hand_score
                    );
                }

                let mut crib_hand = Hand::new([card1, card2, card3, card4], cut, true);
                let crib_hand_score = crib_hand.score();
                if crib_hand_score != expected_crib_hand_score {
                    panic!(
                        "Line {} in {:?}: {:?} scored {} points as a crib hand,\
                         but should have scored {} points",
                        line_number + 1,
                        path,
                        hand,
                        non_crib_hand_score,
                        expected_non_crib_hand_score
                    );
                }
            }

            path.pop();
        }
    }
}