diff --git a/config.json b/config.json index 266223c0b..10aa008e5 100644 --- a/config.json +++ b/config.json @@ -623,6 +623,20 @@ "topics": [ ] }, + { + "uuid": "0a33f3ac-cedd-4a40-a132-9d044b0e9977", + "slug": "poker", + "core": false, + "unlocked_by": null, + "difficulty": 6, + "topics": [ + "lifetimes", + "struct", + "string parsing", + "enum", + "traits" + ] + }, { "uuid": "f3172997-91f5-4941-a76e-91c4b8eed401", "slug": "anagram", diff --git a/exercises/poker/.gitignore b/exercises/poker/.gitignore new file mode 100644 index 000000000..cb14a4206 --- /dev/null +++ b/exercises/poker/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock diff --git a/exercises/poker/.meta/hints.md b/exercises/poker/.meta/hints.md new file mode 100644 index 000000000..3eecd33e5 --- /dev/null +++ b/exercises/poker/.meta/hints.md @@ -0,0 +1,8 @@ +## Hints + +- Ranking a list of poker hands can be considered a sorting problem. +- Rust provides the [sort](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.sort) method for `Vec where T: Ord`. +- [`Ord` types](https://doc.rust-lang.org/std/cmp/trait.Ord.html) are form a [total order](https://en.wikipedia.org/wiki/Total_order): exactly one of `a < b`, `a == b`, or `a > b` must be true. +- Poker hands do not conform to a total order: it is possible for two hands to be non-equal but have equal sort order. Example: `3S 4S 5D 6H JH"`, `"3H 4H 5C 6C JD"`. +- Rust provides the [`PartialOrd` trait](https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html) to handle the case of sortable things which do not have a total order. However, it doesn't provide a standard `sort` method for `Vec where T: PartialOrd`. The standard idiom to sort a vector in this case is `your_vec.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::{Less|Equal|Greater}));`, depending on your needs. ` +- You might consider implementing a type representing a poker hand which implements `PartialOrd`. diff --git a/exercises/poker/Cargo-example.toml b/exercises/poker/Cargo-example.toml new file mode 100644 index 000000000..fb30e27d7 --- /dev/null +++ b/exercises/poker/Cargo-example.toml @@ -0,0 +1,8 @@ +[package] +name = "poker" +version = "1.0.0" +authors = ["Peter Goodspeed-Niklaus "] + +[dependencies] +try_opt = "0.1.1" +counter = "0.1.0" diff --git a/exercises/poker/Cargo.toml b/exercises/poker/Cargo.toml new file mode 100644 index 000000000..7ada23c5e --- /dev/null +++ b/exercises/poker/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "poker" +version = "1.0.0" +authors = ["Peter Goodspeed-Niklaus "] + +[dependencies] diff --git a/exercises/poker/README.md b/exercises/poker/README.md new file mode 100644 index 000000000..2f978d0eb --- /dev/null +++ b/exercises/poker/README.md @@ -0,0 +1,54 @@ +# Poker + +Pick the best hand(s) from a list of poker hands. + +See [wikipedia](https://en.wikipedia.org/wiki/List_of_poker_hands) for an +overview of poker hands. + +## Hints + +- Ranking a list of poker hands can be considered a sorting problem. +- Rust provides the [sort](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.sort) method for `Vec where T: Ord`. +- [`Ord` types](https://doc.rust-lang.org/std/cmp/trait.Ord.html) are form a [total order](https://en.wikipedia.org/wiki/Total_order): exactly one of `a < b`, `a == b`, or `a > b` must be true. +- Poker hands do not conform to a total order: it is possible for two hands to be non-equal but have equal sort order. Example: `3S 4S 5D 6H JH"`, `"3H 4H 5C 6C JD"`. +- Rust provides the [`PartialOrd` trait](https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html) to handle the case of sortable things which do not have a total order. However, it doesn't provide a standard `sort` method for `Vec where T: PartialOrd`. The standard idiom to sort a vector in this case is `your_vec.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::{Less|Equal|Greater}));`, depending on your needs. ` +- You might consider implementing a type representing a poker hand which implements `PartialOrd`. + + +## Rust Installation + +Refer to the [exercism help page][help-page] for Rust installation and learning +resources. + +## Writing the Code + +Execute the tests with: + +```bash +$ cargo test +``` + +All but the first test have been ignored. After you get the first test to +pass, remove the ignore flag (`#[ignore]`) from the next test and get the tests +to pass again. The test file is located in the `tests` directory. You can +also remove the ignore flag from all the tests to get them to run all at once +if you wish. + +Make sure to read the [Crates and Modules](https://doc.rust-lang.org/stable/book/crates-and-modules.html) chapter if you +haven't already, it will help you with organizing your files. + +## Feedback, Issues, Pull Requests + +The [exercism/rust](https://github.com/exercism/rust) repository on GitHub is the home for all of the Rust exercises. If you have feedback about an exercise, or want to help implement new exercises, head over there and create an issue. Members of the [rust track team](https://github.com/orgs/exercism/teams/rust) are happy to help! + +If you want to know more about Exercism, take a look at the [contribution guide](https://github.com/exercism/docs/blob/master/contributing-to-language-tracks/README.md). + +[help-page]: http://exercism.io/languages/rust +[crates-and-modules]: http://doc.rust-lang.org/stable/book/crates-and-modules.html + +## Source + +Inspired by the training course from Udacity. [https://www.udacity.com/course/viewer#!/c-cs212/](https://www.udacity.com/course/viewer#!/c-cs212/) + +## Submitting Incomplete Solutions +It's possible to submit an incomplete solution so you can see how others have completed the exercise. diff --git a/exercises/poker/example.rs b/exercises/poker/example.rs new file mode 100644 index 000000000..2937d964f --- /dev/null +++ b/exercises/poker/example.rs @@ -0,0 +1,341 @@ +use std::fmt; +use std::cmp::Ordering; + +#[macro_use] +extern crate try_opt; + +extern crate counter; +use counter::Counter; + +/// 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]) -> Option> { + let mut hands = try_opt!( + hands + .iter() + .map(|source| Hand::try_from(source)) + .collect::>>() + ); + hands.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Less)); + hands.last().map(|last| { + hands + .iter() + .rev() + .take_while(|&item| item.partial_cmp(last) == Some(Ordering::Equal)) + .map(|hand| hand.source) + .collect() + }) +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy, Hash)] +enum Suit { + Spades, + Clubs, + Diamonds, + Hearts, +} + +impl Suit { + fn try_from(source: &str) -> Option { + use Suit::*; + match source { + "S" => Some(Spades), + "C" => Some(Clubs), + "D" => Some(Diamonds), + "H" => Some(Hearts), + _ => None, + } + } +} + +impl fmt::Display for Suit { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use Suit::*; + write!( + f, + "{}", + match *self { + Spades => "S", + Clubs => "C", + Diamonds => "D", + Hearts => "H", + } + ) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +enum Rank { + Number(u8), + Jack, + Queen, + King, + Ace, +} + +impl Rank { + fn try_from(source: &str) -> Option { + use Rank::*; + match source { + "A" => Some(Ace), + "K" => Some(King), + "Q" => Some(Queen), + "J" => Some(Jack), + "10" => Some(Number(10)), + "9" => Some(Number(9)), + "8" => Some(Number(8)), + "7" => Some(Number(7)), + "6" => Some(Number(6)), + "5" => Some(Number(5)), + "4" => Some(Number(4)), + "3" => Some(Number(3)), + "2" => Some(Number(2)), + _ => None, + } + } + + fn value(&self) -> usize { + use Rank::*; + match *self { + Ace => 14, + King => 13, + Queen => 12, + Jack => 11, + Number(n) => n as usize, + } + } +} + +impl fmt::Display for Rank { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use Rank::*; + let num_str; // early declaration to placate NLL of Number case + write!( + f, + "{}", + match *self { + Ace => "A", + King => "K", + Queen => "Q", + Jack => "J", + Number(n) => { + num_str = n.to_string(); + &num_str + } + } + ) + } +} + +impl PartialOrd for Rank { + fn partial_cmp(&self, other: &Rank) -> Option { + Some(self.value().cmp(&other.value())) + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)] +struct Card { + rank: Rank, + suit: Suit, +} + +impl Card { + fn try_from_split(source: &str, split: usize) -> Option { + Some(Card { + rank: try_opt!(Rank::try_from(&source[..split])), + suit: try_opt!(Suit::try_from(&source[split..])), + }) + } + + fn try_from(source: &str) -> Option { + match source.len() { + 3 => Card::try_from_split(source, 2), + 2 => Card::try_from_split(source, 1), + _ => None, + } + } +} + +impl fmt::Display for Card { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}{}", self.rank, self.suit) + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +enum PokerHand { + HighCard, + OnePair, + TwoPair, + ThreeOfAKind, + Straight, + Flush, + FullHouse, + FourOfAKind, + StraightFlush, +} + +impl PokerHand { + fn is_ace_low_straight(cards: &[Card]) -> bool { + // special case: ace-low straight + // still depends on the sorted precondition + cards[0].rank.value() == 2 && cards[4].rank == Rank::Ace && + cards + .windows(2) + .take(3) // (0, 1), (1, 2), (2, 3) --> skips 4, ace + .map(|pair| pair[1].rank.value() - pair[0].rank.value()) + .all(|diff| diff == 1) + } + + fn analyze(cards: &[Card]) -> Option { + if cards.len() == 5 { + let suit_counter = Counter::init(cards.iter().map(|c| c.suit)); + let is_flush = suit_counter + .most_common() + .map(|(_suit, count)| count) + .next() == Some(5); + // Note that `is_straight` depends on a precondition: it only works + // if the input `cards` are sorted by rank value ascending. + let is_straight = cards + .windows(2) + .map(|pair| pair[1].rank.value() - pair[0].rank.value()) + .all(|diff| diff == 1) || + PokerHand::is_ace_low_straight(cards); + + if is_flush && is_straight { + return Some(PokerHand::StraightFlush); + } + + let rank_counter = Counter::init(cards.iter().map(|c| c.rank)); + let mut rc_iter = rank_counter.most_common().map(|(_rank, count)| count); + let rc_most = rc_iter.next(); + let rc_second = rc_iter.next(); + + if rc_most == Some(4) { + return Some(PokerHand::FourOfAKind); + } + if rc_most == Some(3) && rc_second == Some(2) { + return Some(PokerHand::FullHouse); + } + if is_flush { + return Some(PokerHand::Flush); + } + if is_straight { + return Some(PokerHand::Straight); + } + if rc_most == Some(3) { + return Some(PokerHand::ThreeOfAKind); + } + if rc_most == Some(2) && rc_second == Some(2) { + return Some(PokerHand::TwoPair); + } + if rc_most == Some(2) { + return Some(PokerHand::OnePair); + } + Some(PokerHand::HighCard) + } else { + None + } + } +} + +#[derive(Debug, PartialEq, Eq)] +struct Hand<'a> { + source: &'a str, + cards: [Card; 5], + hand_type: PokerHand, +} + +impl<'a> Hand<'a> { + fn try_from(source: &'a str) -> Option { + let mut cards = try_opt!( + source + .split_whitespace() + .map(|s| Card::try_from(s)) + .collect::>>() + ); + cards.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Less)); + if cards.len() == 5 { + Some(Hand { + source: source, + cards: [cards[0], cards[1], cards[2], cards[3], cards[4]], + hand_type: try_opt!(PokerHand::analyze(&cards)), + }) + } else { + None + } + } + + fn cmp_high_card(&self, other: &Hand, card: usize) -> Ordering { + let mut ordering = self.cards[card].rank.value().cmp( + &other.cards[card].rank.value(), + ); + if card != 0 { + ordering = ordering.then_with(|| self.cmp_high_card(other, card - 1)); + } + ordering + } + + fn value_by_frequency(&self) -> (Option, Option, Option) { + let rank_counter = Counter::init(self.cards.iter().map(|c| c.rank)); + let mut rc_iter = rank_counter + .most_common_tiebreaker(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Less)) + .map(|(rank, _count)| rank); + (rc_iter.next(), rc_iter.next(), rc_iter.next()) + } + + fn cmp_cascade_by_freq(&self, other: &Hand) -> Ordering { + let (s1, s2, s3) = self.value_by_frequency(); + let (o1, o2, o3) = other.value_by_frequency(); + s1.partial_cmp(&o1) + .map(|c| { + c.then( + s2.partial_cmp(&o2) + .map(|c2| c2.then(s3.partial_cmp(&o3).unwrap_or(Ordering::Equal))) + .unwrap_or(Ordering::Equal), + ) + }) + .unwrap_or(Ordering::Equal) + } + + fn cmp_straight(&self, other: &Hand) -> Ordering { + let s = if PokerHand::is_ace_low_straight(&self.cards) { + 5 + } else { + self.cards[4].rank.value() + }; + let o = if PokerHand::is_ace_low_straight(&other.cards) { + 5 + } else { + other.cards[4].rank.value() + }; + s.cmp(&o) + } +} + +impl<'a> fmt::Display for Hand<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.source) + } +} + +impl<'a> PartialOrd for Hand<'a> { + fn partial_cmp(&self, other: &Hand) -> Option { + Some(self.hand_type.cmp(&other.hand_type).then_with(|| { + use PokerHand::*; + match self.hand_type { + HighCard => self.cmp_high_card(other, 4), + OnePair => self.cmp_cascade_by_freq(other), + TwoPair => self.cmp_cascade_by_freq(other), + ThreeOfAKind => self.cmp_cascade_by_freq(other), + Straight => self.cmp_straight(other), + Flush => self.cmp_high_card(other, 4), + FullHouse => self.cmp_cascade_by_freq(other), + FourOfAKind => self.cmp_cascade_by_freq(other), + StraightFlush => self.cmp_straight(other), + } + })) + } +} diff --git a/exercises/poker/src/lib.rs b/exercises/poker/src/lib.rs new file mode 100644 index 000000000..e34909fec --- /dev/null +++ b/exercises/poker/src/lib.rs @@ -0,0 +1,7 @@ +/// 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>(_: &[&'a str]) -> Option> { + unimplemented!() +} diff --git a/exercises/poker/tests/poker.rs b/exercises/poker/tests/poker.rs new file mode 100644 index 000000000..85f04dae0 --- /dev/null +++ b/exercises/poker/tests/poker.rs @@ -0,0 +1,298 @@ +extern crate poker; +use std::collections::HashSet; +use poker::winning_hands; + +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<'a, 'b>(input: &[&'a str], expected: &[&'b str]) { + assert_eq!( + hs_from(&winning_hands(input).expect( + "This test should produce Some value", + )), + hs_from(expected) + ) +} + +#[test] +fn test_single_hand_always_wins() { + test(&vec!["4S 5S 7H 8D JC"], &vec!["4S 5S 7H 8D JC"]) +} + +#[test] +#[ignore] +fn test_highest_card_of_all_hands_wins() { + test( + &vec!["4D 5S 6S 8D 3C", "2S 4C 7S 9H 10H", "3S 4S 5D 6H JH"], + &vec!["3S 4S 5D 6H JH"], + ) +} + +#[test] +#[ignore] +fn test_a_tie_has_multiple_winners() { + test( + &vec![ + "4D 5S 6S 8D 3C", + "2S 4C 7S 9H 10H", + "3S 4S 5D 6H JH", + "3H 4H 5C 6C JD", + ], + &vec!["3S 4S 5D 6H JH", "3H 4H 5C 6C JD"], + ) +} + +#[test] +#[ignore] +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( + &vec!["3S 5H 6S 8D 7H", "2S 5D 6D 8C 7S"], + &vec!["3S 5H 6S 8D 7H"], + ) +} + +#[test] +#[ignore] +fn test_one_pair_beats_high_card() { + test( + &vec!["4S 5H 6C 8D KH", "2S 4H 6S 4D JH"], + &vec!["2S 4H 6S 4D JH"], + ) +} + +#[test] +#[ignore] +fn test_highest_pair_wins() { + test( + &vec!["4S 2H 6S 2D JH", "2S 4H 6C 4D JD"], + &vec!["2S 4H 6C 4D JD"], + ) +} + +#[test] +#[ignore] +fn test_two_pairs_beats_one_pair() { + test( + &vec!["2S 8H 6S 8D JH", "4S 5H 4C 8C 5C"], + &vec!["4S 5H 4C 8C 5C"], + ) +} + +#[test] +#[ignore] +fn test_two_pair_ranks() { + // both hands have two pairs, highest ranked pair wins + test( + &vec!["2S 8H 2D 8D 3H", "4S 5H 4C 8S 5D"], + &vec!["2S 8H 2D 8D 3H"], + ) +} + +#[test] +#[ignore] +fn test_two_pairs_second_pair_cascade() { + // both hands have two pairs, with the same highest ranked pair, + // tie goes to low pair + test( + &vec!["2S QS 2C QD JH", "JD QH JS 8D QC"], + &vec!["JD QH JS 8D QC"], + ) +} + +#[test] +#[ignore] +fn test_two_pairs_last_card_cascade() { + // both hands have two identically ranked pairs, + // tie goes to remaining card (kicker) + test( + &vec!["JD QH JS 8D QC", "JS QS JC 2D QD"], + &vec!["JD QH JS 8D QC"], + ) +} + +#[test] +#[ignore] +fn test_three_of_a_kind_beats_two_pair() { + test( + &vec!["2S 8H 2H 8D JH", "4S 5H 4C 8S 4H"], + &vec!["4S 5H 4C 8S 4H"], + ) +} + +#[test] +#[ignore] +fn test_three_of_a_kind_ranks() { + //both hands have three of a kind, tie goes to highest ranked triplet + test( + &vec!["2S 2H 2C 8D JH", "4S AH AS 8C AD"], + &vec!["4S AH AS 8C AD"], + ) +} + +#[test] +#[ignore] +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( + &vec!["4S AH AS 7C AD", "4S AH AS 8C AD"], + &vec!["4S AH AS 8C AD"], + ) +} + +#[test] +#[ignore] +fn test_straight_beats_three_of_a_kind() { + test( + &vec!["4S 5H 4C 8D 4H", "3S 4D 2S 6D 5C"], + &vec!["3S 4D 2S 6D 5C"], + ) +} + +#[test] +#[ignore] +fn test_aces_can_end_a_straight_high() { + // aces can end a straight (10 J Q K A) + test( + &vec!["4S 5H 4C 8D 4H", "10D JH QS KD AC"], + &vec!["10D JH QS KD AC"], + ) +} + +#[test] +#[ignore] +fn test_aces_can_end_a_straight_low() { + // aces can start a straight (A 2 3 4 5) + test( + &vec!["4S 5H 4C 8D 4H", "4D AH 3S 2D 5C"], + &vec!["4D AH 3S 2D 5C"], + ) +} + +#[test] +#[ignore] +fn test_straight_cascade() { + // both hands with a straight, tie goes to highest ranked card + test( + &vec!["4S 6C 7S 8D 5H", "5S 7H 8S 9D 6H"], + &vec!["5S 7H 8S 9D 6H"], + ) +} + +#[test] +#[ignore] +fn test_straight_scoring() { + // even though an ace is usually high, a 5-high straight is the lowest-scoring straight + test( + &vec!["2H 3C 4D 5D 6H", "4S AH 3S 2D 5H"], + &vec!["2H 3C 4D 5D 6H"], + ) +} + +#[test] +#[ignore] +fn test_flush_beats_a_straight() { + test( + &vec!["4C 6H 7D 8D 5H", "2S 4S 5S 6S 7S"], + &vec!["2S 4S 5S 6S 7S"], + ) +} + +#[test] +#[ignore] +fn test_flush_cascade() { + // both hands have a flush, tie goes to high card, down to the last one if necessary + test( + &vec!["4H 7H 8H 9H 6H", "2S 4S 5S 6S 7S"], + &vec!["4H 7H 8H 9H 6H"], + ) +} + +#[test] +#[ignore] +fn test_full_house_beats_a_flush() { + test( + &vec!["3H 6H 7H 8H 5H", "4S 5C 4C 5D 4H"], + &vec!["4S 5C 4C 5D 4H"], + ) +} + +#[test] +#[ignore] +fn test_full_house_ranks() { + // both hands have a full house, tie goes to highest-ranked triplet + test( + &vec!["4H 4S 4D 9S 9D", "5H 5S 5D 8S 8D"], + &vec!["5H 5S 5D 8S 8D"], + ) +} + +#[test] +#[ignore] +fn test_full_house_cascade() { + // with multiple decks, both hands have a full house with the same triplet, tie goes to the pair + test( + &vec!["5H 5S 5D 9S 9D", "5H 5S 5D 8S 8D"], + &vec!["5H 5S 5D 9S 9D"], + ) +} + +#[test] +#[ignore] +fn test_four_of_a_kind_beats_full_house() { + test( + &vec!["4S 5H 4D 5D 4H", "3S 3H 2S 3D 3C"], + &vec!["3S 3H 2S 3D 3C"], + ) +} + +#[test] +#[ignore] +fn test_four_of_a_kind_ranks() { + // both hands have four of a kind, tie goes to high quad + test( + &vec!["2S 2H 2C 8D 2D", "4S 5H 5S 5D 5C"], + &vec!["4S 5H 5S 5D 5C"], + ) +} + +#[test] +#[ignore] +fn test_four_of_a_kind_cascade() { + // with multiple decks, both hands with identical four of a kind, tie determined by kicker + test( + &vec!["3S 3H 2S 3D 3C", "3S 3H 4S 3D 3C"], + &vec!["3S 3H 4S 3D 3C"], + ) +} + +#[test] +#[ignore] +fn test_straight_flush_beats_four_of_a_kind() { + test( + &vec!["4S 5H 5S 5D 5C", "7S 8S 9S 6S 10S"], + &vec!["7S 8S 9S 6S 10S"], + ) +} + +#[test] +#[ignore] +fn test_straight_flush_ranks() { + // both hands have straight flush, tie goes to highest-ranked card + test( + &vec!["4H 6H 7H 8H 5H", "5S 7S 8S 9S 6S"], + &vec!["5S 7S 8S 9S 6S"], + ) +}