If it were me, I would rely on the behaviour of #[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
instead of trying to implement the traits manually. That way you can make sure things will be ordered correctly just by looking at how their definitions are ordered.
First, I would define the basic types you need for representing a card. Note that there is no ordering between suits in poker, and therefore we don't derive Ord
(and PartialOrd
) for the Suit
and Card
types.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct Card {
suit: Suit,
rank: Rank,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Suit {
Hearts,
Clubs,
Diamonds,
Spades,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Rank {
Number(usize),
Jack,
Queen,
King,
Ace,
}
impl Rank {
pub fn from_number(n: usize) -> Option<Self> {
match n {
1 => Some(Rank::Ace),
2..=10 => Some(Rank::Number(n)),
11 => Some(Rank::Jack),
12 => Some(Rank::Queen),
13 => Some(Rank::King),
_ => None,
}
}
}
Next, I would define the different kinds of hand you can get (pair, straight, etc.). Note that the ordering of fields and the traits we derive are important here.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum HandValue {
HighCard(Rank),
Pair(Rank),
TwoPair {
high_card: Rank,
other: Rank,
},
ThreeOfAKind(Rank),
Straight {
highest_value: Rank,
},
Flush {
high_card: Rank,
other_cards: [Rank; 4],
},
FullHouse {
triple: Rank,
pair: Rank,
},
FourOfAKind(Rank),
StraightFlush {
highest_rank: Rank,
other_cards: [Rank; 4],
},
}
(Note: As @KSwanson's comment points out, this could be simplified by making Flush
hold a [Rank; 5]
and skipping the high_card
/other_cards
business)
Finally, we can define Hand
and a way to compare two hands. Note that I've derived PartialEq
and Eq
for Hand
because we can always check whether two hands have the same cards in them.
The value()
method is where you would look at the hand and figure out whether it's got a pair or straight flush or whatever.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct Hand([Card; 5]);
impl Hand {
pub fn value(&self) -> HandValue {
todo!("Figure out what type of hand the player has");
}
}
For convenience, I'd throw in a FromStr
impl which lets you parse something like "3H 4H 5C 6C JD"
into a Hand
.
impl FromStr for Hand {
type Err = InvalidFormat;
fn from_str(s: &str) -> Result<Self, Self::Err> {
todo!("Parse a string like '3H 4H 5C 6C JD' into its 5 cards");
}
}
#[derive(Debug)]
struct InvalidFormat;
Finally, we can start checking which hand is better.
fn main() {
let person_1: Hand = "3H 4H 5C 6C 7D".parse().unwrap();
let person_2: Hand = "4S 5D 6H 7H 8C".parse().unwrap();
// the two hands have different cards
assert_ne!(person_1, person_2);
// checking ordering directly is a compile error
// assert!(person_1 < person_2); // uncomment me
// however, we can check whether they have the same
assert!(person_1.value() < person_2.value());
}
All the above code is on the playground.
Note that besides the todo!()
bits, that was all I needed to do. By ordering my enums so that the lowest ranked variant comes first and variants like HandValue::TwoPair
have fields ordered so the tie-breaker is compared first (e.g. if multiple people have two pairs, we'll first compare their high_card
field before the other
) I was able to let the compiler generate the tricky implementations.
Also, regarding your is_royal_flush()
-like methods, there is a really nice blog post which explains why the value()
way of doing things is superior... "Parse, don't validate".