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".