From 9c5ee74639d9616dec8d26f8fbbf29c0c9a51481 Mon Sep 17 00:00:00 2001 From: Aryadev Chavali Date: Thu, 2 Apr 2026 05:27:39 +0100 Subject: [PATCH] modes:single: tests for footstooling and invalid singles --- src/modes/single.rs | 133 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/src/modes/single.rs b/src/modes/single.rs index fc09c75..99e573f 100644 --- a/src/modes/single.rs +++ b/src/modes/single.rs @@ -33,3 +33,136 @@ impl Display for Single { write!(f, "Single({})", self.0) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::card::{make_decks, PlayingCard, Rank, Suit}; + + #[test] + fn invalid_singles() { + let deck = make_decks(1); + let singles: Vec> = + deck.iter().map(|&c| Single::new(c)).collect(); + let valid_singles: Vec = singles + .iter() + .filter(|x| !x.is_none()) + .map(|x| x.unwrap()) + .collect(); + + // There are exactly two cards in a single deck that aren't valid + // singles. In other words, all other cards are valid. + assert!(valid_singles.len() == deck.len() - 2); + + // All valid singles are playing cards. + assert!(valid_singles.iter().all(|Single(card)| !card.is_joker())); + + // By the previous two results, the only invalid singles are jokers. A + // direct test of this is fine as well. + assert!(Single::new(Card::from(-1)).is_none()); + } + + #[test] + fn footstools() { + let deck = make_decks(1); + let deck = &deck[2..]; // skip the jokers + let singles: Vec = + deck.iter().map(|&c| Single::new(c).unwrap()).collect(); + + singles.windows(3).for_each(|single_slice| { + let (s1, s2, s3) = + (single_slice[0], single_slice[1], single_slice[2]); + + // A single is full footstooled by itself + assert!(s1.footstool(s1) == Footstool::Full); + + // s2 is half-footstooled by s3, and s1 is half footstooled by s2. + assert!(s3.footstool(s2) == Footstool::Half); + assert!(s2.footstool(s1) == Footstool::Half); + + // Footstooling is not a reflexive relation + assert!(s1.footstool(s2) == Footstool::None); + assert!(s2.footstool(s3) == Footstool::None); + + // s1 does not footstool whatsoever with s3 + assert!(s1.footstool(s3) == Footstool::None); + assert!(s3.footstool(s1) == Footstool::None); + }); + + // An exhaustive check to verify that: + // 1) All footstool results are not reflexive. + // 2) A single is ONLY full-footstooled by itself + // 3) A single is half-footstooled by at most one singles + // 4) A single is not footstooled by any other singles + for single in &singles { + // Check footstools against every other card + let footstool_results: Vec<(Footstool, Footstool)> = singles + .iter() + .map(|&other_single| { + ( + single.footstool(other_single), + other_single.footstool(*single), + ) + }) + .collect(); + + // (1) + assert!(footstool_results.iter().all(|(x, y)| match (x, y) { + (Footstool::None, Footstool::None) => true, + (Footstool::Half, Footstool::None) + | (Footstool::None, Footstool::Half) => true, + (Footstool::Full, Footstool::Full) => true, + _ => false, + })); + let footstool_results: Vec = + footstool_results.iter().map(|x| x.0).collect(); + + // (2) + let full_footstools = footstool_results + .iter() + .filter(|&&x| x == Footstool::Full) + .count(); + assert!(full_footstools == 1); + + // (3) + let half_footstools = footstool_results + .iter() + .filter(|&&x| x == Footstool::Half) + .count(); + assert!(half_footstools <= 1); + + // (4) + let non_footstools = footstool_results + .iter() + .filter(|&&x| x == Footstool::None) + .count(); + assert!( + non_footstools < singles.len() + && non_footstools >= singles.len() - 3 + ); + } + } + + #[test] + fn deck_irrelevance() { + // For a fixed card, comparing to another deck's cards doesn't change if + // it gets footstooled. + let pivot = PlayingCard::new(0, Rank::Three, Suit::Club); + let pivot = Card::PlayingCard(pivot); + let pivot = Single(pivot); + for i in 1..10 { + let piv_copy = Single(Card::PlayingCard(PlayingCard { + deck: i, + ..pivot.0.playing_card().unwrap() + })); + let piv_before = Single(Card::from(i64::from(piv_copy.0) - 1)); + let piv_after = Single(Card::from(i64::from(piv_copy.0) + 1)); + let piv_way_after = Single(Card::from(i64::from(piv_copy.0) + 2)); + + assert!(pivot.footstool(piv_copy) == Footstool::Full); + assert!(pivot.footstool(piv_before) == Footstool::Half); + assert!(piv_after.footstool(pivot) == Footstool::Half); + assert!(pivot.footstool(piv_way_after) == Footstool::None); + } + } +}