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::{Competitive, Constructive, Defensive, Family, Pair, System};
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
}
struct Constant(f32);
impl System for Constant {
fn classify(&self, _: Hand, _: RelativeVulnerability, _: &[Call]) -> Option<Logits> {
Some(marker_logits(self.0))
}
}
struct VulProbe;
impl System for VulProbe {
fn classify(&self, _: Hand, vul: RelativeVulnerability, _: &[Call]) -> Option<Logits> {
Some(marker_logits(f32::from(vul.bits())))
}
}
struct Silent;
impl System for Silent {
fn classify(&self, _: Hand, _: RelativeVulnerability, _: &[Call]) -> Option<Logits> {
None
}
}
struct NoMass;
impl System for NoMass {
fn classify(&self, _: Hand, _: RelativeVulnerability, _: &[Call]) -> Option<Logits> {
Some(Logits::new())
}
}
fn classify_marker(system: &impl System, auction: &[Call]) -> Option<f32> {
system
.classify(Hand::default(), RelativeVulnerability::NONE, auction)
.map(|logits| *logits.0.get(Call::Pass))
}
fn assert_marker_eq(actual: f32, expected: f32) {
assert!((actual - expected).abs() <= f32::EPSILON);
}
#[test]
fn test_stance_routes_by_phase() {
let one_s = bid(1, Strain::Spades);
let two_h = bid(2, Strain::Hearts);
let mut constructive = Constructive::new();
constructive.insert(&[], classifier(|_, _| marker_logits(1.0)));
constructive.insert(&[one_s, Call::Pass], classifier(|_, _| marker_logits(2.0)));
let mut competitive = Competitive::new();
competitive.insert(&[one_s, two_h], classifier(|_, _| marker_logits(4.0)));
let mut defensive = Defensive::new();
defensive.insert(&[one_s], classifier(|_, _| marker_logits(3.0)));
let stance =
Pair::new(Family::NATURAL, constructive, competitive, defensive).against(Family::NATURAL);
assert_eq!(classify_marker(&stance, &[]), Some(1.0));
assert_eq!(classify_marker(&stance, &[one_s, Call::Pass]), Some(2.0));
assert_eq!(classify_marker(&stance, &[one_s, two_h]), Some(4.0));
assert_eq!(classify_marker(&stance, &[one_s]), Some(3.0));
}
#[test]
fn test_books_gate_on_phase() {
let one_s = bid(1, Strain::Spades);
let two_h = bid(2, Strain::Hearts);
let mut constructive = Constructive::new();
constructive.insert(&[one_s], classifier(|_, _| marker_logits(1.0)));
constructive.insert(&[one_s, two_h], classifier(|_, _| marker_logits(1.5)));
assert_eq!(classify_marker(&constructive, &[one_s]), None);
assert_eq!(classify_marker(&constructive, &[one_s, two_h]), None);
let mut competitive = Competitive::new();
competitive.insert(&[], classifier(|_, _| marker_logits(2.0)));
competitive.insert(&[one_s], classifier(|_, _| marker_logits(2.5)));
competitive.insert(&[one_s, two_h], classifier(|_, _| marker_logits(3.0)));
assert_eq!(classify_marker(&competitive, &[]), None);
assert_eq!(classify_marker(&competitive, &[one_s]), None);
assert_eq!(classify_marker(&competitive, &[one_s, two_h]), Some(3.0));
let mut defensive = Defensive::new();
defensive.insert(&[], classifier(|_, _| marker_logits(4.0)));
assert_eq!(classify_marker(&defensive, &[]), None);
}
#[test]
fn test_family_override_selects_book() {
let one_c = bid(1, Strain::Clubs);
let mut natural_defense = Defensive::new();
natural_defense.insert(&[one_c], classifier(|_, _| marker_logits(1.0)));
let mut special_defense = Defensive::new();
special_defense.insert(&[one_c], classifier(|_, _| marker_logits(2.0)));
let pair = Pair::new(
Family::NATURAL,
Constructive::new(),
Competitive::new(),
natural_defense,
)
.defensive_vs(Family::POLISH_CLUB, special_defense);
assert_eq!(
classify_marker(&pair.against(Family::NATURAL), &[one_c]),
Some(1.0)
);
assert_eq!(
classify_marker(&pair.against(Family::POLISH_CLUB), &[one_c]),
Some(2.0)
);
assert_eq!(
classify_marker(&pair.against(Family::STRONG_CLUB), &[one_c]),
Some(1.0)
);
}
#[test]
fn test_versus_dispatches_by_parity() {
let table = Constant(1.0).vs(Constant(2.0));
let passes = [Call::Pass; 3];
assert_eq!(classify_marker(&table, &passes[..0]), Some(1.0));
assert_eq!(classify_marker(&table, &passes[..1]), Some(2.0));
assert_eq!(classify_marker(&table, &passes[..2]), Some(1.0));
assert_eq!(classify_marker(&table, &passes[..3]), Some(2.0));
}
#[test]
fn test_versus_passes_vulnerability_through() {
let table = VulProbe.vs(VulProbe);
let passes = [Call::Pass; 2];
for vul in [
RelativeVulnerability::NONE,
RelativeVulnerability::WE,
RelativeVulnerability::THEY,
RelativeVulnerability::ALL,
] {
for len in 0..=2 {
let logits = table
.classify(Hand::default(), vul, &passes[..len])
.expect("probe always answers");
assert_marker_eq(*logits.0.get(Call::Pass), f32::from(vul.bits()));
}
}
}
#[test]
fn test_versus_borrows_without_cloning() {
let system = Constant(1.0);
let table = (&system).vs(&system);
assert_eq!(classify_marker(&table, &[]), Some(1.0));
assert_eq!(classify_marker(&table, &[Call::Pass]), Some(1.0));
}
#[test]
fn test_or_else_falls_through_on_none() {
let layered = Silent.or_else(Constant(9.0));
assert_eq!(classify_marker(&layered, &[]), Some(9.0));
}
#[test]
fn test_or_else_falls_through_on_no_mass() {
let layered = NoMass.or_else(Constant(9.0));
assert_eq!(classify_marker(&layered, &[]), Some(9.0));
}
#[test]
fn test_or_else_prefers_first_answer_with_mass() {
let layered = Constant(1.0).or_else(Constant(9.0));
assert_eq!(classify_marker(&layered, &[]), Some(1.0));
}