modes:single:tests: major refactor

Better exhaustive testing, addition of messages on asserts as well.
This commit is contained in:
2026-04-05 04:15:09 +01:00
committed by oreodave
parent b796156ea5
commit abf638e869

View File

@@ -24,16 +24,13 @@ impl Hand for Single {
} }
fn footstool(&self, other: &Self) -> Footstool { fn footstool(&self, other: &Self) -> Footstool {
let self_abs = self.0.deck_abs(); // We use deck_abs() to get an index in the overall deck ordering.
let other_abs = other.0.deck_abs(); match (self.0.deck_abs(), other.0.deck_abs()) {
// A full footstool only occurs when both are the same.
// Trivial implementation (x, y) if x == y => Footstool::Full,
if self_abs == other_abs { // Half footstools can only occur when x is consecutive to y.
Footstool::Full (x, y) if x == y + 1 => Footstool::Half,
} else if self_abs == (other_abs + 1) % 52 { _ => Footstool::None,
Footstool::Half
} else {
Footstool::None
} }
} }
} }
@@ -47,129 +44,219 @@ impl Display for Single {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::{HashMap, HashSet};
use super::*; use super::*;
use crate::{ use crate::{
card::{make_decks, PlayingCard, Rank, Suit}, card::{PlayingCard, Rank, Suit},
modes::tests::test_footstool, modes::tests::test_footstool,
}; };
#[test] #[test]
fn new() { fn new() {
// TEST: Jokers are not valid singles. // 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 = let valid_singles = Card::iter_all(1)
make_decks(1).filter_map(Single::new).collect::<Vec<_>>(); .filter_map(Single::new)
let deck = make_decks(1).collect::<Vec<_>>(); .collect::<Vec<_>>();
let deck = Card::iter_all(1).collect::<Vec<_>>();
// TEST: Only two cards in a single deck aren't valid singles. // TEST: Only two cards in a single deck aren't valid singles.
assert!(valid_singles.len() == deck.len() - 2); assert_eq!(valid_singles.len(), deck.len() - 2);
// TEST: All valid singles are playing cards.
assert!(valid_singles.iter().all(|Single(card)| !card.is_joker()));
} }
#[test] #[test]
fn footstool() { fn footstool() {
// Make a deck with no jokers. // Create a vector for all possible Singles in 1 deck of cards. Due to
let singles = PlayingCard::iter_deck(0) // ordering of Card::iter_all, we expect this to be sorted as well.
.map(Card::PlayingCard) let singles = Card::iter_all(1)
.filter_map(Single::new) .filter_map(Single::new)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// TEST: Consecutive singles footstool testing.
singles.windows(3).for_each(|single_slice| { singles.windows(3).for_each(|single_slice| {
let (s1, s2, s3) = let (s1, s2, s3) =
(single_slice[0], single_slice[1], single_slice[2]); (single_slice[0], single_slice[1], single_slice[2]);
// TEST: A single is always full footstooled by itself. // TEST: Test footstool patterns and get some results back for
assert!(s1.footstool(&s1) == Footstool::Full); // further testing.
// TEST: non-reflexivity of footstool on neighbours.
let (_, s2_on_s1) = test_footstool(&s1, &s2); let (_, s2_on_s1) = test_footstool(&s1, &s2);
let (_, s3_on_s2) = test_footstool(&s2, &s3); let (_, s3_on_s2) = test_footstool(&s2, &s3);
let (s1_on_s3, s3_on_s1) = test_footstool(&s1, &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 // TEST: s2 is half-footstooled by s3, and s1 is half footstooled by
// s2. // s2.
assert!(s3_on_s2 == Footstool::Half); assert!(
assert!(s2_on_s1 == Footstool::Half); 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. // TEST: s1 does not footstool whatsoever with s3.
assert!(s1_on_s3 == Footstool::None); assert!(
assert!(s3_on_s1 == Footstool::None); 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 { // Exhaustive testing over every possible combinations of Singles.
let footstool_results = singles
.iter() // Create an exhaustive map for all combinations (Single, Single) along
.map(|&other_single| { // with the results of the first footstooling the second.
// TEST: All footstool results are non-reflexive. let exhaustive_singles_footstool = singles
test_footstool(single, &other_single) .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::<Vec<_>>();
// TEST: Half footstools.
{
// Maps Singles to a Vector of the Singles they half footstool.
let counter = {
let mut counter: HashMap<Single, Vec<Single>> = 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::<Single>::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::<Vec<_>>();
// TEST: A single is only full-footstooled by itself. // TEST: The only Single that doesn't have a half footstool is 3[D]
let full_footstools = footstool_results {
.iter() let card = Single::new(Card::from(0)).unwrap();
.filter(|&&x| x == Footstool::Full) assert_eq!(
.count(); counter.get(&card),
assert!(full_footstools == 1); None,
"Expected {card} to not have any half footstools."
);
}
}
// TEST: A single is half-footstooled by at most one single. // TEST: Non-footstools
let half_footstools = footstool_results {
.iter() // A little combinatorial check. 3[D] has no half footstools and 1
.filter(|&&x| x == Footstool::Half) // full footstool (itself). Every other card should have 1 unique
.count(); // half footstool and 1 full footstool.
assert!(half_footstools <= 1);
// TEST: A single is not footstooled by any other singles. // 51 cards should satisfy the latter condition => 102 instances of
let non_footstools = footstool_results // a half or full footstool. With 3[D], that's 103 instances of a
.iter() // footstool.
.filter(|&&x| x == Footstool::None)
.count(); // If the above conditions hold, then we'd expect the number of non
assert!( // footstools in our exhaustive set to be (52 ** 2) - 103.
non_footstools < singles.len()
&& non_footstools >= singles.len() - 3 assert_eq!(
exhaustive_singles_footstool
.iter()
.filter(|(_, _, footstool)| *footstool == Footstool::None)
.count(),
(52 * 52) - 103
); );
} }
} }
#[test] #[test]
fn footstool_deck_irrelevance() { fn footstool_deck_irrelevance() {
// For a fixed Single, comparing to another deck's cards doesn't change if // For a fixed Single, comparing to another deck's cards doesn't change
// it gets footstooled. // if it gets footstooled.
let pivot = PlayingCard::new(0, Rank::Three, Suit::Club); let piv_card = Card::make_playing_card(Rank::Three, Suit::Club);
let pivot = Card::PlayingCard(pivot); let pivot = Single::new(piv_card).unwrap();
let pivot = Single(pivot);
for i in 1..10 { for i in 1..10 {
let piv_copy = Single(Card::PlayingCard(PlayingCard { let piv_copy = Single(Card::PlayingCard(PlayingCard {
deck: i, 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_before = Single(Card::from(i64::from(piv_copy.0) - 1));
let piv_after = 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)); 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 // TEST: a single may be footstooled by a single from another deck
// the same rank and suit. // with the same rank and suit.
let (piv_on_piv_copy, _) = test_footstool(&pivot, &piv_copy); 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); 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); 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 // TEST: A single is still not footstooled by singles from other
// decks that aren't adjacent. // decks that aren't adjacent.
let (piv_on_piv_way_after, _) = let (piv_on_piv_way_after, _) =
test_footstool(&pivot, &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."
);
} }
} }
} }