From abf638e869180dbaac3fca0d788886950a6c6c79 Mon Sep 17 00:00:00 2001 From: Aryadev Chavali Date: Sun, 5 Apr 2026 04:15:09 +0100 Subject: [PATCH] modes:single:tests: major refactor Better exhaustive testing, addition of messages on asserts as well. --- src/modes/single.rs | 229 ++++++++++++++++++++++++++++++-------------- 1 file changed, 158 insertions(+), 71 deletions(-) diff --git a/src/modes/single.rs b/src/modes/single.rs index d63bc65..201f07c 100644 --- a/src/modes/single.rs +++ b/src/modes/single.rs @@ -24,16 +24,13 @@ impl Hand for Single { } fn footstool(&self, other: &Self) -> Footstool { - let self_abs = self.0.deck_abs(); - let other_abs = other.0.deck_abs(); - - // Trivial implementation - if self_abs == other_abs { - Footstool::Full - } else if self_abs == (other_abs + 1) % 52 { - Footstool::Half - } else { - Footstool::None + // We use deck_abs() to get an index in the overall deck ordering. + match (self.0.deck_abs(), other.0.deck_abs()) { + // A full footstool only occurs when both are the same. + (x, y) if x == y => Footstool::Full, + // Half footstools can only occur when x is consecutive to y. + (x, y) if x == y + 1 => Footstool::Half, + _ => Footstool::None, } } } @@ -47,129 +44,219 @@ impl Display for Single { #[cfg(test)] mod tests { + use std::collections::{HashMap, HashSet}; + use super::*; use crate::{ - card::{make_decks, PlayingCard, Rank, Suit}, + card::{PlayingCard, Rank, Suit}, modes::tests::test_footstool, }; #[test] fn new() { // TEST: Jokers are not valid singles. - assert!(Single::new(Card::make_joker()).is_none()); + assert_eq!( + Single::new(Card::make_joker()), + None, + "Expected Jokers to never be valid singles" + ); - let valid_singles = - make_decks(1).filter_map(Single::new).collect::>(); - let deck = make_decks(1).collect::>(); + let valid_singles = Card::iter_all(1) + .filter_map(Single::new) + .collect::>(); + let deck = Card::iter_all(1).collect::>(); // TEST: Only two cards in a single deck aren't valid singles. - assert!(valid_singles.len() == deck.len() - 2); - - // TEST: All valid singles are playing cards. - assert!(valid_singles.iter().all(|Single(card)| !card.is_joker())); + assert_eq!(valid_singles.len(), deck.len() - 2); } #[test] fn footstool() { - // Make a deck with no jokers. - let singles = PlayingCard::iter_deck(0) - .map(Card::PlayingCard) + // Create a vector for all possible Singles in 1 deck of cards. Due to + // ordering of Card::iter_all, we expect this to be sorted as well. + let singles = Card::iter_all(1) .filter_map(Single::new) .collect::>(); + // TEST: Consecutive singles footstool testing. singles.windows(3).for_each(|single_slice| { let (s1, s2, s3) = (single_slice[0], single_slice[1], single_slice[2]); - // TEST: A single is always full footstooled by itself. - assert!(s1.footstool(&s1) == Footstool::Full); - - // TEST: non-reflexivity of footstool on neighbours. + // TEST: Test footstool patterns and get some results back for + // further testing. let (_, s2_on_s1) = test_footstool(&s1, &s2); let (_, s3_on_s2) = test_footstool(&s2, &s3); let (s1_on_s3, s3_on_s1) = test_footstool(&s1, &s3); // TEST: s2 is half-footstooled by s3, and s1 is half footstooled by // s2. - assert!(s3_on_s2 == Footstool::Half); - assert!(s2_on_s1 == Footstool::Half); + assert!( + s3_on_s2 == Footstool::Half, + "{s3} should half footstool {s2}" + ); + assert!( + s2_on_s1 == Footstool::Half, + "{s2} should half footstool {s1}" + ); // TEST: s1 does not footstool whatsoever with s3. - assert!(s1_on_s3 == Footstool::None); - assert!(s3_on_s1 == Footstool::None); + assert!( + s1_on_s3 == Footstool::None, + "{s1} should not footstool {s3}" + ); + assert!( + s3_on_s1 == Footstool::None, + "{s3} should not footstool {s1}" + ); }); - for single in &singles { - let footstool_results = singles - .iter() - .map(|&other_single| { - // TEST: All footstool results are non-reflexive. - test_footstool(single, &other_single) + // Exhaustive testing over every possible combinations of Singles. + + // Create an exhaustive map for all combinations (Single, Single) along + // with the results of the first footstooling the second. + let exhaustive_singles_footstool = singles + .iter() + .flat_map(|single| { + singles + .iter() + .map(move |other_single| (single, other_single)) + }) + .map(|(single, other_single)| { + // TEST: Expected generic pattern for footstooling of hands - + // see mod::tests::test_footstool for details. + + // NOTE: Due to test_footstool impl, this automatically tests + // that single == other_single <=> single full footstools + // other_single (and vice versa) + (single, other_single, test_footstool(single, other_single).0) + }) + .collect::>(); + + // TEST: Half footstools. + { + // Maps Singles to a Vector of the Singles they half footstool. + let counter = { + let mut counter: HashMap> = HashMap::new(); + exhaustive_singles_footstool + .iter() + .filter(|(_, _, res)| *res == Footstool::Half) + .for_each(|(c1, c2, _)| { + if let Some(val) = counter.get_mut(*c1) { + val.push(**c2); + } else { + counter.insert(**c1, vec![**c2]); + } + }); + counter + }; + + // TEST: For any Single there is only 1 other Single that it + // half footstools. + counter.iter().for_each(|(c1, counter)| { + assert_eq!( + counter.len(), + 1, + "Expected {c1} to only have 1 card that it half footstools" + ); + }); + + // TEST: For any Single, the Single that it half footstools + // is unique to it. + { + let mut unique_half_footstools = HashSet::::new(); + counter.iter().for_each(|(c1, counter)| { + let c2 = counter[0]; + assert_eq!(unique_half_footstools.get(&c2), None, "Expected {c2} to be unique to the half footstools of {c1}"); + unique_half_footstools.insert(c2); }) - .map(|x| x.0) - .collect::>(); + } - // TEST: A single is only full-footstooled by itself. - let full_footstools = footstool_results - .iter() - .filter(|&&x| x == Footstool::Full) - .count(); - assert!(full_footstools == 1); + // TEST: The only Single that doesn't have a half footstool is 3[D] + { + let card = Single::new(Card::from(0)).unwrap(); + assert_eq!( + counter.get(&card), + None, + "Expected {card} to not have any half footstools." + ); + } + } - // TEST: A single is half-footstooled by at most one single. - let half_footstools = footstool_results - .iter() - .filter(|&&x| x == Footstool::Half) - .count(); - assert!(half_footstools <= 1); + // TEST: Non-footstools + { + // A little combinatorial check. 3[D] has no half footstools and 1 + // full footstool (itself). Every other card should have 1 unique + // half footstool and 1 full footstool. - // TEST: A single is not footstooled by any other singles. - let non_footstools = footstool_results - .iter() - .filter(|&&x| x == Footstool::None) - .count(); - assert!( - non_footstools < singles.len() - && non_footstools >= singles.len() - 3 + // 51 cards should satisfy the latter condition => 102 instances of + // a half or full footstool. With 3[D], that's 103 instances of a + // footstool. + + // If the above conditions hold, then we'd expect the number of non + // footstools in our exhaustive set to be (52 ** 2) - 103. + + assert_eq!( + exhaustive_singles_footstool + .iter() + .filter(|(_, _, footstool)| *footstool == Footstool::None) + .count(), + (52 * 52) - 103 ); } } #[test] fn footstool_deck_irrelevance() { - // For a fixed Single, 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 a fixed Single, comparing to another deck's cards doesn't change + // if it gets footstooled. + let piv_card = Card::make_playing_card(Rank::Three, Suit::Club); + let pivot = Single::new(piv_card).unwrap(); for i in 1..10 { let piv_copy = Single(Card::PlayingCard(PlayingCard { deck: i, - ..pivot.0.playing_card().unwrap() + ..piv_card.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)); - // TEST: a single may be footstooled by a single from another deck with - // the same rank and suit. + // TEST: a single may be footstooled by a single from another deck + // with the same rank and suit. let (piv_on_piv_copy, _) = test_footstool(&pivot, &piv_copy); - assert!(piv_on_piv_copy == Footstool::Full); + assert_eq!( + piv_on_piv_copy, + Footstool::Full, + "Expected {pivot}, {piv_copy} to full footstool." + ); - // TEST: A single may be half footstooled by singles from another deck. + // TEST: A single may be half footstooled by singles from another + // deck. let (piv_on_piv_before, _) = test_footstool(&pivot, &piv_before); - assert!(piv_on_piv_before == Footstool::Half); + assert_eq!( + piv_on_piv_before, + Footstool::Half, + "Expected {pivot}, {piv_before} to half footstool." + ); let (_, piv_after_on_piv) = test_footstool(&pivot, &piv_after); - assert!(piv_after_on_piv == Footstool::Half); + assert_eq!( + piv_after_on_piv, + Footstool::Half, + "Expected {pivot}, {piv_after} to half footstool." + ); // TEST: A single is still not footstooled by singles from other // decks that aren't adjacent. let (piv_on_piv_way_after, _) = test_footstool(&pivot, &piv_way_after); - assert!(piv_on_piv_way_after == Footstool::None); + assert_eq!( + piv_on_piv_way_after, + Footstool::None, + "Expected {pivot}, {piv_way_after} to not footstool." + ); } } }