use contract_bridge::auction::{Call, RelativeVulnerability};
use contract_bridge::{Bid, Hand, Level, Strain};
use pons::bidding::array::Logits;
use pons::bidding::trie::classifier;
use pons::bidding::{Context, System, Trie};
const fn bid(level: u8, strain: Strain) -> Call {
Call::Bid(Bid {
level: Level::new(level),
strain,
})
}
const fn marker_logits(value: f32) -> Logits {
let mut logits = Logits::new();
*logits.0.get_mut(Call::Pass) = value;
logits
}
const fn just_pass() -> Logits {
marker_logits(0.0)
}
fn classify_at(trie: &Trie, auction: &[Call]) -> Logits {
let f = trie
.get(auction)
.expect("classifier missing at exact auction");
let context = Context::new(RelativeVulnerability::NONE, auction)
.with_prefixes(trie.common_prefixes(auction));
f.classify(Hand::default(), &context)
}
#[test]
fn test_trie_new_is_empty() {
let trie = Trie::new();
assert!(trie.is_prefix(&[]));
assert!(trie.get(&[]).is_none());
assert!(trie.iter().next().is_none());
assert!(trie.longest_prefix(&[]).is_none());
}
#[test]
fn test_trie_default_constructs() {
let trie = Trie::default();
assert!(trie.iter().next().is_none());
}
#[test]
fn test_trie_insert_and_get_round_trip() {
let mut trie = Trie::new();
let auction = [bid(1, Strain::Clubs)];
trie.insert(&auction, classifier(|_, _| marker_logits(1.0)));
assert_eq!(classify_at(&trie, &auction), marker_logits(1.0));
}
#[test]
fn test_trie_insert_returns_previous_when_overwriting() {
let mut trie = Trie::new();
let auction = [bid(1, Strain::Clubs)];
assert!(
trie.insert(&auction, classifier(|_, _| marker_logits(1.0)))
.is_none()
);
assert!(
trie.insert(&auction, classifier(|_, _| marker_logits(2.0)))
.is_some()
);
assert_eq!(classify_at(&trie, &auction), marker_logits(2.0));
}
#[test]
fn test_trie_is_prefix() {
let mut trie = Trie::new();
let auction = [bid(1, Strain::Clubs), Call::Pass, bid(1, Strain::Hearts)];
trie.insert(&auction, classifier(|_, _| just_pass()));
assert!(trie.is_prefix(&[]));
assert!(trie.is_prefix(&auction[..1]));
assert!(trie.is_prefix(&auction[..2]));
assert!(trie.is_prefix(&auction));
assert!(!trie.is_prefix(&[bid(1, Strain::Spades)]));
assert!(!trie.is_prefix(&[bid(1, Strain::Clubs), bid(2, Strain::Clubs)]));
}
#[test]
fn test_trie_longest_prefix_picks_deepest_match() {
let mut trie = Trie::new();
let one_c = bid(1, Strain::Clubs);
let one_h = bid(1, Strain::Hearts);
let one_d = bid(1, Strain::Diamonds);
trie.insert(&[], classifier(|_, _| marker_logits(0.0)));
trie.insert(&[one_c], classifier(|_, _| marker_logits(1.0)));
trie.insert(
&[one_c, Call::Pass, one_h],
classifier(|_, _| marker_logits(2.0)),
);
let query = [one_c, Call::Pass, one_d];
let (prefix, _) = trie.longest_prefix(&query).expect("expected a match");
assert_eq!(prefix, &[one_c]);
let exact = [one_c, Call::Pass, one_h];
let (prefix, _) = trie.longest_prefix(&exact).expect("expected a match");
assert_eq!(prefix, &exact[..]);
let (prefix, _) = trie.longest_prefix(&[]).expect("expected root match");
assert!(prefix.is_empty());
}
#[test]
fn test_trie_longest_prefix_no_match_returns_none() {
let trie = Trie::new();
assert!(trie.longest_prefix(&[bid(1, Strain::Clubs)]).is_none());
}
#[test]
fn test_trie_longest_prefix_skips_empty_intermediate() {
let mut trie = Trie::new();
let one_c = bid(1, Strain::Clubs);
let one_h = bid(1, Strain::Hearts);
let auction = [one_c, Call::Pass, one_h];
trie.insert(&auction, classifier(|_, _| just_pass()));
let (prefix, _) = trie
.longest_prefix(&auction)
.expect("expected deepest match");
assert_eq!(prefix, &auction[..]);
assert!(trie.longest_prefix(&[bid(1, Strain::Spades)]).is_none());
}
#[test]
fn test_trie_suffixes_enumerate_with_correct_paths() {
let mut trie = Trie::new();
let one_c = bid(1, Strain::Clubs);
let one_h = bid(1, Strain::Hearts);
trie.insert(&[one_c], classifier(|_, _| marker_logits(1.0)));
trie.insert(
&[one_c, Call::Pass, one_h],
classifier(|_, _| marker_logits(2.0)),
);
trie.insert(
&[bid(1, Strain::Spades)],
classifier(|_, _| marker_logits(3.0)),
);
let suffixes: Vec<Box<[Call]>> = trie.suffixes(&[one_c]).map(|(s, _)| s).collect();
assert_eq!(suffixes.len(), 2);
assert!(suffixes.iter().any(|s| s.is_empty()));
assert!(suffixes.iter().any(|s| **s == [Call::Pass, one_h]));
}
#[test]
fn test_trie_suffixes_empty_when_prefix_absent() {
let trie = Trie::new();
assert_eq!(trie.suffixes(&[bid(1, Strain::Clubs)]).count(), 0);
}
#[test]
fn test_trie_suffixes_isolated_to_subtree() {
let mut trie = Trie::new();
trie.insert(&[bid(1, Strain::Clubs)], classifier(|_, _| just_pass()));
trie.insert(&[bid(1, Strain::Hearts)], classifier(|_, _| just_pass()));
assert_eq!(trie.suffixes(&[bid(1, Strain::Clubs)]).count(), 1);
}
#[test]
fn test_trie_suffixes_is_fused() {
let trie = Trie::new();
let mut iter = trie.suffixes(&[]);
assert!(iter.next().is_none());
assert!(iter.next().is_none());
assert!(iter.next().is_none());
}
#[test]
fn test_trie_iter_visits_every_classifier() {
let mut trie = Trie::new();
trie.insert(&[bid(1, Strain::Clubs)], classifier(|_, _| just_pass()));
trie.insert(&[bid(1, Strain::Hearts)], classifier(|_, _| just_pass()));
trie.insert(
&[bid(1, Strain::Hearts), Call::Pass],
classifier(|_, _| just_pass()),
);
assert_eq!(trie.iter().count(), 3);
}
#[test]
fn test_trie_common_prefixes_returns_ancestors_with_classifiers() {
let mut trie = Trie::new();
let one_c = bid(1, Strain::Clubs);
let one_h = bid(1, Strain::Hearts);
trie.insert(&[], classifier(|_, _| marker_logits(0.0)));
trie.insert(&[one_c], classifier(|_, _| marker_logits(1.0)));
trie.insert(
&[one_c, Call::Pass, one_h],
classifier(|_, _| marker_logits(2.0)),
);
let query = [one_c, Call::Pass, one_h];
let prefixes: Vec<Vec<Call>> = trie
.common_prefixes(&query)
.map(|(p, _)| p.to_vec())
.collect();
assert_eq!(prefixes.len(), 3);
assert_eq!(prefixes[0], Vec::<Call>::new());
assert_eq!(prefixes[1], vec![one_c]);
assert_eq!(prefixes[2], vec![one_c, Call::Pass, one_h]);
}
#[test]
fn test_trie_common_prefixes_skips_uncovered_intermediate() {
let mut trie = Trie::new();
let one_c = bid(1, Strain::Clubs);
let one_h = bid(1, Strain::Hearts);
trie.insert(&[one_c, Call::Pass, one_h], classifier(|_, _| just_pass()));
let query = [one_c, Call::Pass, one_h];
let prefixes: Vec<Vec<Call>> = trie
.common_prefixes(&query)
.map(|(p, _)| p.to_vec())
.collect();
assert_eq!(prefixes, vec![vec![one_c, Call::Pass, one_h]]);
}
#[test]
fn test_trie_common_prefixes_empty_when_diverges() {
let mut trie = Trie::new();
trie.insert(&[bid(1, Strain::Clubs)], classifier(|_, _| just_pass()));
let query = [bid(1, Strain::Spades)];
assert_eq!(trie.common_prefixes(&query).count(), 0);
}
#[test]
fn test_pass_everything_classifies_root() {
let mut trie = Trie::new();
trie.insert(&[], classifier(|_, _| just_pass()));
let result = trie.classify(Hand::default(), RelativeVulnerability::NONE, &[]);
assert_eq!(result, Some(just_pass()));
}
#[test]
fn test_system_trie_returns_none_for_unknown_auction() {
let trie = Trie::new();
let result = trie.classify(Hand::default(), RelativeVulnerability::NONE, &[]);
assert!(result.is_none());
}
#[test]
fn test_merge_disjoint_fragments() {
let one_h = bid(1, Strain::Hearts);
let one_s = bid(1, Strain::Spades);
let mut core = Trie::new();
core.insert(&[one_h], classifier(|_, _| marker_logits(1.0)));
let mut package = Trie::new();
package.insert(&[one_s], classifier(|_, _| marker_logits(2.0)));
package.insert(&[one_h, Call::Pass], classifier(|_, _| marker_logits(3.0)));
assert!(core.merge(package).is_empty());
assert_eq!(classify_at(&core, &[one_h]), marker_logits(1.0));
assert_eq!(classify_at(&core, &[one_s]), marker_logits(2.0));
assert_eq!(classify_at(&core, &[one_h, Call::Pass]), marker_logits(3.0));
}
#[test]
fn test_merge_reports_collisions_and_keeps_self() {
let one_h = bid(1, Strain::Hearts);
let mut core = Trie::new();
core.insert(&[one_h], classifier(|_, _| marker_logits(1.0)));
let mut other = Trie::new();
other.insert(&[one_h], classifier(|_, _| marker_logits(2.0)));
other.insert(&[one_h, Call::Pass], classifier(|_, _| marker_logits(3.0)));
let collisions = core.merge(other);
assert_eq!(collisions.len(), 1);
assert_eq!(&*collisions[0], &[one_h]);
assert_eq!(classify_at(&core, &[one_h]), marker_logits(1.0));
assert_eq!(classify_at(&core, &[one_h, Call::Pass]), marker_logits(3.0));
}
#[test]
fn test_merge_concatenates_fallbacks_self_first() {
use pons::bidding::fallback::{Always, Fallback};
let mut core = Trie::new();
core.fallback_at(
&[],
Always,
Fallback::classify(classifier(|_, _| marker_logits(1.0))),
);
let mut other = Trie::new();
other.fallback_at(
&[],
Always,
Fallback::classify(classifier(|_, _| marker_logits(2.0))),
);
core.merge(other);
let result = core.classify(
Hand::default(),
RelativeVulnerability::NONE,
&[bid(2, Strain::Clubs)],
);
assert_eq!(result, Some(marker_logits(1.0)));
}