use super::context::Context;
use super::inference::{Inference, Inferences, Range};
use contract_bridge::eval::{self, HandEvaluator, SimpleEvaluator};
use contract_bridge::{Hand, Holding, Level, Rank, Strain, Suit};
use core::cell::Cell;
use core::fmt;
use core::ops::{BitAnd, BitOr, Bound, Not, RangeBounds};
use std::borrow::Cow;
pub trait Constraint: Send + Sync {
fn eval(&self, hand: Hand, context: &Context<'_>) -> f32;
fn describe(&self) -> Description {
Description::Opaque
}
fn project(&self, _context: &Context<'_>) -> Inference {
Inference::unknown()
}
}
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)
}
fn describe(&self) -> Description {
self.0.describe()
}
fn project(&self, context: &Context<'_>) -> Inference {
self.0.project(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)
}
fn describe(&self) -> Description {
self.0.describe().and(self.1.describe())
}
fn project(&self, context: &Context<'_>) -> Inference {
self.0.project(context).intersect(&self.1.project(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))
}
fn describe(&self) -> Description {
self.0.describe().or(self.1.describe())
}
fn project(&self, context: &Context<'_>) -> Inference {
self.0.project(context).union(&self.1.project(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)
}
fn describe(&self) -> Description {
self.0.describe().negate()
}
}
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 }
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Description {
Atom(Cow<'static, str>),
All(Vec<Description>),
Any(Vec<Description>),
Not(Box<Description>),
Opaque,
}
impl Description {
fn atom(text: impl Into<Cow<'static, str>>) -> Self {
Self::Atom(text.into())
}
fn and(self, other: Self) -> Self {
let mut parts = self.into_all_parts();
parts.extend(other.into_all_parts());
Self::All(parts)
}
fn or(self, other: Self) -> Self {
let mut parts = self.into_any_parts();
parts.extend(other.into_any_parts());
Self::Any(parts)
}
fn negate(self) -> Self {
match self {
Self::Not(inner) => *inner,
other => Self::Not(Box::new(other)),
}
}
fn into_all_parts(self) -> Vec<Self> {
match self {
Self::All(parts) => parts,
other => vec![other],
}
}
fn into_any_parts(self) -> Vec<Self> {
match self {
Self::Any(parts) => parts,
other => vec![other],
}
}
}
fn write_member(f: &mut fmt::Formatter<'_>, member: &Description) -> fmt::Result {
match member {
Description::All(_) | Description::Any(_) => write!(f, "({member})"),
_ => write!(f, "{member}"),
}
}
fn write_list(f: &mut fmt::Formatter<'_>, parts: &[Description], last_word: &str) -> fmt::Result {
match parts.split_last() {
None => Ok(()),
Some((last, [])) => write_member(f, last),
Some((last, init)) => {
for part in init {
write_member(f, part)?;
f.write_str(", ")?;
}
f.write_str(last_word)?;
write_member(f, last)
}
}
}
impl fmt::Display for Description {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Atom(text) => f.write_str(text),
Self::Opaque => f.write_str("(opaque condition)"),
Self::Not(inner) => write!(f, "not ({inner})"),
Self::All(parts) => write_list(f, parts, "and "),
Self::Any(parts) => write_list(f, parts, "or "),
}
}
}
trait ToU64: Copy {
fn to_u64(self) -> u64;
}
impl ToU64 for u8 {
fn to_u64(self) -> u64 {
u64::from(self)
}
}
impl ToU64 for usize {
fn to_u64(self) -> u64 {
self as u64
}
}
fn describe_int_range<T: ToU64>(range: &impl RangeBounds<T>, noun: &str) -> Description {
let lo = match range.start_bound() {
Bound::Included(&x) => Some(x.to_u64()),
Bound::Excluded(&x) => Some(x.to_u64() + 1),
Bound::Unbounded => None,
};
let hi = match range.end_bound() {
Bound::Included(&x) => Some(x.to_u64()),
Bound::Excluded(&x) => Some(x.to_u64().saturating_sub(1)),
Bound::Unbounded => None,
};
let text = match (lo, hi) {
(Some(a), Some(b)) if a == b => format!("exactly {a} {noun}"),
(Some(a), Some(b)) => format!("{a}–{b} {noun}"),
(Some(a), None) => format!("{a}+ {noun}"),
(None, Some(b)) => format!("≤{b} {noun}"),
(None, None) => format!("any {noun}"),
};
Description::atom(text)
}
fn describe_real_range(range: &impl RangeBounds<f64>, noun: &str) -> Description {
let endpoint = |bound: Bound<&f64>| match bound {
Bound::Included(&x) | Bound::Excluded(&x) => Some(x),
Bound::Unbounded => None,
};
let lo = endpoint(range.start_bound());
let hi = endpoint(range.end_bound());
let text = match (lo, hi) {
(Some(a), Some(b)) => format!("{a:.1}–{b:.1} {noun}"),
(Some(a), None) => format!("{a:.1}+ {noun}"),
(None, Some(b)) => format!("≤{b:.1} {noun}"),
(None, None) => format!("any {noun}"),
};
Description::atom(text)
}
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)))
}
#[derive(Clone)]
struct Described<F> {
condition: F,
label: Cow<'static, str>,
}
impl<F> Constraint for Described<F>
where
F: Fn(Hand, &Context<'_>) -> bool + Send + Sync,
{
fn eval(&self, hand: Hand, context: &Context<'_>) -> f32 {
crisp((self.condition)(hand, context))
}
fn describe(&self) -> Description {
Description::atom(self.label.clone())
}
}
pub fn described<F>(
label: impl Into<Cow<'static, str>>,
condition: F,
) -> Cons<impl Constraint + Clone>
where
F: Fn(Hand, &Context<'_>) -> bool + Clone + Send + Sync,
{
Cons(Described {
condition,
label: label.into(),
})
}
#[doc(hidden)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum FifthsCompanion {
Hcp,
Bumrap,
}
std::thread_local! {
static FUZZY_POINTS: Cell<bool> = const { Cell::new(true) };
static FUZZY_FIFTHS: Cell<bool> = const { Cell::new(true) };
static FIFTHS_COMPANION: Cell<FifthsCompanion> = const { Cell::new(FifthsCompanion::Bumrap) };
}
#[doc(hidden)]
pub fn set_fuzzy_strength(enabled: bool) {
set_fuzzy_points(enabled);
set_fuzzy_fifths(enabled);
}
#[doc(hidden)]
pub fn set_fuzzy_points(enabled: bool) {
FUZZY_POINTS.with(|flag| flag.set(enabled));
}
#[doc(hidden)]
pub fn set_fuzzy_fifths(enabled: bool) {
FUZZY_FIFTHS.with(|flag| flag.set(enabled));
}
#[doc(hidden)]
pub fn set_fifths_companion(companion: FifthsCompanion) {
FIFTHS_COMPANION.with(|cell| cell.set(companion));
}
fn raw_hcp(hand: Hand) -> u8 {
SimpleEvaluator(eval::hcp::<u8>).eval(hand)
}
fn bound_range<T: ToU64>(range: &impl RangeBounds<T>, cap: u8) -> Range {
let cap = u64::from(cap);
let min = match range.start_bound() {
Bound::Included(&x) => x.to_u64(),
Bound::Excluded(&x) => x.to_u64() + 1,
Bound::Unbounded => 0,
};
let max = match range.end_bound() {
Bound::Included(&x) => x.to_u64(),
Bound::Excluded(&x) => x.to_u64().saturating_sub(1),
Bound::Unbounded => cap,
};
let clamp = |x: u64| u8::try_from(x.min(cap)).unwrap_or_else(|_| unreachable!());
Range::new(clamp(min), clamp(max))
}
#[derive(Clone)]
struct Hcp<R>(R);
impl<R: RangeBounds<u8> + Clone + Send + Sync> Constraint for Hcp<R> {
fn eval(&self, hand: Hand, _: &Context<'_>) -> f32 {
crisp(self.0.contains(&raw_hcp(hand)))
}
fn describe(&self) -> Description {
describe_int_range(&self.0, "HCP")
}
fn project(&self, _: &Context<'_>) -> Inference {
let floor = bound_range(&self.0, Range::FULL_POINTS.max).min;
let mut inference = Inference::unknown();
inference.points = Range::new(floor, Range::FULL_POINTS.max);
inference
}
}
#[must_use]
pub fn hcp(range: impl RangeBounds<u8> + Clone + Send + Sync) -> Cons<impl Constraint + Clone> {
Cons(Hcp(range))
}
const fn blocks_upgrade(holding: Holding) -> bool {
holding.len() <= 2
&& (holding.contains(Rank::Q)
|| holding.contains(Rank::J)
|| (holding.contains(Rank::A) && holding.contains(Rank::K))
|| (holding.len() == 1 && (holding.contains(Rank::A) || holding.contains(Rank::K))))
}
#[must_use]
pub fn upgrade(hand: Hand) -> u8 {
let holdings = Suit::ASC.map(|suit| hand[suit]);
if holdings.iter().any(|&holding| blocks_upgrade(holding)) {
return 0;
}
let mut lengths = holdings.map(Holding::len);
lengths.sort_unstable();
u8::from(!is_balanced(hand)) + u8::from(lengths[2] + lengths[3] >= 10)
}
#[must_use]
pub fn point_count(hand: Hand) -> u8 {
raw_hcp(hand) + upgrade(hand)
}
#[derive(Clone)]
struct Points<R>(R);
impl<R: RangeBounds<u8> + Clone + Send + Sync> Constraint for Points<R> {
fn eval(&self, hand: Hand, _: &Context<'_>) -> f32 {
let bonus = if FUZZY_POINTS.with(Cell::get) {
upgrade(hand)
} else {
0
};
crisp(self.0.contains(&(raw_hcp(hand) + bonus)))
}
fn describe(&self) -> Description {
describe_int_range(&self.0, "points")
}
fn project(&self, _: &Context<'_>) -> Inference {
let floor = bound_range(&self.0, Range::FULL_POINTS.max).min;
let mut inference = Inference::unknown();
inference.points = Range::new(floor, Range::FULL_POINTS.max);
inference
}
}
#[must_use]
pub fn points(range: impl RangeBounds<u8> + Clone + Send + Sync) -> Cons<impl Constraint + Clone> {
Cons(Points(range))
}
#[derive(Clone)]
struct Fifths<R>(R);
impl<R: RangeBounds<f64> + Clone + Send + Sync> Constraint for Fifths<R> {
fn eval(&self, hand: Hand, _: &Context<'_>) -> f32 {
let value = if FUZZY_FIFTHS.with(Cell::get) {
let companion = match FIFTHS_COMPANION.with(Cell::get) {
FifthsCompanion::Hcp => f64::from(raw_hcp(hand)),
FifthsCompanion::Bumrap => eval::BUMRAP.eval(hand),
};
f64::midpoint(eval::FIFTHS.eval(hand), companion)
} else {
f64::from(raw_hcp(hand))
};
crisp(self.0.contains(&value))
}
fn describe(&self) -> Description {
describe_real_range(&self.0, "fifths")
}
}
#[must_use]
pub fn fifths(range: impl RangeBounds<f64> + Clone + Send + Sync) -> Cons<impl Constraint + Clone> {
Cons(Fifths(range))
}
#[derive(Clone)]
struct Len<R> {
suit: Suit,
range: R,
}
impl<R: RangeBounds<usize> + Clone + Send + Sync> Constraint for Len<R> {
fn eval(&self, hand: Hand, _: &Context<'_>) -> f32 {
crisp(self.range.contains(&hand[self.suit].len()))
}
fn describe(&self) -> Description {
describe_int_range(&self.range, &self.suit.to_string())
}
fn project(&self, _: &Context<'_>) -> Inference {
len_projection(self.suit, &self.range)
}
}
pub fn len(
suit: Suit,
range: impl RangeBounds<usize> + Clone + Send + Sync,
) -> Cons<impl Constraint + Clone> {
Cons(Len { suit, range })
}
fn len_projection<R: RangeBounds<usize>>(suit: Suit, range: &R) -> Inference {
let mut inference = Inference::unknown();
inference.lengths[suit as usize] = bound_range(range, Range::FULL_LENGTH.max);
inference
}
#[derive(Clone)]
struct AllLen<const N: usize, R> {
suits: [Suit; N],
range: R,
}
impl<const N: usize, R: RangeBounds<usize> + Clone + Send + Sync> Constraint for AllLen<N, R> {
fn eval(&self, hand: Hand, _: &Context<'_>) -> f32 {
crisp(
self.suits
.iter()
.all(|&suit| self.range.contains(&hand[suit].len())),
)
}
fn describe(&self) -> Description {
self.suits
.iter()
.map(|suit| describe_int_range(&self.range, &suit.to_string()))
.reduce(|a, b| a.and(b))
.unwrap_or(Description::Opaque)
}
fn project(&self, _: &Context<'_>) -> Inference {
self.suits
.iter()
.map(|&suit| len_projection(suit, &self.range))
.reduce(|acc, inf| acc.intersect(&inf))
.unwrap_or_else(Inference::unknown)
}
}
#[must_use]
pub fn and<const N: usize>(
suits: [Suit; N],
range: impl RangeBounds<usize> + Clone + Send + Sync,
) -> Cons<impl Constraint + Clone> {
Cons(AllLen { suits, range })
}
#[derive(Clone)]
struct AnyLen<const N: usize, R> {
suits: [Suit; N],
range: R,
}
impl<const N: usize, R: RangeBounds<usize> + Clone + Send + Sync> Constraint for AnyLen<N, R> {
fn eval(&self, hand: Hand, _: &Context<'_>) -> f32 {
crisp(
self.suits
.iter()
.any(|&suit| self.range.contains(&hand[suit].len())),
)
}
fn describe(&self) -> Description {
self.suits
.iter()
.map(|suit| describe_int_range(&self.range, &suit.to_string()))
.reduce(|a, b| a.or(b))
.unwrap_or(Description::Opaque)
}
fn project(&self, _: &Context<'_>) -> Inference {
self.suits
.iter()
.map(|&suit| len_projection(suit, &self.range))
.reduce(|acc, inf| acc.union(&inf))
.unwrap_or_else(Inference::unknown)
}
}
#[must_use]
pub fn or<const N: usize>(
suits: [Suit; N],
range: impl RangeBounds<usize> + Clone + Send + Sync,
) -> Cons<impl Constraint + Clone> {
Cons(AnyLen { suits, range })
}
#[derive(Clone)]
struct SuitHcp<R> {
suit: Suit,
range: R,
}
impl<R: RangeBounds<u8> + Clone + Send + Sync> Constraint for SuitHcp<R> {
fn eval(&self, hand: Hand, _: &Context<'_>) -> f32 {
crisp(self.range.contains(&eval::hcp::<u8>(hand[self.suit])))
}
fn describe(&self) -> Description {
describe_int_range(&self.range, &format!("HCP in {}", self.suit))
}
}
#[must_use]
pub fn suit_hcp(
suit: Suit,
range: impl RangeBounds<u8> + Clone + Send + Sync,
) -> Cons<impl Constraint + Clone> {
Cons(SuitHcp { suit, range })
}
fn is_balanced(hand: Hand) -> bool {
let lengths = Suit::ASC.map(|suit| hand[suit].len());
lengths.iter().all(|&length| length >= 2)
&& lengths.iter().filter(|&&length| length == 2).count() <= 1
}
#[derive(Clone)]
struct Balanced;
impl Constraint for Balanced {
fn eval(&self, hand: Hand, _: &Context<'_>) -> f32 {
crisp(is_balanced(hand))
}
fn describe(&self) -> Description {
Description::atom("balanced")
}
}
#[must_use]
pub fn balanced() -> Cons<impl Constraint + Clone> {
Cons(Balanced)
}
fn is_rule_of_20(hand: Hand) -> bool {
let mut lengths = Suit::ASC.map(|suit| hand[suit].len());
lengths.sort_unstable();
usize::from(raw_hcp(hand)) + lengths[2] + lengths[3] >= 20
}
#[derive(Clone)]
struct RuleOf20;
impl Constraint for RuleOf20 {
fn eval(&self, hand: Hand, _: &Context<'_>) -> f32 {
crisp(is_rule_of_20(hand))
}
fn describe(&self) -> Description {
Description::atom("Rule of 20")
}
}
#[must_use]
pub fn rule_of_20() -> Cons<impl Constraint + Clone> {
Cons(RuleOf20)
}
#[derive(Clone)]
struct CcccAtLeast(f64);
impl Constraint for CcccAtLeast {
fn eval(&self, hand: Hand, _: &Context<'_>) -> f32 {
crisp(eval::cccc(hand) >= self.0)
}
fn describe(&self) -> Description {
Description::atom(format!("CCCC ≥ {}", self.0))
}
}
#[must_use]
pub fn cccc_at_least(points: f64) -> Cons<impl Constraint + Clone> {
Cons(CcccAtLeast(points))
}
#[derive(Clone)]
struct Support<R>(R);
impl<R: RangeBounds<usize> + Clone + Send + Sync> Constraint for Support<R> {
fn eval(&self, hand: Hand, context: &Context<'_>) -> f32 {
crisp(
context
.partner_last_suit()
.is_some_and(|suit| self.0.contains(&hand[suit].len())),
)
}
fn describe(&self) -> Description {
describe_int_range(&self.0, "card support for partner")
}
}
pub fn support(
range: impl RangeBounds<usize> + Clone + Send + Sync,
) -> Cons<impl Constraint + Clone> {
Cons(Support(range))
}
#[derive(Clone)]
struct PartnerShownLen<R> {
suit: Suit,
range: R,
}
impl<R: RangeBounds<u8> + Clone + Send + Sync> Constraint for PartnerShownLen<R> {
fn eval(&self, _: Hand, context: &Context<'_>) -> f32 {
let shown = Inferences::read(context).partner().length(self.suit);
crisp(self.range.contains(&shown.min))
}
fn describe(&self) -> Description {
describe_int_range(&self.range, &format!("{} shown by partner", self.suit))
}
}
pub fn partner_shown_len(
suit: Suit,
range: impl RangeBounds<u8> + Clone + Send + Sync,
) -> Cons<impl Constraint + Clone> {
Cons(PartnerShownLen { suit, range })
}
#[derive(Clone)]
struct PartnerShownPoints<R>(R);
impl<R: RangeBounds<u8> + Clone + Send + Sync> Constraint for PartnerShownPoints<R> {
fn eval(&self, _: Hand, context: &Context<'_>) -> f32 {
let shown = Inferences::read(context).partner().points;
crisp(self.0.contains(&shown.min))
}
fn describe(&self) -> Description {
describe_int_range(&self.0, "points shown by partner")
}
}
pub fn partner_shown_points(
range: impl RangeBounds<u8> + Clone + Send + Sync,
) -> Cons<impl Constraint + Clone> {
Cons(PartnerShownPoints(range))
}
#[derive(Clone)]
struct TopHonors<R> {
suit: Suit,
range: R,
}
impl<R: RangeBounds<usize> + Clone + Send + Sync> Constraint for TopHonors<R> {
fn eval(&self, hand: Hand, _: &Context<'_>) -> f32 {
let holding = hand[self.suit];
let count = [Rank::A, Rank::K, Rank::Q]
.into_iter()
.filter(|&rank| holding.contains(rank))
.count();
crisp(self.range.contains(&count))
}
fn describe(&self) -> Description {
describe_int_range(&self.range, &format!("of the top honors in {}", self.suit))
}
}
pub fn top_honors(
suit: Suit,
range: impl RangeBounds<usize> + Clone + Send + Sync,
) -> Cons<impl Constraint + Clone> {
Cons(TopHonors { suit, range })
}
pub(crate) 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)
}
#[derive(Clone)]
struct StopperIn(Suit);
impl Constraint for StopperIn {
fn eval(&self, hand: Hand, _: &Context<'_>) -> f32 {
crisp(has_stopper(hand[self.0]))
}
fn describe(&self) -> Description {
Description::atom(format!("stopper in {}", self.0))
}
}
#[must_use]
pub fn stopper_in(suit: Suit) -> Cons<impl Constraint + Clone> {
Cons(StopperIn(suit))
}
#[derive(Clone)]
struct StopperInTheirSuits;
impl Constraint for StopperInTheirSuits {
fn eval(&self, hand: Hand, context: &Context<'_>) -> f32 {
crisp(context.their_suits().all(|suit| has_stopper(hand[suit])))
}
fn describe(&self) -> Description {
Description::atom("stopper in their suit(s)")
}
}
#[must_use]
pub fn stopper_in_their_suits() -> Cons<impl Constraint + Clone> {
Cons(StopperInTheirSuits)
}
#[derive(Clone)]
struct TheyBid(Strain);
impl Constraint for TheyBid {
fn eval(&self, _: Hand, context: &Context<'_>) -> f32 {
crisp(context.they_bid(self.0))
}
fn describe(&self) -> Description {
Description::atom(format!("opponents bid {}", self.0))
}
}
#[must_use]
pub fn they_bid(strain: Strain) -> Cons<impl Constraint + Clone> {
Cons(TheyBid(strain))
}
#[derive(Clone)]
struct ShortInTheirSuits;
impl Constraint for ShortInTheirSuits {
fn eval(&self, hand: Hand, context: &Context<'_>) -> f32 {
crisp(context.their_suits().all(|suit| hand[suit].len() <= 3))
}
fn describe(&self) -> Description {
Description::atom("at most three cards in each of their suits")
}
}
#[must_use]
pub fn short_in_their_suits() -> Cons<impl Constraint + Clone> {
Cons(ShortInTheirSuits)
}
std::thread_local! {
static SUPPRESS_FLAT_4333_TAKEOUT: Cell<bool> = const { Cell::new(true) };
static SUPPRESS_5332_TAKEOUT: Cell<bool> = const { Cell::new(true) };
static SUPPRESS_4432_VS_MAJOR: Cell<bool> = const { Cell::new(false) };
static SUPPRESS_4432_VS_MINOR: Cell<bool> = const { Cell::new(false) };
}
#[doc(hidden)]
pub fn set_suppress_flat_4333_takeout(on: bool) {
SUPPRESS_FLAT_4333_TAKEOUT.with(|flag| flag.set(on));
}
fn suppress_flat_4333_takeout() -> bool {
SUPPRESS_FLAT_4333_TAKEOUT.with(Cell::get)
}
#[doc(hidden)]
pub fn set_suppress_5332_takeout(on: bool) {
SUPPRESS_5332_TAKEOUT.with(|flag| flag.set(on));
}
fn suppress_5332_takeout() -> bool {
SUPPRESS_5332_TAKEOUT.with(Cell::get)
}
#[doc(hidden)]
pub fn set_suppress_4432_vs_major(on: bool) {
SUPPRESS_4432_VS_MAJOR.with(|flag| flag.set(on));
}
fn suppress_4432_vs_major() -> bool {
SUPPRESS_4432_VS_MAJOR.with(Cell::get)
}
#[doc(hidden)]
pub fn set_suppress_4432_vs_minor(on: bool) {
SUPPRESS_4432_VS_MINOR.with(|flag| flag.set(on));
}
fn suppress_4432_vs_minor() -> bool {
SUPPRESS_4432_VS_MINOR.with(Cell::get)
}
#[must_use]
pub(crate) fn takeout_double_shape_ok() -> Cons<impl Constraint + Clone> {
let suppress_4333 = suppress_flat_4333_takeout();
let suppress_5332 = suppress_5332_takeout();
let suppress_4432_major = suppress_4432_vs_major();
let suppress_4432_minor = suppress_4432_vs_minor();
described(
"not a weak balanced hand diverted to Pass",
move |hand: Hand, context: &Context<'_>| {
let mut lens = [0usize; 4];
for (slot, suit) in Suit::ASC.into_iter().enumerate() {
lens[slot] = hand[suit].len();
}
lens.sort_unstable_by(|a, b| b.cmp(a));
let hcp = raw_hcp(hand);
let reject_4333 = suppress_4333 && lens == [4, 3, 3, 3] && hcp < 15;
let reject_5332 = suppress_5332 && lens == [5, 3, 3, 2] && hcp < 14;
let their_major = context
.their_suits()
.any(|suit| Strain::from(suit).is_major());
let reject_4432 = lens == [4, 4, 3, 2]
&& hcp < 14
&& (if their_major {
suppress_4432_major
} else {
suppress_4432_minor
});
!(reject_4333 || reject_5332 || reject_4432)
},
)
}
#[derive(Clone)]
struct UnbidSupport {
max_short: usize,
}
impl Constraint for UnbidSupport {
fn eval(&self, hand: Hand, context: &Context<'_>) -> f32 {
let short = [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades]
.into_iter()
.filter(|&suit| context.their_suits().all(|theirs| theirs != suit))
.filter(|&suit| hand[suit].len() < 3)
.count();
crisp(short <= self.max_short)
}
fn describe(&self) -> Description {
Description::atom(if self.max_short == 0 {
"at least three cards in each unbid suit".to_owned()
} else {
format!(
"at most {} unbid suit(s) shorter than three cards",
self.max_short
)
})
}
}
#[must_use]
pub fn unbid_support(max_short: usize) -> Cons<impl Constraint + Clone> {
Cons(UnbidSupport { max_short })
}
#[derive(Clone)]
struct PartnerSuitIs(Suit);
impl Constraint for PartnerSuitIs {
fn eval(&self, _: Hand, context: &Context<'_>) -> f32 {
crisp(context.partner_last_suit() == Some(self.0))
}
fn describe(&self) -> Description {
Description::atom(format!("partner's last suit is {}", self.0))
}
}
#[must_use]
pub fn partner_suit_is(suit: Suit) -> Cons<impl Constraint + Clone> {
Cons(PartnerSuitIs(suit))
}
#[derive(Clone)]
struct MinLevelIs {
level: u8,
strain: Strain,
}
impl Constraint for MinLevelIs {
fn eval(&self, _: Hand, context: &Context<'_>) -> f32 {
crisp(context.min_level(self.strain) == Some(Level::new(self.level)))
}
fn describe(&self) -> Description {
Description::atom(format!("{}{} is the cheapest bid", self.level, self.strain))
}
}
#[must_use]
pub fn min_level_is(level: u8, strain: Strain) -> Cons<impl Constraint + Clone> {
Cons(MinLevelIs { level, strain })
}
#[derive(Clone)]
struct PassedHand;
impl Constraint for PassedHand {
fn eval(&self, _: Hand, context: &Context<'_>) -> f32 {
crisp(context.passed_hand())
}
fn describe(&self) -> Description {
Description::atom("a passed hand")
}
}
#[must_use]
pub fn passed_hand() -> Cons<impl Constraint + Clone> {
Cons(PassedHand)
}
#[derive(Clone)]
struct Undisturbed;
impl Constraint for Undisturbed {
fn eval(&self, _: Hand, context: &Context<'_>) -> f32 {
crisp(context.undisturbed())
}
fn describe(&self) -> Description {
Description::atom("the opponents have passed throughout")
}
}
#[must_use]
pub fn undisturbed() -> Cons<impl Constraint + Clone> {
Cons(Undisturbed)
}
#[derive(Clone)]
struct Vulnerable;
impl Constraint for Vulnerable {
fn eval(&self, _: Hand, context: &Context<'_>) -> f32 {
use contract_bridge::auction::RelativeVulnerability;
crisp(context.vul().contains(RelativeVulnerability::WE))
}
fn describe(&self) -> Description {
Description::atom("vulnerable")
}
}
#[must_use]
pub fn vulnerable() -> Cons<impl Constraint + Clone> {
Cons(Vulnerable)
}
#[derive(Clone)]
struct TheyVulnerable;
impl Constraint for TheyVulnerable {
fn eval(&self, _: Hand, context: &Context<'_>) -> f32 {
use contract_bridge::auction::RelativeVulnerability;
crisp(context.vul().contains(RelativeVulnerability::THEY))
}
fn describe(&self) -> Description {
Description::atom("opponents vulnerable")
}
}
#[must_use]
pub fn they_vulnerable() -> Cons<impl Constraint + Clone> {
Cons(TheyVulnerable)
}
#[derive(Clone)]
struct NthSeat(u8);
impl Constraint for NthSeat {
fn eval(&self, _: Hand, context: &Context<'_>) -> f32 {
crisp(context.seat_to_open() == Some(self.0))
}
fn describe(&self) -> Description {
Description::atom(format!("opening in seat {}", self.0))
}
}
#[must_use]
pub fn nth_seat(seat: u8) -> Cons<impl Constraint + Clone> {
Cons(NthSeat(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_blocks_upgrade() {
let clean = ["", "2", "32", "A2", "K2", "KT", "AKQ", "QJ2", "J32"];
let wasted = [
"A", "K", "Q", "J", "Q2", "J2", "AK", "AQ", "AJ", "KQ", "KJ", "QJ",
];
for text in clean {
let holding: Holding = text.parse().expect(text);
assert!(!blocks_upgrade(holding), "{text} should not block");
}
for text in wasted {
let holding: Holding = text.parse().expect(text);
assert!(blocks_upgrade(holding), "{text} should block");
}
}
#[test]
fn test_upgrade() {
assert_eq!(upgrade(hand(BALANCED_15)), 0);
assert_eq!(upgrade(hand("AQJ32.K53.QJ4.92")), 0);
assert_eq!(upgrade(hand("KQ765.A876.532.2")), 1);
assert_eq!(upgrade(hand("KQ765.A8765.32.2")), 2);
assert_eq!(upgrade(hand("KQ8765.A876.32.2")), 2);
assert_eq!(upgrade(hand("KQ765.87654.A2.2")), 2);
assert_eq!(upgrade(hand("KQ765.A876.532.K")), 0); assert_eq!(upgrade(hand("KQ765.A8765.Q2.2")), 0); assert_eq!(upgrade(hand("KQ765.87654.AK.2")), 0); }
#[test]
fn test_rule_of_20() {
let context = empty_context();
let opens = |text: &str| rule_of_20().eval(hand(text), &context);
assert_pass(opens("AK986.J9.QJT6.64"));
assert_pass(opens(".KQ7542.A.Q96542"));
assert_pass(opens("KJ9876.5.KQJ4.32"));
assert_pass(opens("A765432.K76543.."));
assert_reject(opens("KQ32.K32.Q32.J32"));
assert_reject(opens("KQ876.K32.Q32.J2"));
}
#[test]
fn test_unbid_support() {
let auction = [Call::Bid(Bid::new(1, Strain::Hearts))];
let context = Context::new(RelativeVulnerability::NONE, &auction);
let shapely = hand("AQ82.5.KJ64.Q975");
assert_pass(unbid_support(0).eval(shapely, &context));
assert_pass(unbid_support(1).eval(shapely, &context));
let semi = hand("Q2.A54.K54.KJ876");
assert_reject(unbid_support(0).eval(semi, &context));
assert_pass(unbid_support(1).eval(semi, &context));
let one_suiter = hand("K2.A54.Q2.KJ8763");
assert_reject(unbid_support(0).eval(one_suiter, &context));
assert_reject(unbid_support(1).eval(one_suiter, &context));
}
#[test]
fn test_points_and_fifths() {
let context = empty_context();
let two_suiter = hand("KQ765.A8765.32.2");
assert_pass(points(11..=11).eval(two_suiter, &context));
assert_reject(points(..=10).eval(two_suiter, &context));
assert_pass(points(15..=15).eval(hand(BALANCED_15), &context));
assert_reject(fifths(15.0..18.0).eval(hand(BALANCED_15), &context));
assert_pass(fifths(12.0..15.0).eval(hand(BALANCED_15), &context));
assert_pass(cccc_at_least(14.9).eval(hand("AQ32.K53.QJ4.A92"), &context));
assert_reject(cccc_at_least(15.0).eval(hand("AQ32.K53.QJ4.A92"), &context));
}
#[test]
fn test_fifths_companion() {
let context = empty_context();
let quacky = hand("AQ4.QJT.QJT.KQJT");
set_fifths_companion(FifthsCompanion::Hcp);
assert_reject(fifths(15.0..18.0).eval(quacky, &context));
set_fifths_companion(FifthsCompanion::Bumrap);
assert_pass(fifths(15.0..18.0).eval(quacky, &context));
}
#[test]
fn test_fuzzy_strength_toggle() {
let context = empty_context();
let two_suiter = hand("KQ765.A8765.32.2");
set_fuzzy_strength(false);
assert_pass(points(9..=9).eval(two_suiter, &context));
assert_pass(fifths(15.0..18.0).eval(hand(BALANCED_15), &context));
assert_reject(fifths(15.5..18.0).eval(hand(BALANCED_15), &context));
set_fuzzy_strength(true);
assert_pass(points(11..=11).eval(two_suiter, &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_partner_shown_len_and_points() {
let auction = [Call::Bid(Bid::new(1, Strain::Diamonds)), Call::Pass];
let context = Context::new(RelativeVulnerability::NONE, &auction);
assert_pass(partner_shown_len(Suit::Diamonds, 3..).eval(hand(BALANCED_15), &context));
assert_reject(partner_shown_len(Suit::Diamonds, 4..).eval(hand(BALANCED_15), &context));
assert_pass(partner_shown_points(10..).eval(hand(BALANCED_15), &context));
assert_reject(partner_shown_points(11..).eval(hand(BALANCED_15), &context));
assert_reject(partner_shown_len(Suit::Spades, 1..).eval(hand(BALANCED_15), &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));
}
fn prose(constraint: &impl Constraint) -> String {
constraint.describe().to_string()
}
#[test]
fn test_describe_ranges() {
assert_eq!(prose(&hcp(15..=17)), "15–17 HCP");
assert_eq!(prose(&hcp(16..)), "16+ HCP");
assert_eq!(prose(&hcp(..11)), "≤10 HCP"); assert_eq!(prose(&points(12..=21)), "12–21 points");
assert_eq!(prose(&len(Suit::Spades, 5..)), "5+ ♠");
assert_eq!(prose(&len(Suit::Hearts, 6..=6)), "exactly 6 ♥");
assert_eq!(prose(&support(3..)), "3+ card support for partner");
assert_eq!(
prose(&top_honors(Suit::Spades, 2..)),
"2+ of the top honors in ♠"
);
assert_eq!(
prose(&partner_shown_len(Suit::Diamonds, 3..)),
"3+ ♦ shown by partner",
);
assert_eq!(
prose(&partner_shown_points(12..)),
"12+ points shown by partner"
);
assert_eq!(prose(&fifths(15.0..18.0)), "15.0–18.0 fifths");
assert_eq!(prose(&fifths(20.0..22.0)), "20.0–22.0 fifths");
}
#[test]
fn test_describe_atoms() {
assert_eq!(prose(&balanced()), "balanced");
assert_eq!(prose(&cccc_at_least(14.9)), "CCCC ≥ 14.9");
assert_eq!(prose(&stopper_in(Suit::Hearts)), "stopper in ♥");
assert_eq!(prose(&stopper_in_their_suits()), "stopper in their suit(s)");
assert_eq!(
prose(&short_in_their_suits()),
"at most three cards in each of their suits",
);
assert_eq!(prose(&they_bid(Strain::Spades)), "opponents bid ♠");
assert_eq!(prose(&they_bid(Strain::Notrump)), "opponents bid NT");
assert_eq!(
prose(&partner_suit_is(Suit::Hearts)),
"partner's last suit is ♥"
);
assert_eq!(
prose(&min_level_is(2, Strain::Diamonds)),
"2♦ is the cheapest bid"
);
assert_eq!(prose(&passed_hand()), "a passed hand");
assert_eq!(
prose(&undisturbed()),
"the opponents have passed throughout"
);
assert_eq!(prose(&vulnerable()), "vulnerable");
assert_eq!(prose(&they_vulnerable()), "opponents vulnerable");
assert_eq!(prose(&nth_seat(3)), "opening in seat 3");
}
#[test]
fn test_describe_composition() {
assert_eq!(
prose(&(points(12..=21) & len(Suit::Spades, 5..))),
"12–21 points, and 5+ ♠",
);
assert_eq!(
prose(&(points(12..=21) & len(Suit::Spades, 5..) & balanced())),
"12–21 points, 5+ ♠, and balanced",
);
assert_eq!(
prose(&(len(Suit::Clubs, 5..) | len(Suit::Diamonds, 5..))),
"5+ ♣, or 5+ ♦",
);
assert_eq!(prose(&!hcp(16..)), "not (16+ HCP)");
assert_eq!(prose(&!!balanced()), "balanced");
assert_eq!(
prose(&(points(9..=11) & len(Suit::Spades, 5..) & (nth_seat(3) | nth_seat(4)))),
"9–11 points, 5+ ♠, and (opening in seat 3, or opening in seat 4)",
);
}
#[test]
fn test_describe_opaque_and_labeled() {
assert_eq!(pred(|_, _| true).describe(), Description::Opaque);
assert_eq!(prose(&pred(|_, _| true)), "(opaque condition)");
assert_eq!(
prose(&(hcp(15..) & pred(|_, _| true))),
"15+ HCP, and (opaque condition)",
);
let prefers_diamonds = described("prefers diamonds", |hand: Hand, _: &Context<'_>| {
hand[Suit::Diamonds].len() >= hand[Suit::Clubs].len()
});
assert_eq!(prose(&prefers_diamonds), "prefers diamonds");
assert_pass(prefers_diamonds.eval(hand(BALANCED_15), &empty_context()));
}
}