Created
May 17, 2023 20:59
-
-
Save graffhyrum/dec535ff3a595113b89dac1b9a42fa80 to your computer and use it in GitHub Desktop.
Rust Exercism Poker challenge
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// Given a list of poker hands, return a list of those hands which win. | |
| /// | |
| /// Note the type signature: this function should return _the same_ reference to | |
| /// the winning hand(s) as were passed in, not reconstructed strings which happen to be equal. | |
| pub fn winning_hands<'a>(hands: &[&'a str]) -> Vec<&'a str> { | |
| let mut winning_hands = Vec::new(); | |
| let mut winning_hand_rank = rank_hand(hands[0]); | |
| winning_hands.push(hands[0]); | |
| if hands.len() == 1 { | |
| return winning_hands; | |
| } | |
| for hand in hands.iter().skip(1) { | |
| let this_hand_rank = rank_hand(hand); | |
| if this_hand_rank > winning_hand_rank { | |
| winning_hand_rank = this_hand_rank; | |
| winning_hands.clear(); | |
| winning_hands.push(*hand); | |
| } else if this_hand_rank == winning_hand_rank { | |
| winning_hands.push(*hand); | |
| } | |
| } | |
| winning_hands | |
| } | |
| fn rank_hand(hand_str: &str) -> Hands { | |
| let hand = parse_hand(hand_str); | |
| let mut rank_counts = [0; 14]; | |
| let mut suit_counts = [0; 4]; | |
| for card in hand.cards.iter() { | |
| rank_counts[card.rank] += 1; | |
| suit_counts[card.suit] += 1; | |
| } | |
| let mut rank_count_counts = [0; 5]; | |
| for rank_count in rank_counts.iter() { | |
| rank_count_counts[*rank_count as usize] += 1; | |
| } | |
| let is_flush = is_flush(&suit_counts); | |
| let is_straight = is_straight(rank_counts); | |
| if is_flush && is_straight { | |
| parse_straight_flush(hand) | |
| } else if rank_count_counts[4] == 1 { | |
| parse_four_of_kind(rank_counts) | |
| } else if rank_count_counts[3] == 1 && rank_count_counts[2] == 1 { | |
| parse_full_house(rank_counts) | |
| } else if is_flush { | |
| parse_flush(hand) | |
| } else if is_straight { | |
| parse_straight(hand) | |
| } else if rank_count_counts[3] == 1 { | |
| parse_three_of_kind(rank_counts) | |
| } else if rank_count_counts[2] == 2 { | |
| parse_two_pair(rank_counts) | |
| } else if rank_count_counts[2] == 1 { | |
| parse_pair(rank_counts) | |
| } else { | |
| Hands::HighCard([ | |
| hand.cards[4].rank, | |
| hand.cards[3].rank, | |
| hand.cards[2].rank, | |
| hand.cards[1].rank, | |
| hand.cards[0].rank, | |
| ]) | |
| } | |
| } | |
| fn parse_straight_flush(hand: Hand) -> Hands { | |
| // even though an ace is usually high, a 5-high straight flush is the lowest-scoring straight flush | |
| if hand.cards[4].rank == 13 { | |
| Hands::StraightFlush(4) | |
| } else { | |
| Hands::StraightFlush(hand.cards[4].rank) | |
| } | |
| } | |
| fn parse_straight(hand: Hand) -> Hands { | |
| // even though an ace is usually high, a 5-high straight is the lowest-scoring straight | |
| if hand.cards[4].rank == 13 { | |
| Hands::Straight(4) | |
| } else { | |
| Hands::Straight(hand.cards[4].rank) | |
| } | |
| } | |
| fn parse_flush(hand: Hand) -> Hands { | |
| Hands::Flush([ | |
| hand.cards[4].rank, | |
| hand.cards[3].rank, | |
| hand.cards[2].rank, | |
| hand.cards[1].rank, | |
| hand.cards[0].rank, | |
| ]) | |
| } | |
| fn is_straight(rank_counts: [i32; 14]) -> bool { | |
| //special ace check, then normal check | |
| let ace_straight = rank_counts[1] == 1 | |
| && rank_counts[2] == 1 | |
| && rank_counts[3] == 1 | |
| && rank_counts[4] == 1 | |
| && rank_counts[13] == 1; | |
| if ace_straight { | |
| return true; | |
| } | |
| rank_counts.windows(5).enumerate().any(|(_, window)| { | |
| window.iter().all(|&count| count == 1) | |
| }) | |
| } | |
| fn is_flush(suit_counts: &[i32; 4]) -> bool { | |
| suit_counts.iter().any(|&count| count == 5) | |
| } | |
| fn parse_four_of_kind(counts: [i32; 14]) -> Hands { | |
| let mut rank = 0; | |
| let mut kicker = 0; | |
| for (i, &count) in counts.iter().enumerate() { | |
| if count == 4 { | |
| rank = i; | |
| } else if count == 1 { | |
| kicker = i; | |
| } | |
| } | |
| Hands::FourOfAKind(rank, kicker) | |
| } | |
| fn parse_full_house(counts: [i32; 14]) -> Hands { | |
| let mut three_of_kind = 0; | |
| let mut pair = 0; | |
| for (i, &count) in counts.iter().enumerate() { | |
| if count == 3 { | |
| three_of_kind = i; | |
| } else if count == 2 { | |
| pair = i; | |
| } | |
| } | |
| Hands::FullHouse(three_of_kind, pair) | |
| } | |
| fn parse_three_of_kind(counts: [i32; 14]) -> Hands { | |
| let mut rank = 0; | |
| let mut kickers = [0; 2]; | |
| for (i, &count) in counts.iter().enumerate() { | |
| if count == 3 { | |
| rank = i; | |
| } else if count == 1 { | |
| if kickers[0] == 0 { | |
| kickers[0] = i; | |
| } else { | |
| kickers[1] = i; | |
| } | |
| } | |
| } | |
| Hands::ThreeOfAKind(rank, kickers) | |
| } | |
| fn parse_two_pair(counts: [i32; 14]) -> Hands { | |
| let mut pairs = [0; 2]; | |
| let mut kicker = 0; | |
| for (i, &count) in counts.iter().enumerate() { | |
| if count == 2 { | |
| if pairs[0] == 0 { | |
| pairs[0] = i; | |
| } else { | |
| pairs[1] = i; | |
| } | |
| } else if count == 1 { | |
| kicker = i; | |
| } | |
| } | |
| Hands::TwoPair(pairs[1], pairs[0], kicker) | |
| } | |
| fn parse_pair(counts: [i32; 14]) -> Hands { | |
| let mut rank = 0; | |
| let mut kickers = [0; 3]; | |
| for (i, &count) in counts.iter().enumerate() { | |
| if count == 2 { | |
| rank = i; | |
| } else if count == 1 { | |
| if kickers[0] == 0 { | |
| kickers[0] = i; | |
| } else if kickers[1] == 0 { | |
| kickers[1] = i; | |
| } else { | |
| kickers[2] = i; | |
| } | |
| } | |
| } | |
| Hands::Pair(rank, kickers) | |
| } | |
| // Parse a (sorted) hand from a string slice | |
| fn parse_hand(hand_str: &str) -> Hand { | |
| let mut cards = [Card::new(0, 0).unwrap(); 5]; | |
| for (i, card_str) in hand_str.split_whitespace().enumerate() { | |
| cards[i] = Card::from(card_str); | |
| } | |
| cards | |
| .sort_by(|a, b| a.rank.partial_cmp(&b.rank).unwrap()); | |
| Hand { cards } | |
| } | |
| #[derive(PartialEq, PartialOrd)] | |
| enum Hands { | |
| HighCard([usize; 5]), | |
| Pair(usize, [usize; 3]), | |
| TwoPair(usize, usize, usize), | |
| ThreeOfAKind(usize, [usize; 2]), | |
| Straight(usize), | |
| Flush([usize; 5]), | |
| FullHouse(usize, usize), | |
| FourOfAKind(usize, usize), | |
| StraightFlush(usize), | |
| } | |
| #[derive(PartialEq, PartialOrd)] | |
| struct Hand { | |
| cards: [Card; 5], | |
| } | |
| #[derive(PartialEq, PartialOrd, Clone, Copy)] | |
| struct Card { | |
| rank: usize, | |
| //0 is empty | |
| suit: usize, | |
| } | |
| impl Card { | |
| fn new(rank: usize, suit: usize) -> Option<Self> { | |
| if rank > 13 || suit > 3 { | |
| None | |
| } else { | |
| Some(Card { rank, suit }) | |
| } | |
| } | |
| } | |
| impl From<&str> for Card { | |
| fn from(input: &str) -> Self { | |
| match input.len() { | |
| 2..=3 => { | |
| let rank = Card::get_rank(input); | |
| let suit = Card::get_suit(input); | |
| Card { rank, suit } | |
| } | |
| _ => panic!("Invalid card"), | |
| } | |
| } | |
| } | |
| impl Card { | |
| fn get_rank(input: &str) -> usize { | |
| let rank = match input.chars().next().unwrap() { | |
| '2' => 1, | |
| '3' => 2, | |
| '4' => 3, | |
| '5' => 4, | |
| '6' => 5, | |
| '7' => 6, | |
| '8' => 7, | |
| '9' => 8, | |
| '1' => 9, | |
| 'J' => 10, | |
| 'Q' => 11, | |
| 'K' => 12, | |
| 'A' => 13, | |
| _ => panic!("Invalid rank"), | |
| }; | |
| rank | |
| } | |
| fn get_suit(input: &str) -> usize { | |
| let suit = match input.chars().last().unwrap() { | |
| 'C' => 0, | |
| 'D' => 1, | |
| 'H' => 2, | |
| 'S' => 3, | |
| _ => panic!("Invalid suit"), | |
| }; | |
| suit | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use poker::winning_hands; | |
| use std::collections::HashSet; | |
| fn hs_from<'a>(input: &[&'a str]) -> HashSet<&'a str> { | |
| let mut hs = HashSet::new(); | |
| for item in input.iter() { | |
| hs.insert(*item); | |
| } | |
| hs | |
| } | |
| /// Test that the expected output is produced from the given input | |
| /// using the `winning_hands` function. | |
| /// | |
| /// Note that the output can be in any order. Here, we use a HashSet to | |
| /// abstract away the order of outputs. | |
| fn test(input: &[&str], expected: &[&str]) { | |
| assert_eq!(hs_from(&winning_hands(input)), hs_from(expected)) | |
| } | |
| #[test] | |
| fn test_single_hand_always_wins() { | |
| test(&["4S 5S 7H 8D JC"], &["4S 5S 7H 8D JC"]) | |
| } | |
| #[test] | |
| fn test_duplicate_hands_always_tie() { | |
| let input = &["3S 4S 5D 6H JH", "3S 4S 5D 6H JH", "3S 4S 5D 6H JH"]; | |
| assert_eq!(&winning_hands(input), input) | |
| } | |
| #[test] | |
| fn test_highest_card_of_all_hands_wins() { | |
| test( | |
| &["4D 5S 6S 8D 3C", "2S 4C 7S 9H 10H", "3S 4S 5D 6H JH"], | |
| &["3S 4S 5D 6H JH"], | |
| ) | |
| } | |
| #[test] | |
| fn test_a_tie_has_multiple_winners() { | |
| test( | |
| &[ | |
| "4D 5S 6S 8D 3C", | |
| "2S 4C 7S 9H 10H", | |
| "3S 4S 5D 6H JH", | |
| "3H 4H 5C 6C JD", | |
| ], | |
| &["3S 4S 5D 6H JH", "3H 4H 5C 6C JD"], | |
| ) | |
| } | |
| #[test] | |
| fn test_high_card_can_be_low_card_in_an_otherwise_tie() { | |
| // multiple hands with the same high cards, tie compares next highest ranked, | |
| // down to last card | |
| test(&["3S 5H 6S 8D 7H", "2S 5D 6D 8C 7S"], &["3S 5H 6S 8D 7H"]) | |
| } | |
| #[test] | |
| fn test_one_pair_beats_high_card() { | |
| test(&["4S 5H 6C 8D KH", "2S 4H 6S 4D JH"], &["2S 4H 6S 4D JH"]) | |
| } | |
| #[test] | |
| fn test_highest_pair_wins() { | |
| test(&["4S 2H 6S 2D JH", "2S 4H 6C 4D JD"], &["2S 4H 6C 4D JD"]) | |
| } | |
| #[test] | |
| fn test_two_pairs_beats_one_pair() { | |
| test(&["2S 8H 6S 8D JH", "4S 5H 4C 8C 5C"], &["4S 5H 4C 8C 5C"]) | |
| } | |
| #[test] | |
| fn test_two_pair_ranks() { | |
| // both hands have two pairs, highest ranked pair wins | |
| test(&["2S 8H 2D 8D 3H", "4S 5H 4C 8S 5D"], &["2S 8H 2D 8D 3H"]) | |
| } | |
| #[test] | |
| fn test_two_pairs_second_pair_cascade() { | |
| // both hands have two pairs, with the same highest ranked pair, | |
| // tie goes to low pair | |
| test(&["2S QS 2C QD JH", "JD QH JS 8D QC"], &["JD QH JS 8D QC"]) | |
| } | |
| #[test] | |
| fn test_two_pairs_last_card_cascade() { | |
| // both hands have two identically ranked pairs, | |
| // tie goes to remaining card (kicker) | |
| test(&["JD QH JS 8D QC", "JS QS JC 2D QD"], &["JD QH JS 8D QC"]) | |
| } | |
| #[test] | |
| fn test_three_of_a_kind_beats_two_pair() { | |
| test(&["2S 8H 2H 8D JH", "4S 5H 4C 8S 4H"], &["4S 5H 4C 8S 4H"]) | |
| } | |
| #[test] | |
| fn test_three_of_a_kind_ranks() { | |
| //both hands have three of a kind, tie goes to highest ranked triplet | |
| test(&["2S 2H 2C 8D JH", "4S AH AS 8C AD"], &["4S AH AS 8C AD"]) | |
| } | |
| #[test] | |
| fn test_low_three_of_a_kind_beats_high_two_pair() { | |
| test(&["2H 2D 2C 8H 5H", "AS AC KS KC 6S"], &["2H 2D 2C 8H 5H"]) | |
| } | |
| #[test] | |
| fn test_three_of_a_kind_cascade_ranks() { | |
| // with multiple decks, two players can have same three of a kind, | |
| // ties go to highest remaining cards | |
| test(&["4S AH AS 7C AD", "4S AH AS 8C AD"], &["4S AH AS 8C AD"]) | |
| } | |
| #[test] | |
| fn test_straight_beats_three_of_a_kind() { | |
| test(&["4S 5H 4C 8D 4H", "3S 4D 2S 6D 5C"], &["3S 4D 2S 6D 5C"]) | |
| } | |
| #[test] | |
| fn test_aces_can_end_a_straight_high() { | |
| // aces can end a straight (10 J Q K A) | |
| test(&["4S 5H 4C 8D 4H", "10D JH QS KD AC"], &["10D JH QS KD AC"]) | |
| } | |
| #[test] | |
| fn test_aces_can_start_a_straight_low() { | |
| // aces can start a straight (A 2 3 4 5) | |
| test(&["4S 5H 4C 8D 4H", "4D AH 3S 2D 5C"], &["4D AH 3S 2D 5C"]) | |
| } | |
| #[test] | |
| fn test_no_ace_in_middle_of_straight() { | |
| // aces cannot be in the middle of a straight (Q K A 2 3) | |
| test(&["2C 3D 7H 5H 2S", "QS KH AC 2D 3S"], &["2C 3D 7H 5H 2S"]) | |
| } | |
| #[test] | |
| fn test_straight_ranks() { | |
| // both hands with a straight, tie goes to highest ranked card | |
| test(&["4S 6C 7S 8D 5H", "5S 7H 8S 9D 6H"], &["5S 7H 8S 9D 6H"]) | |
| } | |
| #[test] | |
| fn test_straight_scoring() { | |
| // even though an ace is usually high, a 5-high straight is the lowest-scoring straight | |
| test(&["2H 3C 4D 5D 6H", "4S AH 3S 2D 5H"], &["2H 3C 4D 5D 6H"]) | |
| } | |
| #[test] | |
| fn test_flush_beats_a_straight() { | |
| test(&["4C 6H 7D 8D 5H", "2S 4S 5S 6S 7S"], &["2S 4S 5S 6S 7S"]) | |
| } | |
| #[test] | |
| fn test_flush_cascade() { | |
| // both hands have a flush, tie goes to high card, down to the last one if necessary | |
| test(&["4H 7H 8H 9H 6H", "2S 4S 5S 6S 7S"], &["4H 7H 8H 9H 6H"]) | |
| } | |
| #[test] | |
| fn test_full_house_beats_a_flush() { | |
| test(&["3H 6H 7H 8H 5H", "4S 5C 4C 5D 4H"], &["4S 5C 4C 5D 4H"]) | |
| } | |
| #[test] | |
| fn test_full_house_ranks() { | |
| // both hands have a full house, tie goes to highest-ranked triplet | |
| test(&["4H 4S 4D 9S 9D", "5H 5S 5D 8S 8D"], &["5H 5S 5D 8S 8D"]) | |
| } | |
| #[test] | |
| fn test_full_house_cascade() { | |
| // with multiple decks, both hands have a full house with the same triplet, tie goes to the pair | |
| test(&["5H 5S 5D 9S 9D", "5H 5S 5D 8S 8D"], &["5H 5S 5D 9S 9D"]) | |
| } | |
| #[test] | |
| fn test_four_of_a_kind_beats_full_house() { | |
| test(&["4S 5H 4D 5D 4H", "3S 3H 2S 3D 3C"], &["3S 3H 2S 3D 3C"]) | |
| } | |
| #[test] | |
| fn test_four_of_a_kind_ranks() { | |
| // both hands have four of a kind, tie goes to high quad | |
| test(&["2S 2H 2C 8D 2D", "4S 5H 5S 5D 5C"], &["4S 5H 5S 5D 5C"]) | |
| } | |
| #[test] | |
| fn test_four_of_a_kind_cascade() { | |
| // with multiple decks, both hands with identical four of a kind, tie determined by kicker | |
| test(&["3S 3H 2S 3D 3C", "3S 3H 4S 3D 3C"], &["3S 3H 4S 3D 3C"]) | |
| } | |
| #[test] | |
| fn test_straight_flush_beats_four_of_a_kind() { | |
| test(&["4S 5H 5S 5D 5C", "7S 8S 9S 6S 10S"], &["7S 8S 9S 6S 10S"]) | |
| } | |
| #[test] | |
| fn test_aces_can_end_a_straight_flush_high() { | |
| // aces can end a straight flush (10 J Q K A) | |
| test(&["KC AH AS AD AC", "10C JC QC KC AC"], &["10C JC QC KC AC"]) | |
| } | |
| #[test] | |
| fn test_aces_can_start_a_straight_flush_low() { | |
| // aces can start a straight flush (A 2 3 4 5) | |
| test(&["KS AH AS AD AC", "4H AH 3H 2H 5H"], &["4H AH 3H 2H 5H"]) | |
| } | |
| #[test] | |
| fn test_no_ace_in_middle_of_straight_flush() { | |
| // aces cannot be in the middle of a straight flush (Q K A 2 3) | |
| test(&["2C AC QC 10C KC", "QH KH AH 2H 3H"], &["2C AC QC 10C KC"]) | |
| } | |
| #[test] | |
| fn test_straight_flush_ranks() { | |
| // both hands have a straight flush, tie goes to highest-ranked card | |
| test(&["4H 6H 7H 8H 5H", "5S 7S 8S 9S 6S"], &["5S 7S 8S 9S 6S"]) | |
| } | |
| #[test] | |
| fn test_straight_flush_scoring() { | |
| // even though an ace is usually high, a 5-high straight flush is the lowest-scoring straight flush | |
| test(&["2H 3H 4H 5H 6H", "4D AD 3D 2D 5D"], &["2H 3H 4H 5H 6H"]) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment