Skip to content

Instantly share code, notes, and snippets.

@graffhyrum
Created May 17, 2023 20:59
Show Gist options
  • Select an option

  • Save graffhyrum/dec535ff3a595113b89dac1b9a42fa80 to your computer and use it in GitHub Desktop.

Select an option

Save graffhyrum/dec535ff3a595113b89dac1b9a42fa80 to your computer and use it in GitHub Desktop.
Rust Exercism Poker challenge
/// 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
}
}
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