use super::context::Context;
use contract_bridge::eval::{self, HandEvaluator, SimpleEvaluator};
use contract_bridge::{Hand, Holding, Level, Rank, Strain, Suit};
use core::ops::{BitAnd, BitOr, Not, RangeBounds};
pub trait Constraint: Send + Sync {
fn eval(&self, hand: Hand, context: &Context<'_>) -> f32;
}
impl<F> Constraint for F
where
F: Fn(Hand, &Context<'_>) -> f32 + Send + Sync,
{
fn eval(&self, hand: Hand, context: &Context<'_>) -> f32 {
self(hand, context)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Cons<T>(
pub T,
);
impl<T: Constraint> Constraint for Cons<T> {
fn eval(&self, hand: Hand, context: &Context<'_>) -> f32 {
self.0.eval(hand, context)
}
}
#[derive(Clone, Copy, Debug)]
pub struct And<A, B>(A, B);
impl<A: Constraint, B: Constraint> Constraint for And<A, B> {
fn eval(&self, hand: Hand, context: &Context<'_>) -> f32 {
self.0.eval(hand, context) + self.1.eval(hand, context)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Or<A, B>(A, B);
impl<A: Constraint, B: Constraint> Constraint for Or<A, B> {
fn eval(&self, hand: Hand, context: &Context<'_>) -> f32 {
self.0.eval(hand, context).max(self.1.eval(hand, context))
}
}
#[derive(Clone, Copy, Debug)]
pub struct Flip<T>(T);
impl<T: Constraint> Constraint for Flip<T> {
fn eval(&self, hand: Hand, context: &Context<'_>) -> f32 {
crisp(self.0.eval(hand, context) == f32::NEG_INFINITY)
}
}
impl<A, B> BitAnd<Cons<B>> for Cons<A> {
type Output = Cons<And<A, B>>;
fn bitand(self, rhs: Cons<B>) -> Self::Output {
Cons(And(self.0, rhs.0))
}
}
impl<A, B> BitOr<Cons<B>> for Cons<A> {
type Output = Cons<Or<A, B>>;
fn bitor(self, rhs: Cons<B>) -> Self::Output {
Cons(Or(self.0, rhs.0))
}
}
impl<A> Not for Cons<A> {
type Output = Cons<Flip<A>>;
fn not(self) -> Self::Output {
Cons(Flip(self.0))
}
}
const fn crisp(condition: bool) -> f32 {
if condition { 0.0 } else { f32::NEG_INFINITY }
}
pub fn pred<F>(condition: F) -> Cons<impl Constraint + Clone>
where
F: Fn(Hand, &Context<'_>) -> bool + Clone + Send + Sync,
{
Cons(move |hand: Hand, context: &Context<'_>| crisp(condition(hand, context)))
}
#[must_use]
pub fn hcp(range: impl RangeBounds<u8> + Clone + Send + Sync) -> Cons<impl Constraint + Clone> {
pred(move |hand: Hand, _: &Context<'_>| {
range.contains(&SimpleEvaluator(eval::hcp::<u8>).eval(hand))
})
}
pub fn len(
suit: Suit,
range: impl RangeBounds<usize> + Clone + Send + Sync,
) -> Cons<impl Constraint + Clone> {
pred(move |hand: Hand, _: &Context<'_>| range.contains(&hand[suit].len()))
}
#[must_use]
pub fn balanced() -> Cons<impl Constraint + Clone> {
pred(|hand: Hand, _: &Context<'_>| {
let lengths = Suit::ASC.map(|suit| hand[suit].len());
lengths.iter().all(|&length| length >= 2)
&& lengths.iter().filter(|&&length| length == 2).count() <= 1
})
}
#[must_use]
pub fn nltc_at_most(losers: f64) -> Cons<impl Constraint + Clone> {
pred(move |hand: Hand, _: &Context<'_>| eval::NLTC.eval(hand) <= losers)
}
pub fn support(
range: impl RangeBounds<usize> + Clone + Send + Sync,
) -> Cons<impl Constraint + Clone> {
pred(move |hand: Hand, context: &Context<'_>| {
context
.partner_last_suit()
.is_some_and(|suit| range.contains(&hand[suit].len()))
})
}
pub fn top_honors(
suit: Suit,
range: impl RangeBounds<usize> + Clone + Send + Sync,
) -> Cons<impl Constraint + Clone> {
pred(move |hand: Hand, _: &Context<'_>| {
let holding = hand[suit];
let count = [Rank::A, Rank::K, Rank::Q]
.into_iter()
.filter(|&rank| holding.contains(rank))
.count();
range.contains(&count)
})
}
const fn has_stopper(holding: Holding) -> bool {
holding.contains(Rank::A)
|| (holding.contains(Rank::K) && holding.len() >= 2)
|| (holding.contains(Rank::Q) && holding.len() >= 3)
|| (holding.contains(Rank::J) && holding.len() >= 4)
}
#[must_use]
pub fn stopper_in(suit: Suit) -> Cons<impl Constraint + Clone> {
pred(move |hand: Hand, _: &Context<'_>| has_stopper(hand[suit]))
}
#[must_use]
pub fn stopper_in_their_suits() -> Cons<impl Constraint + Clone> {
pred(|hand: Hand, context: &Context<'_>| {
context.their_suits().all(|suit| has_stopper(hand[suit]))
})
}
#[must_use]
pub fn they_bid(strain: Strain) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| context.they_bid(strain))
}
#[must_use]
pub fn short_in_their_suits() -> Cons<impl Constraint + Clone> {
pred(|hand: Hand, context: &Context<'_>| {
context.their_suits().all(|suit| hand[suit].len() <= 3)
})
}
#[must_use]
pub fn partner_suit_is(suit: Suit) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| context.partner_last_suit() == Some(suit))
}
#[must_use]
pub fn min_level_is(level: u8, strain: Strain) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| context.min_level(strain) == Some(Level::new(level)))
}
#[must_use]
pub fn passed_hand() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| context.passed_hand())
}
#[must_use]
pub fn undisturbed() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| context.undisturbed())
}
#[must_use]
pub fn vulnerable() -> Cons<impl Constraint + Clone> {
use contract_bridge::auction::RelativeVulnerability;
pred(|_: Hand, context: &Context<'_>| context.vul().contains(RelativeVulnerability::WE))
}
#[must_use]
pub fn they_vulnerable() -> Cons<impl Constraint + Clone> {
use contract_bridge::auction::RelativeVulnerability;
pred(|_: Hand, context: &Context<'_>| context.vul().contains(RelativeVulnerability::THEY))
}
#[must_use]
pub fn nth_seat(seat: u8) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| context.seat_to_open() == Some(seat))
}
#[cfg(test)]
mod tests {
use super::*;
use contract_bridge::auction::{Call, RelativeVulnerability};
use contract_bridge::{Bid, Strain};
const BALANCED_15: &str = "AKQ2.K53.QJ4.T92";
fn hand(s: &str) -> Hand {
s.parse().expect("valid test hand")
}
fn empty_context() -> Context<'static> {
Context::new(RelativeVulnerability::NONE, &[])
}
fn assert_pass(logit: f32) {
assert!(logit.is_finite() && logit.abs() <= f32::EPSILON);
}
fn assert_reject(logit: f32) {
assert!(logit.is_infinite() && logit.is_sign_negative());
}
#[test]
fn test_hcp_and_balanced() {
let context = empty_context();
assert_pass(hcp(15..=17).eval(hand(BALANCED_15), &context));
assert_reject(hcp(16..).eval(hand(BALANCED_15), &context));
assert_pass(balanced().eval(hand(BALANCED_15), &context));
assert_reject(balanced().eval(hand("AKQJ2.K543.QJ4.2"), &context));
}
#[test]
fn test_combinators() {
let context = empty_context();
let strong_notrump = hcp(15..=17) & balanced();
assert_pass(strong_notrump.eval(hand(BALANCED_15), &context));
let either = hcp(16..) | len(Suit::Spades, 4..);
assert_pass(either.eval(hand(BALANCED_15), &context));
let neither = hcp(16..) | len(Suit::Spades, 5..);
assert_reject(neither.eval(hand(BALANCED_15), &context));
assert_reject((!balanced()).eval(hand(BALANCED_15), &context));
assert_pass((!hcp(16..)).eval(hand(BALANCED_15), &context));
}
#[test]
fn test_support_and_stoppers() {
let auction = [
Call::Bid(Bid::new(1, Strain::Diamonds)),
Call::Bid(Bid::new(1, Strain::Hearts)),
Call::Pass,
];
let context = Context::new(RelativeVulnerability::NONE, &auction);
assert_pass(support(3..).eval(hand(BALANCED_15), &context));
assert_reject(support(4..).eval(hand(BALANCED_15), &context));
assert_pass(stopper_in_their_suits().eval(hand(BALANCED_15), &context));
assert_reject(stopper_in_their_suits().eval(hand("AKQ2.K53.T92.QJ4"), &context));
}
#[test]
fn test_support_without_partner_suit() {
let context = empty_context();
assert_reject(support(0..).eval(hand(BALANCED_15), &context));
}
#[test]
fn test_top_honors_and_stopper_in() {
let context = empty_context();
assert_pass(top_honors(Suit::Spades, 3..).eval(hand(BALANCED_15), &context));
assert_pass(top_honors(Suit::Hearts, 1..=1).eval(hand(BALANCED_15), &context));
assert_reject(top_honors(Suit::Clubs, 1..).eval(hand(BALANCED_15), &context));
assert_pass(stopper_in(Suit::Hearts).eval(hand(BALANCED_15), &context));
assert_reject(stopper_in(Suit::Clubs).eval(hand(BALANCED_15), &context));
}
#[test]
fn test_partner_suit_and_min_level() {
let auction = [
Call::Bid(Bid::new(1, Strain::Diamonds)),
Call::Bid(Bid::new(1, Strain::Hearts)),
Call::Pass,
];
let context = Context::new(RelativeVulnerability::NONE, &auction);
assert_pass(partner_suit_is(Suit::Hearts).eval(hand(BALANCED_15), &context));
assert_reject(partner_suit_is(Suit::Spades).eval(hand(BALANCED_15), &context));
assert_pass(min_level_is(1, Strain::Spades).eval(hand(BALANCED_15), &context));
assert_pass(min_level_is(2, Strain::Diamonds).eval(hand(BALANCED_15), &context));
assert_reject(min_level_is(2, Strain::Spades).eval(hand(BALANCED_15), &context));
}
#[test]
fn test_vulnerability_and_seats() {
let auction = [Call::Pass];
let context = Context::new(RelativeVulnerability::WE, &auction);
assert_pass(vulnerable().eval(hand(BALANCED_15), &context));
assert_reject(they_vulnerable().eval(hand(BALANCED_15), &context));
assert_pass(nth_seat(2).eval(hand(BALANCED_15), &context));
assert_reject(nth_seat(1).eval(hand(BALANCED_15), &context));
}
}