use crate::{card::Card, helper::ordered}; #[derive(Debug, Copy, Clone)] pub struct Triple(Card, Card, Card); impl Triple { /// Create a new triple utilising 3 cards: `c1`, `c2`, and `c3`. Will /// return None iff a Triple cannot be constructed out of those 3 cards. /// /// NOTE: By construction, if a triple includes 1 Joker, then Triple::0 is /// that joker. If a triple includes 2 jokers, then Triple::0 and Triple::1 /// are those jokers. This means Triple::2 will always be a valid playing /// card. pub fn new(c1: Card, c2: Card, c3: Card) -> Option { let [c1, c2, c3] = ordered([c1, c2, c3]); match [c1, c2, c3].map(|c| c.rank()) { [None, None, Some(_)] => Some(Triple(c1, c2, c3)), [None, Some(r2), Some(r3)] if r2 == r3 => Some(Triple(c1, c2, c3)), [Some(r1), Some(r2), Some(r3)] if r1 == r2 && r2 == r3 => { Some(Triple(c1, c2, c3)) } _ => None, } } fn high_pair(&self) -> Pair { Pair::new(self.1, self.2).unwrap() } fn count_jokers(&self) -> usize { [self.0, self.1, self.2] .iter() .filter(|c| c.is_joker()) .count() } } use crate::helper::impl_cmp_eq_on_ord; use std::cmp::Ordering; impl Ord for Triple { fn cmp(&self, other: &Self) -> Ordering { let Triple(s1, s2, s3) = self; let Triple(o1, o2, o3) = other; // The most critical part of ordering is top card rank comparison. s3.rank() .cmp(&o3.rank()) // if we have 2 triples, both with the same ranked high card, and // one has 2 jokers while the other doesn't => the 2 joker triple is // worse. .then_with(|| match (self.count_jokers(), other.count_jokers()) { (2, x) if x < 2 => Ordering::Less, (x, 2) if x < 2 => Ordering::Greater, _ => Ordering::Equal, }) // then compare by the highest to lowest cards .then_with(|| s3.suit().cmp(&o3.suit())) .then_with(|| s2.cmp(o2)) .then_with(|| s1.cmp(o1)) } } impl_cmp_eq_on_ord!(Triple); use crate::modes::{pair::Pair, Footstool, Hand}; impl Hand for Triple { fn is_proper(&self) -> bool { self.count_jokers() == 0 } fn footstool(&self, other: &Self) -> Footstool { match self.cmp(other) { // There is no footstool if self is beaten by other. Ordering::Less => Footstool::None, // We can only full footstool if we have equivalent triples. Ordering::Equal => Footstool::Full, // Half footstools can only proc if the 2 high cards of each hand // footstool each other using Pair rules. Ordering::Greater => { // By construction, Triple::1 and Triple::2 should always make a // Pair so it's cool to unwrap. let [p1, p2] = [self, other].map(|x| x.high_pair()); match p1.footstool(&p2) { Footstool::Full => Footstool::Half, _ => Footstool::None, } } } } } use std::fmt::{Display, Formatter, Result}; impl Display for Triple { fn fmt(&self, f: &mut Formatter<'_>) -> Result { write!(f, "Triple[{}, {}, {}]", self.0, self.1, self.2) } } use std::hash::{Hash, Hasher}; impl Hash for Triple { fn hash(&self, state: &mut H) { // Pairs are just tuples lol. (self.0, self.1, self.2).hash(state); } } #[cfg(test)] mod tests { use crate::card::{PlayingCard, Rank}; use super::*; #[test] fn new() { let joker = Card::make_joker(); // TEST: Cannot make a triple out of three jokers assert_eq!( Triple::new(joker, joker, joker), None, "Expected triple of 3 jokers to be None" ); for card in PlayingCard::iter_all(0).map(Card::PlayingCard) { let trip = Triple::new(card, joker, joker); // TEST: Any card with two jokers is a triple assert!( trip.is_some(), "Expected ({card}, {joker}, {joker}) to make a triple" ); let trip = trip.unwrap(); // TEST: Triples formed with 2 jokers are improper. assert!(trip.is_improper(), "Expected {trip} to be improper"); // TEST: Triples with 2 jokers should have a playing card for // Triple::2. assert_eq!( trip.2, card, "Expected the highest card of the triple ({}) to be the sole PlayingCard ({card})", trip.2 ); } for rank in Rank::iter_all() { for (c1, c2) in rank.cards().zip(rank.cards()) { let trip = Triple::new(c1, c2, joker); // TEST: Any two similar rank cards with 1 joker are a // Triple. assert_ne!( trip, None, "Expected ({c1}, {c2}, Joker) to make a Triple" ); let trip = trip.unwrap(); // TEST: Triples formed with 1 joker are improper. assert!(trip.is_improper(), "Expected {trip} to be improper"); // TEST: 1 joker triples have Triple::0 as the joker. assert!( matches!(trip.0, Card::Joker(_)), "Expected {} to be a joker", trip.0 ); let [c1, c2] = ordered([c1, c2]); // TEST: Expect Triple::1 and Triple::2 to follow ordering // of c1 and c2. assert_eq!( [trip.1, trip.2], [c1, c2], "Expected {} = min({c1}, {c2}) and {} = max({c1}, {c2})", trip.1, trip.2, ); } } for (c1, (c2, c3)) in PlayingCard::iter_all(0) .zip(PlayingCard::iter_all(0).zip(PlayingCard::iter_all(0))) { let [c1, c2, c3] = [c1, c2, c3].map(Card::PlayingCard); let trip = Triple::new(c1, c2, c3); // TEST: Any 3 playing cards make a triple iff they match in // rank if !(c1.rank() == c2.rank() && c2.rank() == c3.rank()) { assert_eq!( trip, None, "Expected {c1}, {c2}, {c3} to never make a Triple." ); continue; } else { assert_ne!( trip, None, "Expected {c1}, {c2}, {c3} to make a Triple." ); } let trip = trip.unwrap(); // TEST: Triples formed of 3 playing cards are proper. assert!(trip.is_proper(), "Expected {trip} to be proper"); let [c1, c2, c3] = ordered([c1, c2, c3]); // TEST: If a triple is formed of 3 playing cards, they are // ordered s.t. Triple::2 > Triple::1 > Triple::0. assert_eq!( [trip.0, trip.1, trip.2], [c1, c2, c3], "Expected cards of {} to match ordered cards [{}, {}, {}]", trip, c1, c2, c3 ); } } } }