use super::Rules;
use super::constraint::{
Cons, Constraint, balanced, described, hcp, len, min_level_is, partner_shown_len,
partner_suit_is, point_count, points, pred, short_in_their_suits, stopper_in_their_suits,
support, takeout_double_shape_ok, they_bid, top_honors,
};
use super::context::Context;
use super::inference::Inferences;
use super::rules::Alert;
use contract_bridge::auction::Call;
use contract_bridge::eval::hcp as holding_hcp;
use contract_bridge::{Bid, Hand, Penalty, Rank, Strain, Suit};
use core::cell::Cell;
const GAMBLING_3NT: Alert = Alert("1ntx:gambling-3nt");
#[doc(hidden)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum Unusual2nt {
FourFour,
FiveFiveAdd,
#[default]
Direct,
}
#[doc(hidden)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum LatchStyle {
#[default]
Penalty,
Optional,
}
std::thread_local! {
static INFERENCE_AWARE: Cell<bool> = const { Cell::new(true) };
static ONE_NT_RUNOUT: Cell<bool> = const { Cell::new(true) };
static RUNOUT_XX_MIN: Cell<u8> = const { Cell::new(7) };
static ONE_NT_RUNOUT_UNIVERSAL: Cell<bool> = const { Cell::new(true) };
static UNUSUAL_2NT: Cell<Unusual2nt> = const { Cell::new(Unusual2nt::Direct) };
static PENALIZE_ESCAPE_STACK: Cell<bool> = const { Cell::new(true) };
static PENALIZE_ESCAPE_VALUES: Cell<bool> = const { Cell::new(true) };
static UVU_ENCIRCLE: Cell<bool> = const { Cell::new(true) };
static SETTLE_FLOOR: Cell<bool> = const { Cell::new(true) };
static PENALTY_LATCH: Cell<bool> = const { Cell::new(true) };
static LATCH_STYLE: Cell<LatchStyle> = const { Cell::new(LatchStyle::Penalty) };
static PENALTY_NO_PULL: Cell<bool> = const { Cell::new(true) };
static ADVANCER_XX_RUNOUT: Cell<bool> = const { Cell::new(true) };
static DOUBLER_XX_RUNOUT: Cell<bool> = const { Cell::new(true) };
static RUBENS_ADVANCES: Cell<bool> = const { Cell::new(true) };
static NT_RESPONDER_GAME_FLOOR: Cell<u8> = const { Cell::new(9) };
static SUPPRESS_NT_GF_OVER_DOUBLE: Cell<bool> = const { Cell::new(true) };
static CORRECT_3NT_TO_MAJOR: Cell<bool> = const { Cell::new(true) };
static GAMBLING_3NT_OVER_DOUBLE: Cell<bool> = const { Cell::new(false) };
static GAMBLING_3NT_TOP_HONORS: Cell<u8> = const { Cell::new(2) };
static GAMBLING_3NT_REQUIRE_ACE: Cell<bool> = const { Cell::new(true) };
static PREEMPT_4M_OVER_DOUBLE: Cell<bool> = const { Cell::new(false) };
static PREEMPT_4M_FLOOR: Cell<u8> = const { Cell::new(5) };
static PREEMPT_4M_TOP_HONORS: Cell<u8> = const { Cell::new(2) };
static PREEMPT_4M_REQUIRE_ACE: Cell<bool> = const { Cell::new(true) };
static COMPETITIVE_REBID: Cell<bool> = const { Cell::new(true) };
}
const RUNOUT_MAX_HCP: u8 = 8;
#[doc(hidden)]
pub fn set_inference_aware(enabled: bool) {
INFERENCE_AWARE.with(|flag| flag.set(enabled));
}
fn inference_aware() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, _: &Context<'_>| INFERENCE_AWARE.with(Cell::get))
}
#[doc(hidden)]
pub fn set_one_nt_runout(enabled: bool) {
ONE_NT_RUNOUT.with(|flag| flag.set(enabled));
}
fn one_nt_runout_enabled() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, _: &Context<'_>| ONE_NT_RUNOUT.with(Cell::get))
}
#[doc(hidden)]
pub fn set_settle_floor(enabled: bool) {
SETTLE_FLOOR.with(|flag| flag.set(enabled));
}
fn settle_floor() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, _: &Context<'_>| SETTLE_FLOOR.with(Cell::get))
}
#[doc(hidden)]
pub fn set_competitive_rebid(enabled: bool) {
COMPETITIVE_REBID.with(|flag| flag.set(enabled));
}
fn competitive_rebid_enabled() -> bool {
COMPETITIVE_REBID.with(Cell::get)
}
#[doc(hidden)]
pub fn set_rubens_advances(enabled: bool) {
RUBENS_ADVANCES.with(|flag| flag.set(enabled));
}
pub(super) fn rubens_advances_enabled() -> bool {
RUBENS_ADVANCES.with(Cell::get)
}
const RKCB_FLOOR: Alert = Alert("floor:rkcb");
std::thread_local! {
static FLOOR_RKCB: Cell<bool> = const { Cell::new(true) };
}
#[doc(hidden)]
pub fn set_floor_rkcb(enabled: bool) {
FLOOR_RKCB.with(|flag| flag.set(enabled));
}
fn floor_rkcb_now() -> bool {
FLOOR_RKCB.with(Cell::get)
}
#[doc(hidden)]
pub fn set_runout_xx_min(floor: u8) {
RUNOUT_XX_MIN.with(|cell| cell.set(floor));
}
#[doc(hidden)]
pub fn set_nt_responder_game_floor(floor: u8) {
NT_RESPONDER_GAME_FLOOR.with(|cell| cell.set(floor));
}
fn nt_responder_game_floor() -> u8 {
NT_RESPONDER_GAME_FLOOR.with(Cell::get)
}
#[doc(hidden)]
pub fn set_suppress_nt_game_force_over_double(suppress: bool) {
SUPPRESS_NT_GF_OVER_DOUBLE.with(|cell| cell.set(suppress));
}
#[doc(hidden)]
pub fn set_correct_3nt_to_major(correct: bool) {
CORRECT_3NT_TO_MAJOR.with(|cell| cell.set(correct));
}
fn nt_game_force_3nt_allowed() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| {
!(SUPPRESS_NT_GF_OVER_DOUBLE.with(Cell::get) && responder_one_nt_runout_now(context))
})
}
fn responder_has_xx_values() -> Cons<impl Constraint + Clone> {
pred(|hand: Hand, _: &Context<'_>| {
let hcp: u8 = Suit::ASC
.iter()
.map(|&suit| holding_hcp::<u8>(hand[suit]))
.sum();
hcp >= RUNOUT_XX_MIN.with(Cell::get)
})
}
#[doc(hidden)]
pub fn set_gambling_3nt_over_double(on: bool) {
GAMBLING_3NT_OVER_DOUBLE.with(|cell| cell.set(on));
}
#[doc(hidden)]
pub fn set_gambling_3nt_top_honors(floor: u8) {
GAMBLING_3NT_TOP_HONORS.with(|cell| cell.set(floor));
}
#[doc(hidden)]
pub fn set_gambling_3nt_require_ace(on: bool) {
GAMBLING_3NT_REQUIRE_ACE.with(|cell| cell.set(on));
}
#[doc(hidden)]
pub fn set_preempt_4m_over_double(on: bool) {
PREEMPT_4M_OVER_DOUBLE.with(|cell| cell.set(on));
}
#[doc(hidden)]
pub fn set_preempt_4m_floor(floor: u8) {
PREEMPT_4M_FLOOR.with(|cell| cell.set(floor));
}
#[doc(hidden)]
pub fn set_preempt_4m_top_honors(floor: u8) {
PREEMPT_4M_TOP_HONORS.with(|cell| cell.set(floor));
}
#[doc(hidden)]
pub fn set_preempt_4m_require_ace(on: bool) {
PREEMPT_4M_REQUIRE_ACE.with(|cell| cell.set(on));
}
fn gambling_3nt_authored() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, _: &Context<'_>| GAMBLING_3NT_OVER_DOUBLE.with(Cell::get))
}
fn gambling_3nt_semisolid(minor: Suit) -> Cons<impl Constraint + Clone> {
described("a semi-solid suit", move |hand: Hand, _: &Context<'_>| {
let top = [Rank::A, Rank::K, Rank::Q]
.into_iter()
.filter(|&rank| hand[minor].contains(rank))
.count() as u8;
top >= GAMBLING_3NT_TOP_HONORS.with(Cell::get)
})
}
fn gambling_3nt_suit_ace(minor: Suit) -> Cons<impl Constraint + Clone> {
described("the suit ace", move |hand: Hand, _: &Context<'_>| {
!GAMBLING_3NT_REQUIRE_ACE.with(Cell::get) || hand[minor].contains(Rank::A)
})
}
fn preempt_4m_authored() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, _: &Context<'_>| PREEMPT_4M_OVER_DOUBLE.with(Cell::get))
}
fn preempt_4m_values() -> Cons<impl Constraint + Clone> {
described("a modest opening", |hand: Hand, _: &Context<'_>| {
let hcp: u8 = Suit::ASC
.iter()
.map(|&suit| holding_hcp::<u8>(hand[suit]))
.sum();
hcp >= PREEMPT_4M_FLOOR.with(Cell::get)
})
}
fn preempt_4m_semisolid(major: Suit) -> Cons<impl Constraint + Clone> {
described("a semi-solid major", move |hand: Hand, _: &Context<'_>| {
let top = [Rank::A, Rank::K, Rank::Q]
.into_iter()
.filter(|&rank| hand[major].contains(rank))
.count() as u8;
top >= PREEMPT_4M_TOP_HONORS.with(Cell::get)
})
}
fn preempt_4m_trump_ace(major: Suit) -> Cons<impl Constraint + Clone> {
described("the trump ace", move |hand: Hand, _: &Context<'_>| {
!PREEMPT_4M_REQUIRE_ACE.with(Cell::get) || hand[major].contains(Rank::A)
})
}
#[doc(hidden)]
pub fn set_one_nt_runout_universal(enabled: bool) {
ONE_NT_RUNOUT_UNIVERSAL.with(|flag| flag.set(enabled));
}
fn one_nt_runout_universal() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, _: &Context<'_>| ONE_NT_RUNOUT_UNIVERSAL.with(Cell::get))
}
#[doc(hidden)]
pub fn set_unusual_2nt(mode: Unusual2nt) {
UNUSUAL_2NT.with(|cell| cell.set(mode));
}
fn unusual_2nt_is(mode: Unusual2nt) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, _: &Context<'_>| UNUSUAL_2NT.with(Cell::get) == mode)
}
#[doc(hidden)]
pub fn set_penalize_escape_stack(enabled: bool) {
PENALIZE_ESCAPE_STACK.with(|flag| flag.set(enabled));
}
fn penalize_escape_stack_enabled() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, _: &Context<'_>| PENALIZE_ESCAPE_STACK.with(Cell::get))
}
#[doc(hidden)]
pub fn set_penalize_escape_values(enabled: bool) {
PENALIZE_ESCAPE_VALUES.with(|flag| flag.set(enabled));
}
fn penalize_escape_values_enabled() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, _: &Context<'_>| PENALIZE_ESCAPE_VALUES.with(Cell::get))
}
#[doc(hidden)]
pub fn set_uvu_encircle(enabled: bool) {
UVU_ENCIRCLE.with(|flag| flag.set(enabled));
}
fn uvu_encircle_enabled() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, _: &Context<'_>| UVU_ENCIRCLE.with(Cell::get))
}
fn responder_one_nt_runout_now(context: &Context<'_>) -> bool {
let auction = context.auction();
let n = auction.len();
our_strong_notrump(context, 1, true)
&& auction.last() == Some(&Call::Double)
&& n >= 2
&& matches!(auction[n - 2], Call::Bid(bid) if bid == Bid::new(1, Strain::Notrump))
}
fn responder_one_nt_runout() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| responder_one_nt_runout_now(context))
}
fn opener_after_one_nt_runout_now(context: &Context<'_>) -> bool {
let auction = context.auction();
if !our_strong_notrump(context, 1, false) {
return false;
}
let Some((index, _)) = opening_bid(auction) else {
return false;
};
auction.get(index + 1) == Some(&Call::Double)
&& matches!(
auction.get(index + 2),
Some(&Call::Bid(bid)) if bid.strain.suit().is_some()
)
}
fn opener_after_one_nt_runout() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| opener_after_one_nt_runout_now(context))
}
fn opener_after_one_nt_minors_now(context: &Context<'_>) -> bool {
let auction = context.auction();
if !our_strong_notrump(context, 1, false) {
return false;
}
let Some((index, _)) = opening_bid(auction) else {
return false;
};
auction.get(index + 1) == Some(&Call::Double)
&& auction.get(index + 2) == Some(&Call::Bid(Bid::new(2, Strain::Notrump)))
}
fn opener_after_one_nt_minors() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| opener_after_one_nt_minors_now(context))
}
fn longer_diamonds() -> Cons<impl Constraint + Clone> {
pred(|hand: Hand, _: &Context<'_>| hand[Suit::Diamonds].len() >= hand[Suit::Clubs].len())
}
fn opener_balancing_runout_now(context: &Context<'_>) -> bool {
if !our_strong_notrump(context, 1, false) {
return false;
}
let auction = context.auction();
let Some((index, _)) = opening_bid(auction) else {
return false;
};
auction.len() == index + 4
&& auction.get(index + 1) == Some(&Call::Double)
&& auction[index + 2] == Call::Pass
&& auction[index + 3] == Call::Pass
}
fn opener_balancing_runout() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| opener_balancing_runout_now(context))
}
fn responder_after_opener_sos_now(context: &Context<'_>) -> bool {
if !our_strong_notrump(context, 1, true) {
return false;
}
let auction = context.auction();
let Some((index, _)) = opening_bid(auction) else {
return false;
};
auction.len() >= index + 6
&& auction.get(index + 1) == Some(&Call::Double)
&& auction[index + 2] == Call::Pass
&& auction[index + 3] == Call::Pass
&& auction[index + 4] == Call::Redouble
&& auction[index + 5..].iter().all(|&call| call == Call::Pass)
}
fn responder_after_opener_sos() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| responder_after_opener_sos_now(context))
}
fn opener_after_responder_sos_now(context: &Context<'_>) -> bool {
if !our_strong_notrump(context, 1, false) {
return false;
}
let auction = context.auction();
let Some((index, _)) = opening_bid(auction) else {
return false;
};
auction.len() >= index + 8
&& auction.get(index + 1) == Some(&Call::Double)
&& auction[index + 2] == Call::Pass
&& auction[index + 3] == Call::Pass
&& auction[index + 4] == Call::Redouble
&& auction[index + 5] == Call::Pass
&& matches!(auction[index + 6], Call::Bid(bid) if bid.strain.suit().is_some())
&& auction[index + 7..].iter().all(|&call| call == Call::Pass)
}
fn opener_after_responder_sos() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| opener_after_responder_sos_now(context))
}
fn our_doubled_one_nt_escape(context: &Context<'_>) -> Option<usize> {
let auction = context.auction();
let (index, bid) = opening_bid(auction)?;
if index % 2 != auction.len() % 2 || bid != Bid::new(1, Strain::Notrump) {
return None;
}
if auction.get(index + 1) != Some(&Call::Double) {
return None;
}
let we_only_doubled = auction
.iter()
.enumerate()
.skip(index + 1)
.filter(|(i, _)| i % 2 == index % 2)
.all(|(_, &call)| !matches!(call, Call::Bid(_)));
if !we_only_doubled {
return None;
}
context
.last_bid()
.filter(|bid| bid.strain.suit().is_some() && bid.level.get() <= 3)?;
Some(index)
}
fn opp_escaped_our_nt_undoubled() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| {
our_doubled_one_nt_escape(context).is_some() && context.penalty() == Penalty::Undoubled
})
}
fn opp_escaped_our_business_xx() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| {
our_doubled_one_nt_escape(context).is_some_and(|index| {
context.penalty() == Penalty::Undoubled
&& context.auction().get(index + 2) == Some(&Call::Redouble)
})
})
}
fn leave_in_escape_penalty() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| {
our_doubled_one_nt_escape(context).is_some() && context.penalty() == Penalty::Doubled
})
}
fn our_uvu_penalty_escape(context: &Context<'_>) -> Option<usize> {
let auction = context.auction();
let (index, bid) = opening_bid(auction)?;
if index % 2 != auction.len() % 2 || bid != Bid::new(1, Strain::Notrump) {
return None;
}
if auction.get(index + 1) != Some(&Call::Bid(Bid::new(2, Strain::Notrump)))
|| auction.get(index + 2) != Some(&Call::Double)
{
return None;
}
let we_only_doubled = auction
.iter()
.enumerate()
.skip(index + 1)
.filter(|(i, _)| i % 2 == index % 2)
.all(|(_, &call)| !matches!(call, Call::Bid(_)));
if !we_only_doubled {
return None;
}
context
.last_bid()
.filter(|bid| bid.strain.suit().is_some() && bid.level.get() <= 3)?;
Some(index)
}
fn opp_escaped_our_uvu_undoubled() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| {
our_uvu_penalty_escape(context).is_some() && context.penalty() == Penalty::Undoubled
})
}
fn leave_in_uvu_penalty() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| {
our_uvu_penalty_escape(context).is_some() && context.penalty() == Penalty::Doubled
})
}
fn advancing_a_double_now(context: &Context<'_>) -> bool {
let auction = context.auction();
let n = auction.len();
n >= 2
&& auction[n - 1] == Call::Pass
&& auction[n - 2] == Call::Double
&& context
.last_bid()
.is_some_and(|bid| bid.strain.suit().is_some() && bid.level.get() <= 3)
}
fn advancing_a_double() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| advancing_a_double_now(context))
}
fn has_fit(hand: Hand, context: &Context<'_>) -> bool {
let inferences = Inferences::read(context);
let partner = inferences.partner();
Suit::ASC
.iter()
.any(|&suit| hand[suit].len() + usize::from(partner.length(suit).min) >= 8)
}
fn free_bid_gate(level: u8) -> Cons<impl Constraint + Clone> {
pred(move |hand: Hand, context: &Context<'_>| {
level < 4
|| !SETTLE_FLOOR.with(Cell::get)
|| (point_count(hand) >= 11 && !has_fit(hand, context))
})
}
fn doubled_suit_length() -> Cons<impl Constraint + Clone> {
pred(|hand: Hand, context: &Context<'_>| {
context
.last_bid()
.and_then(|bid| bid.strain.suit())
.is_some_and(|suit| hand[suit].len() >= 4)
})
}
fn doubled_suit_len(range: core::ops::RangeInclusive<usize>) -> Cons<impl Constraint + Clone> {
pred(move |hand: Hand, context: &Context<'_>| {
context
.last_bid()
.and_then(|bid| bid.strain.suit())
.is_some_and(|suit| range.contains(&hand[suit].len()))
})
}
fn doubled_suit_stack() -> Cons<impl Constraint + Clone> {
pred(|hand: Hand, context: &Context<'_>| {
context
.last_bid()
.and_then(|bid| bid.strain.suit())
.is_some_and(|suit| {
let holding = hand[suit];
let honors = [Rank::A, Rank::K, Rank::Q]
.into_iter()
.filter(|&rank| holding.contains(rank))
.count();
holding.len() >= 4 && honors >= 2
})
})
}
fn we_have_not_bid() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| {
!Suit::ASC
.into_iter()
.map(Strain::from)
.chain([Strain::Notrump])
.any(|strain| context.we_bid(strain))
})
}
fn i_bid_suit(suit: Suit) -> Cons<impl Constraint + Clone> {
let strain = Strain::from(suit);
pred(move |_: Hand, context: &Context<'_>| {
let auction = context.auction();
let len = auction.len();
auction.iter().enumerate().any(|(index, &call)| {
(len - index).is_multiple_of(4)
&& matches!(call, Call::Bid(bid) if bid.strain == strain)
})
})
}
fn their_live_bid_at_most(level: u8) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| {
context.penalty() == Penalty::Undoubled
&& context
.last_bid()
.is_some_and(|bid| bid.strain.suit().is_some() && bid.level.get() <= level)
&& context
.auction()
.iter()
.rposition(|&call| call != Call::Pass)
.is_some_and(|index| (context.auction().len() - index) % 2 == 1)
})
}
fn level_available(level: u8, strain: Strain) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| {
context
.min_level(strain)
.is_some_and(|min| min.get() <= level)
})
}
fn opening_bid(auction: &[Call]) -> Option<(usize, Bid)> {
let index = auction.iter().position(|&call| call != Call::Pass)?;
match auction[index] {
Call::Bid(bid) => Some((index, bid)),
_ => None,
}
}
fn our_strong_notrump(context: &Context<'_>, level: u8, partner: bool) -> bool {
let auction = context.auction();
let Some((index, bid)) = opening_bid(auction) else {
return false;
};
if index % 2 != auction.len() % 2 {
return false;
}
if bid.strain != Strain::Notrump || bid.level.get() != level {
return false;
}
match (auction.len() - index) % 4 {
0 => !partner,
2 => partner,
_ => false,
}
}
fn partner_last_call(auction: &[Call]) -> Option<Bid> {
match auction.len().checked_sub(2).map(|i| auction[i]) {
Some(Call::Bid(bid)) => Some(bid),
_ => None,
}
}
fn below_game() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| below_game_now(context))
}
fn correct_3nt_to_major_now() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| {
CORRECT_3NT_TO_MAJOR.with(Cell::get)
&& context.last_bid() == Some(Bid::new(3, Strain::Notrump))
})
}
fn has_ruffing_shortness() -> Cons<impl Constraint + Clone> {
pred(|hand: Hand, _: &Context<'_>| Suit::ASC.iter().any(|&suit| hand[suit].len() <= 2))
}
fn below_game_now(context: &Context<'_>) -> bool {
context.last_bid().is_none_or(|bid| {
let level = bid.level.get();
level <= 2 || (level == 3 && bid.strain != Strain::Notrump)
})
}
fn below_slam() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| context.last_bid().is_none_or(|bid| bid.level.get() <= 5))
}
fn keycard_trump(hand: Hand, context: &Context<'_>) -> Option<Suit> {
let inferences = Inferences::read(context);
let partner = inferences.partner();
#[allow(clippy::cast_possible_truncation)]
[Suit::Hearts, Suit::Spades]
.into_iter()
.map(|suit| (hand[suit].len() as u8 + partner.length(suit).min, suit))
.filter(|&(total, suit)| total >= 8 && hand[suit].len() >= 3)
.max_by_key(|&(total, suit)| (total, suit as u8))
.map(|(_, suit)| suit)
}
fn answer_trump(hand: Hand, context: &Context<'_>) -> Option<Suit> {
keycard_trump(hand, context).or_else(|| {
let inferences = Inferences::read(context);
let shown = |suit: Suit| {
inferences
.partner()
.length(suit)
.min
.max(inferences.me().length(suit).min)
};
[Suit::Hearts, Suit::Spades]
.into_iter()
.filter(|&suit| shown(suit) >= 5)
.max_by_key(|&suit| (shown(suit), suit as u8))
})
}
fn keycard_asked(hand: Hand, context: &Context<'_>) -> Option<Suit> {
if !floor_rkcb_now() || !context.undisturbed() {
return None;
}
let auction = context.auction();
let n = auction.len();
if partner_last_call(auction) != Some(Bid::new(4, Strain::Notrump)) {
return None;
}
let (opening_index, _) = opening_bid(auction)?;
if opening_index == n - 2 {
return None;
}
if n >= 4 && matches!(auction[n - 4], Call::Bid(bid) if bid.strain == Strain::Notrump) {
return None;
}
answer_trump(hand, context)
}
fn keycard_answer(counts: &'static [usize], queen: Option<bool>) -> Cons<impl Constraint + Clone> {
use super::american::slam::count_keycards;
described(
"the matching 1430 keycard answer",
move |hand: Hand, context: &Context<'_>| {
keycard_asked(hand, context).is_some_and(|trump| {
counts.contains(&count_keycards(hand, trump))
&& queen.is_none_or(|wanted| hand[trump].contains(Rank::Q) == wanted)
})
},
)
}
fn keycard_answered(hand: Hand, context: &Context<'_>) -> Option<(Suit, usize)> {
use super::american::slam::count_keycards;
if !floor_rkcb_now() || !context.undisturbed() {
return None;
}
let auction = context.auction();
let n = auction.len();
if n < 4 || auction[n - 4] != Call::Bid(Bid::new(4, Strain::Notrump)) {
return None;
}
let Some(&Call::Bid(answer)) = auction.get(n - 2) else {
return None;
};
if answer.level.get() != 5 {
return None;
}
let answer_suit = answer.strain.suit()?;
let trump = keycard_trump(hand, context)?;
let mine = count_keycards(hand, trump);
let (low, high) = match answer_suit {
Suit::Clubs => (1, 4),
Suit::Diamonds => (0, 3),
Suit::Hearts | Suit::Spades => (2, 2),
};
let partners = if mine + high > 5 { low } else { high };
Some((trump, mine + partners))
}
fn keycard_total(
trump: Suit,
range: impl core::ops::RangeBounds<usize> + Clone + Send + Sync + 'static,
) -> Cons<impl Constraint + Clone> {
pred(move |hand: Hand, context: &Context<'_>| {
keycard_answered(hand, context)
.is_some_and(|(t, total)| t == trump && range.contains(&total))
})
}
fn answer_is_five_of(trump: Suit) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| {
context.last_bid() == Some(Bid::new(5, Strain::from(trump)))
})
}
fn respect_keycard_signoff() -> Cons<impl Constraint + Clone> {
use super::american::slam::count_keycards;
pred(|hand: Hand, context: &Context<'_>| {
if !floor_rkcb_now() || !context.undisturbed() {
return false;
}
let auction = context.auction();
let n = auction.len();
if n < 6 || auction[n - 6] != Call::Bid(Bid::new(4, Strain::Notrump)) {
return false;
}
let (Call::Bid(answer), Call::Bid(signoff)) = (auction[n - 4], auction[n - 2]) else {
return false;
};
if answer.level.get() != 5 || !answer.strain.is_suit() {
return false;
}
let Some(trump) = signoff.strain.suit() else {
return false;
};
count_keycards(hand, trump) <= 1
})
}
fn partner_control_bid(trump: Suit) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| {
if !super::inference::control_bid_reading() || !context.undisturbed() {
return false;
}
let n = context.auction().len();
n >= 2
&& Inferences::read(context)
.control_bid()
.is_some_and(|(index, agreed)| usize::from(index) == n - 2 && agreed == trump)
})
}
fn known_eight_card_fit(suit: Suit) -> Cons<impl Constraint + Clone> {
(len(suit, 5..) & partner_shown_len(suit, 3..))
| (len(suit, 3..) & partner_shown_len(suit, 5..))
| (len(suit, 2..) & partner_shown_len(suit, 6..))
}
fn combined_points(threshold: u8) -> Cons<impl Constraint + Clone> {
pred(move |hand: Hand, context: &Context<'_>| {
let partner_min = Inferences::read(context).partner().points.min;
u16::from(point_count(hand)) + u16::from(partner_min) >= u16::from(threshold)
})
}
fn partner_strong_notrump(level: u8) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| our_strong_notrump(context, level, true))
}
fn opener_forced_past_invitation(context: &Context<'_>) -> bool {
(our_strong_notrump(context, 1, false) || our_strong_notrump(context, 2, false))
&& partner_last_call(context.auction())
.is_some_and(|bid| bid.level.get() == 3 && bid.strain != Strain::Notrump)
}
fn forcing_two_clubs_response(context: &Context<'_>) -> bool {
let auction = context.auction();
let Some((index, bid)) = opening_bid(auction) else {
return false;
};
if index % 2 != auction.len() % 2 {
return false;
}
if bid != Bid::new(2, Strain::Clubs) {
return false;
}
matches!(
auction.get(index + 2),
Some(&Call::Bid(response)) if response != Bid::new(2, Strain::Hearts)
)
}
fn penalizing(context: &Context<'_>) -> bool {
let auction = context.auction();
context.penalty() != Penalty::Undoubled
&& auction
.iter()
.rposition(|call| matches!(call, Call::Bid(_)))
.is_some_and(|index| (auction.len() - index) % 2 == 1)
}
#[derive(Clone, Copy, Debug)]
struct Interpretation {
forced_to_game: bool,
penalizing: bool,
}
impl Interpretation {
fn read(context: &Context<'_>) -> Self {
Self {
forced_to_game: forcing_two_clubs_response(context)
|| opener_forced_past_invitation(context),
penalizing: penalizing(context),
}
}
}
fn auction_forces_game() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| Interpretation::read(context).forced_to_game)
}
fn not_penalizing() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| !Interpretation::read(context).penalizing)
}
fn undisturbed() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| context.undisturbed())
}
#[doc(hidden)]
pub fn set_penalty_latch(enabled: bool) {
PENALTY_LATCH.with(|flag| flag.set(enabled));
}
fn penalty_latched(context: &Context<'_>) -> bool {
if !PENALTY_LATCH.with(Cell::get) {
return false;
}
let auction = context.auction();
let Some(double_index) = super::inference::penalty_x_reading(auction) else {
return false;
};
double_index % 2 == auction.len() % 2
}
pub(super) fn penalty_latch_enabled() -> bool {
PENALTY_LATCH.with(Cell::get)
}
fn penalty_latched_c() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| penalty_latched(context))
}
#[doc(hidden)]
pub fn set_penalty_no_pull(enabled: bool) {
PENALTY_NO_PULL.with(|flag| flag.set(enabled));
}
fn may_pull_penalty() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| {
!(PENALTY_NO_PULL.with(Cell::get) && penalty_latched(context))
})
}
fn not_penalty_latched() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| !penalty_latched(context))
}
#[doc(hidden)]
pub fn set_latch_style(style: LatchStyle) {
LATCH_STYLE.with(|cell| cell.set(style));
}
fn latch_optional_c() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, _: &Context<'_>| LATCH_STYLE.with(Cell::get) == LatchStyle::Optional)
}
fn latch_penalty_c() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, _: &Context<'_>| LATCH_STYLE.with(Cell::get) == LatchStyle::Penalty)
}
#[doc(hidden)]
pub fn set_advancer_xx_runout(enabled: bool) {
ADVANCER_XX_RUNOUT.with(|flag| flag.set(enabled));
}
fn advancer_xx_runout_now(context: &Context<'_>) -> bool {
if !ADVANCER_XX_RUNOUT.with(Cell::get) {
return false;
}
let auction = context.auction();
let Some(x_index) = super::inference::penalty_x_reading(auction) else {
return false;
};
auction.len() == x_index + 2
&& auction.last() == Some(&Call::Redouble)
&& x_index % 2 == auction.len() % 2
}
fn advancer_xx_runout() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| advancer_xx_runout_now(context))
}
#[doc(hidden)]
pub fn set_doubler_xx_runout(enabled: bool) {
DOUBLER_XX_RUNOUT.with(|flag| flag.set(enabled));
}
fn doubler_xx_runout_enabled() -> bool {
DOUBLER_XX_RUNOUT.with(Cell::get)
}
fn doubler_xx_runout_now(context: &Context<'_>) -> bool {
let auction = context.auction();
let Some(x_index) = super::inference::penalty_x_reading(auction) else {
return false;
};
auction.len() == x_index + 4
&& auction[x_index + 1] == Call::Redouble
&& auction[x_index + 2] == Call::Pass
&& auction[x_index + 3] == Call::Pass
}
fn doubler_xx_runout() -> Cons<impl Constraint + Clone> {
pred(|_: Hand, context: &Context<'_>| doubler_xx_runout_now(context))
}
fn partner_transferred_now(context: &Context<'_>, from: Bid, nt_level: u8) -> bool {
our_strong_notrump(context, nt_level, false)
&& partner_last_call(context.auction()) == Some(from)
}
fn partner_transferred(from: Bid, nt_level: u8) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| partner_transferred_now(context, from, nt_level))
}
const TRANSFERS: [(u8, Bid, Bid); 6] = [
(
1,
Bid::new(2, Strain::Diamonds),
Bid::new(2, Strain::Hearts),
),
(1, Bid::new(2, Strain::Hearts), Bid::new(2, Strain::Spades)),
(1, Bid::new(4, Strain::Clubs), Bid::new(4, Strain::Hearts)),
(
1,
Bid::new(4, Strain::Diamonds),
Bid::new(4, Strain::Spades),
),
(
2,
Bid::new(3, Strain::Diamonds),
Bid::new(3, Strain::Hearts),
),
(2, Bid::new(3, Strain::Hearts), Bid::new(3, Strain::Spades)),
];
#[cfg(feature = "neural-floor")]
pub(crate) fn forced(context: &Context<'_>) -> bool {
advancing_a_double_now(context)
|| Interpretation::read(context).forced_to_game
|| TRANSFERS
.iter()
.any(|&(nt_level, from, _)| partner_transferred_now(context, from, nt_level))
}
pub(crate) fn overcall_shape(auction: &[Call]) -> Option<(Suit, Suit, usize, u8)> {
let (open_index, opening) = opening_bid(auction)?;
let x = opening.strain.suit()?;
if opening.level.get() != 1 {
return None;
}
let opening_side = open_index % 2;
let overcall_index =
(open_index + 1..auction.len()).find(|&i| matches!(auction[i], Call::Bid(_)))?;
if overcall_index % 2 == opening_side {
return None;
}
if auction[open_index + 1..overcall_index].contains(&Call::Double) {
return None;
}
let Call::Bid(overcall) = auction[overcall_index] else {
return None;
};
let y = overcall.strain.suit()?;
if y == x {
return None;
}
let simple = if (y as u8) > (x as u8) { 1 } else { 2 };
(overcall.level.get() == simple).then_some((x, y, overcall_index, simple))
}
fn advance_of_overcall(context: &Context<'_>) -> Option<(Suit, Suit, u8)> {
let auction = context.auction();
let (x, y, overcall_index, level) = overcall_shape(auction)?;
(overcall_index + 2 == auction.len() && auction[auction.len() - 1] == Call::Pass)
.then_some((x, y, level))
}
fn rubens_transfer(source: Suit, into_partner: bool) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| {
rubens_advances_enabled()
&& advance_of_overcall(context).is_some_and(|(x, y, level)| {
level == 1
&& (x as u8) <= (source as u8)
&& (source as u8) < (y as u8)
&& (source as u8 + 1 == y as u8) == into_partner
})
})
}
fn rubens_cue_raise(cue: Suit) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| {
rubens_advances_enabled()
&& advance_of_overcall(context)
.is_some_and(|(x, _, level)| level == 2 && x as u8 == cue as u8)
})
}
fn rubens_completion(context: &Context<'_>) -> Option<Suit> {
if !rubens_advances_enabled() {
return None;
}
let auction = context.auction();
let len = auction.len();
let (x, y, overcall_index, level) = overcall_shape(auction)?;
if level != 1
|| overcall_index + 4 != len
|| auction[overcall_index + 1] != Call::Pass
|| !matches!(auction[len - 1], Call::Pass | Call::Double)
{
return None;
}
let Call::Bid(transfer) = auction[overcall_index + 2] else {
return None;
};
let source = transfer.strain.suit()?;
(transfer.level.get() == 2 && (x as u8) <= (source as u8) && (source as u8) < (y as u8))
.then(|| Suit::ASC[(source as u8 + 1) as usize])
}
fn rubens_completes(target: Suit) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| rubens_completion(context) == Some(target))
}
fn rubens_cue_answer(context: &Context<'_>) -> Option<(Suit, Suit)> {
if !rubens_advances_enabled() {
return None;
}
let auction = context.auction();
let len = auction.len();
let (x, y, overcall_index, level) = overcall_shape(auction)?;
if level != 2
|| overcall_index + 4 != len
|| auction[overcall_index + 1] != Call::Pass
|| !matches!(auction[len - 1], Call::Pass | Call::Double)
{
return None;
}
(auction[overcall_index + 2] == Call::Bid(Bid::new(2, Strain::from(x)))).then_some((x, y))
}
fn rubens_cue_answers(y: Suit) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| {
rubens_cue_answer(context).is_some_and(|(_, own)| own == y)
})
}
fn rubens_into_partner(y: Suit) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| {
rubens_completion(context) == Some(y)
&& overcall_shape(context.auction()).is_some_and(|(_, own, _, _)| own == y)
})
}
fn rubens_raiser_rebid(context: &Context<'_>) -> Option<Suit> {
if !rubens_advances_enabled() {
return None;
}
let auction = context.auction();
let len = auction.len();
let (x, y, overcall_index, level) = overcall_shape(auction)?;
if level != 1 || overcall_index + 6 != len || auction[overcall_index + 1] != Call::Pass {
return None;
}
let Call::Bid(transfer) = auction[overcall_index + 2] else {
return None;
};
let source = transfer.strain.suit()?;
if transfer.level.get() != 2 || (source as u8) < (x as u8) || source as u8 + 1 != y as u8 {
return None;
}
(matches!(auction[overcall_index + 3], Call::Pass | Call::Double)
&& auction[overcall_index + 4] == Call::Bid(Bid::new(2, Strain::from(y)))
&& matches!(auction[len - 1], Call::Pass | Call::Double))
.then_some(y)
}
fn rubens_raiser_rebids(y: Suit) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| rubens_raiser_rebid(context) == Some(y))
}
fn rubens_transferee_rebid(context: &Context<'_>) -> Option<Suit> {
if !rubens_advances_enabled() {
return None;
}
let auction = context.auction();
let len = auction.len();
let (x, y, overcall_index, level) = overcall_shape(auction)?;
if level != 1 || overcall_index + 6 != len || auction[overcall_index + 1] != Call::Pass {
return None;
}
let Call::Bid(transfer) = auction[overcall_index + 2] else {
return None;
};
let source = transfer.strain.suit()?;
if transfer.level.get() != 2 || (source as u8) < (x as u8) || (source as u8) >= (y as u8) {
return None;
}
let target = Suit::ASC[(source as u8 + 1) as usize];
if target == y {
return None;
}
(matches!(auction[overcall_index + 3], Call::Pass | Call::Double)
&& auction[overcall_index + 4] == Call::Bid(Bid::new(2, Strain::from(target)))
&& matches!(auction[len - 1], Call::Pass | Call::Double))
.then_some(target)
}
fn rubens_transferee_rebids(target: Suit) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| rubens_transferee_rebid(context) == Some(target))
}
fn rubens_new_suit_completion(target: Suit) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| {
rubens_completion(context) == Some(target)
&& overcall_shape(context.auction()).is_some_and(|(_, own, _, _)| own != target)
})
}
fn natural_new_suit_advance(target: Suit) -> Cons<impl Constraint + Clone> {
pred(move |_: Hand, context: &Context<'_>| {
!rubens_advances_enabled()
&& advance_of_overcall(context).is_some_and(|(x, y, level)| {
level == 1 && (x as u8) < (target as u8) && (target as u8) < (y as u8)
})
})
}
#[must_use]
pub fn instinct() -> Rules {
let mut rules = Rules::new()
.rule(Call::Pass, 1.5, advancing_a_double() & doubled_suit_stack())
.rule(
Call::Pass,
1.35,
settle_floor() & advancing_a_double() & doubled_suit_length(),
)
.rule(
Bid::new(3, Strain::Notrump),
1.3,
advancing_a_double()
& hcp(13..)
& stopper_in_their_suits()
& level_available(3, Strain::Notrump),
)
.rule(Call::Pass, 0.0, !advancing_a_double() | settle_floor())
.rule(Call::Pass, -5.0, hcp(0..));
for major in [Suit::Hearts, Suit::Spades] {
let strain = Strain::from(major);
rules = rules.rule(
Bid::new(4, strain),
1.45,
advancing_a_double()
& len(major, 4..)
& points(11..)
& level_available(4, strain)
& !they_bid(strain),
);
}
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let strain = Strain::from(suit);
let major_bonus = if matches!(suit, Suit::Hearts | Suit::Spades) {
0.05
} else {
0.0
};
for level in 1u8..=4 {
rules = rules
.rule(
Bid::new(level, strain),
1.0 + major_bonus,
advancing_a_double()
& min_level_is(level, strain)
& len(suit, 4..)
& !they_bid(strain)
& free_bid_gate(level),
)
.rule(
Bid::new(level, strain),
1.1 + major_bonus,
advancing_a_double()
& min_level_is(level, strain)
& len(suit, 5..)
& !they_bid(strain)
& free_bid_gate(level),
);
}
for (level, threshold) in [(2u8, 6u8), (3, 10), (4, 13)] {
rules = rules.rule(
Bid::new(level, strain),
1.2,
partner_suit_is(suit)
& partner_shown_len(suit, 3..)
& min_level_is(level, strain)
& support(3..)
& points(threshold..),
);
}
rules = rules.rule(
Bid::new(4, strain),
1.3,
partner_suit_is(suit)
& partner_shown_len(suit, 3..)
& support(5..)
& hcp(..6)
& level_available(4, strain),
);
for (level, floor) in [(1u8, 8u8), (2, 10), (3, 13)] {
rules = rules.rule(
Bid::new(level, strain),
1.0 + major_bonus,
we_have_not_bid()
& may_pull_penalty()
& min_level_is(level, strain)
& len(suit, 5..)
& points(floor..=16)
& !they_bid(strain),
);
}
}
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let strain = Strain::from(suit);
let major_bonus = if matches!(suit, Suit::Hearts | Suit::Spades) {
0.05
} else {
0.0
};
rules = rules
.rule(
Bid::new(2, strain),
1.0 + major_bonus,
one_nt_runout_enabled()
& responder_one_nt_runout()
& len(suit, 5..)
& hcp(..RUNOUT_MAX_HCP),
)
.rule(
Bid::new(2, strain),
1.1 + major_bonus,
one_nt_runout_enabled()
& responder_one_nt_runout()
& len(suit, 6..)
& hcp(..RUNOUT_MAX_HCP),
);
}
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let strain = Strain::from(suit);
let major_bonus = if matches!(suit, Suit::Hearts | Suit::Spades) {
0.05
} else {
0.0
};
rules = rules
.rule(
Bid::new(2, strain),
1.0 + major_bonus,
advancer_xx_runout() & len(suit, 5..) & hcp(..RUNOUT_MAX_HCP),
)
.rule(
Bid::new(2, strain),
1.1 + major_bonus,
advancer_xx_runout() & len(suit, 6..) & hcp(..RUNOUT_MAX_HCP),
);
}
if doubler_xx_runout_enabled() {
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let strain = Strain::from(suit);
let major_bonus = if matches!(suit, Suit::Hearts | Suit::Spades) {
0.05
} else {
0.0
};
rules = rules
.rule(
Bid::new(2, strain),
1.0 + major_bonus,
doubler_xx_runout() & len(suit, 5..),
)
.rule(
Bid::new(2, strain),
1.1 + major_bonus,
doubler_xx_runout() & len(suit, 6..),
);
}
}
rules = rules.rule(
Call::Redouble,
1.2,
one_nt_runout_enabled() & responder_one_nt_runout() & responder_has_xx_values(),
);
rules = rules.rule(
Bid::new(2, Strain::Notrump),
0.5,
one_nt_runout_enabled()
& responder_one_nt_runout()
& !unusual_2nt_is(Unusual2nt::Direct)
& hcp(..RUNOUT_MAX_HCP)
& len(Suit::Clubs, 4..)
& len(Suit::Diamonds, 4..)
& len(Suit::Hearts, ..5)
& len(Suit::Spades, ..5),
);
rules = rules.rule(
Bid::new(2, Strain::Notrump),
1.15,
one_nt_runout_enabled()
& responder_one_nt_runout()
& unusual_2nt_is(Unusual2nt::FiveFiveAdd)
& hcp(..RUNOUT_MAX_HCP)
& len(Suit::Clubs, 5..)
& len(Suit::Diamonds, 5..),
);
let direct_bust = one_nt_runout_enabled()
& responder_one_nt_runout()
& unusual_2nt_is(Unusual2nt::Direct)
& hcp(..RUNOUT_MAX_HCP)
& len(Suit::Clubs, 4..)
& len(Suit::Diamonds, 4..)
& len(Suit::Hearts, ..5)
& len(Suit::Spades, ..5);
rules = rules
.rule(
Bid::new(2, Strain::Diamonds),
1.0,
direct_bust.clone() & longer_diamonds(),
)
.rule(
Bid::new(2, Strain::Clubs),
1.0,
direct_bust & !longer_diamonds(),
);
rules = rules.rule(
Call::Pass,
1.55,
one_nt_runout_enabled() & opener_after_one_nt_runout(),
);
rules = rules
.rule(
Bid::new(3, Strain::Diamonds),
1.6,
one_nt_runout_enabled() & opener_after_one_nt_minors() & longer_diamonds(),
)
.rule(
Bid::new(3, Strain::Clubs),
1.6,
one_nt_runout_enabled() & opener_after_one_nt_minors() & !longer_diamonds(),
);
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let strain = Strain::from(suit);
let major_bonus = if matches!(suit, Suit::Hearts | Suit::Spades) {
0.05
} else {
0.0
};
rules = rules
.rule(
Bid::new(2, strain),
1.0 + major_bonus,
one_nt_runout_enabled()
& one_nt_runout_universal()
& opener_balancing_runout()
& len(suit, 5..)
& hcp(..17),
)
.rule(
Bid::new(2, strain),
1.1 + major_bonus,
one_nt_runout_enabled()
& one_nt_runout_universal()
& opener_balancing_runout()
& len(suit, 6..)
& hcp(..17),
);
}
rules = rules.rule(
Call::Redouble,
1.0,
one_nt_runout_enabled()
& one_nt_runout_universal()
& opener_balancing_runout()
& hcp(..17)
& len(Suit::Clubs, ..5)
& len(Suit::Diamonds, ..5)
& len(Suit::Hearts, ..5)
& len(Suit::Spades, ..5),
);
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let strain = Strain::from(suit);
let major_bonus = if matches!(suit, Suit::Hearts | Suit::Spades) {
0.05
} else {
0.0
};
for (length, weight) in [(4usize, 1.0f32), (5, 1.1), (6, 1.2)] {
rules = rules.rule(
Bid::new(2, strain),
weight + major_bonus,
one_nt_runout_enabled()
& one_nt_runout_universal()
& responder_after_opener_sos()
& len(suit, length..),
);
}
}
rules = rules.rule(
Call::Pass,
1.55,
one_nt_runout_enabled() & one_nt_runout_universal() & opener_after_responder_sos(),
);
rules = rules
.rule(
Call::Double,
1.6,
one_nt_runout_enabled()
& penalize_escape_stack_enabled()
& opp_escaped_our_nt_undoubled()
& doubled_suit_stack(),
)
.rule(
Call::Double,
1.6,
one_nt_runout_enabled()
& penalize_escape_values_enabled()
& opp_escaped_our_business_xx()
& hcp(7..),
);
rules = rules.rule(
Call::Pass,
1.55,
one_nt_runout_enabled() & leave_in_escape_penalty(),
);
rules = rules.rule(
Call::Pass,
1.55,
penalty_latched_c() & latch_penalty_c() & advancing_a_double(),
);
rules = rules
.rule(
Call::Double,
1.6,
uvu_encircle_enabled() & opp_escaped_our_uvu_undoubled() & doubled_suit_stack(),
)
.rule(
Call::Pass,
1.55,
uvu_encircle_enabled() & leave_in_uvu_penalty(),
);
for level in 1u8..=4 {
rules = rules.rule(
Bid::new(level, Strain::Notrump),
0.3,
advancing_a_double() & min_level_is(level, Strain::Notrump),
);
}
for level in 1u8..=3 {
rules = rules.rule(
Bid::new(level, Strain::Notrump),
1.05,
we_have_not_bid()
& may_pull_penalty()
& min_level_is(level, Strain::Notrump)
& balanced()
& hcp(15..=18)
& stopper_in_their_suits(),
);
}
for (nt_level, from, to) in TRANSFERS {
rules = rules.rule(
to,
1.5,
partner_transferred(from, nt_level) & level_available(to.level.get(), to.strain),
);
}
let game_values = ((partner_strong_notrump(1)
& (hcp(10..) | (hcp(nt_responder_game_floor()..) & undisturbed())))
| (partner_strong_notrump(2) & hcp(5..))
| auction_forces_game()
| combined_points(25))
& not_penalizing();
rules = rules.rule(
Bid::new(3, Strain::Notrump),
1.40,
game_values.clone()
& below_game()
& stopper_in_their_suits()
& nt_game_force_3nt_allowed()
& level_available(3, Strain::Notrump),
);
for minor in [Suit::Clubs, Suit::Diamonds] {
rules = rules
.rule(
Bid::new(3, Strain::Notrump),
1.45,
one_nt_runout_enabled()
& responder_one_nt_runout()
& gambling_3nt_authored()
& len(minor, 6..)
& gambling_3nt_semisolid(minor)
& gambling_3nt_suit_ace(minor)
& level_available(3, Strain::Notrump),
)
.alert(GAMBLING_3NT);
}
for minor in [Suit::Clubs, Suit::Diamonds] {
let strain = Strain::from(minor);
let known_minor_fit = (len(minor, 5..) & partner_shown_len(minor, 3..))
| (len(minor, 3..) & partner_shown_len(minor, 5..));
rules = rules.rule(
Bid::new(5, strain),
1.42,
game_values.clone()
& below_game()
& inference_aware()
& known_minor_fit
& !stopper_in_their_suits()
& level_available(5, strain),
);
}
for major in [Suit::Hearts, Suit::Spades] {
let strain = Strain::from(major);
let known_major_fit = (len(major, 5..) & partner_shown_len(major, 3..))
| (len(major, 3..) & partner_shown_len(major, 5..))
| (len(major, 2..) & partner_shown_len(major, 6..));
rules = rules.rule(
Bid::new(4, strain),
1.45,
game_values.clone() & below_game() & len(major, 6..) & level_available(4, strain),
);
rules = rules.rule(
Bid::new(4, strain),
1.45,
one_nt_runout_enabled()
& responder_one_nt_runout()
& preempt_4m_authored()
& len(major, 6..)
& preempt_4m_semisolid(major)
& preempt_4m_trump_ace(major)
& preempt_4m_values()
& below_game()
& level_available(4, strain),
);
rules = rules.rule(
Bid::new(4, strain),
1.50,
game_values.clone()
& below_game()
& inference_aware()
& known_major_fit.clone()
& level_available(4, strain),
);
rules = rules.rule(
Bid::new(4, strain),
1.50,
correct_3nt_to_major_now()
& undisturbed()
& inference_aware()
& known_major_fit.clone()
& has_ruffing_shortness()
& level_available(4, strain),
);
rules = rules.rule(
Bid::new(6, strain),
1.65,
combined_points(33)
& not_penalizing()
& below_slam()
& inference_aware()
& known_major_fit.clone()
& level_available(6, strain),
);
rules = rules.rule(
Bid::new(7, strain),
1.75,
combined_points(37)
& not_penalizing()
& below_slam()
& inference_aware()
& known_major_fit
& level_available(7, strain),
);
}
rules = rules
.rule(
Bid::new(6, Strain::Notrump),
1.60,
combined_points(33)
& not_penalizing()
& below_slam()
& stopper_in_their_suits()
& level_available(6, Strain::Notrump),
)
.rule(
Bid::new(7, Strain::Notrump),
1.70,
combined_points(37)
& not_penalizing()
& below_slam()
& stopper_in_their_suits()
& level_available(7, Strain::Notrump),
);
rules = rules
.rule(
Bid::new(4, Strain::Notrump),
1.68,
pred(|hand: Hand, context: &Context<'_>| {
floor_rkcb_now()
&& context.undisturbed()
&& keycard_trump(hand, context).is_some_and(|trump| {
let inferences = Inferences::read(context);
inferences
.me()
.length(trump)
.min
.max(inferences.partner().length(trump).min)
>= 5
})
&& partner_last_call(context.auction())
.is_none_or(|bid| bid.strain != Strain::Notrump)
}) & inference_aware()
& combined_points(33)
& not_penalizing()
& below_slam()
& level_available(4, Strain::Notrump),
)
.alert(RKCB_FLOOR)
.rule(
Bid::new(5, Strain::Clubs),
1.9,
keycard_answer(&[1, 4, 5], None),
)
.alert(RKCB_FLOOR)
.rule(
Bid::new(5, Strain::Diamonds),
1.9,
keycard_answer(&[0, 3], None),
)
.alert(RKCB_FLOOR)
.rule(
Bid::new(5, Strain::Hearts),
1.9,
keycard_answer(&[2], Some(false)),
)
.alert(RKCB_FLOOR)
.rule(
Bid::new(5, Strain::Spades),
1.9,
keycard_answer(&[2], Some(true)),
)
.alert(RKCB_FLOOR)
.rule(Call::Pass, 1.88, respect_keycard_signoff());
for trump in Suit::ASC {
let strain = Strain::from(trump);
rules = rules
.rule(
Bid::new(7, strain),
1.86,
keycard_total(trump, 5..) & combined_points(37) & level_available(7, strain),
)
.rule(
Bid::new(6, strain),
1.84,
keycard_total(trump, 4..) & level_available(6, strain),
)
.rule(
Bid::new(5, strain),
1.82,
keycard_total(trump, ..=3) & level_available(5, strain),
)
.rule(
Call::Pass,
1.80,
keycard_total(trump, ..=3) & answer_is_five_of(trump),
)
.rule(
Bid::new(6, strain),
0.3,
keycard_total(trump, ..) & level_available(6, strain),
);
}
for trump in Suit::ASC {
let strain = Strain::from(trump);
for level in 4..=6 {
rules = rules.rule(
Bid::new(level, strain),
1.55,
partner_control_bid(trump)
& inference_aware()
& known_eight_card_fit(trump)
& min_level_is(level, strain),
);
}
}
for source in [Suit::Clubs, Suit::Diamonds, Suit::Hearts] {
let source_strain = Strain::from(source);
let target = Suit::ASC[(source as u8 + 1) as usize];
rules = rules
.rule(
Bid::new(2, source_strain),
1.35,
rubens_transfer(source, false)
& len(target, 5..)
& points(10..)
& min_level_is(2, source_strain),
)
.rule(
Bid::new(2, source_strain),
1.45,
rubens_transfer(source, true)
& support(3..)
& points(10..)
& min_level_is(2, source_strain),
);
}
for cue in [Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let cue_strain = Strain::from(cue);
rules = rules.rule(
Bid::new(2, cue_strain),
1.45,
rubens_cue_raise(cue) & support(3..) & points(10..) & min_level_is(2, cue_strain),
);
}
for target in [Suit::Diamonds, Suit::Hearts, Suit::Spades] {
rules = rules.rule(
Bid::new(2, Strain::from(target)),
1.55,
rubens_completes(target),
);
}
for y in [Suit::Hearts, Suit::Spades] {
rules = rules
.rule(
Bid::new(4, Strain::from(y)),
1.6,
rubens_into_partner(y) & points(15..),
)
.rule(
Bid::new(3, Strain::from(y)),
1.58,
rubens_into_partner(y) & points(13..15),
);
}
rules = rules.rule(
Bid::new(3, Strain::Notrump),
1.6,
rubens_into_partner(Suit::Diamonds) & points(15..) & stopper_in_their_suits(),
);
for target in [Suit::Diamonds, Suit::Hearts] {
rules = rules.rule(
Bid::new(3, Strain::from(target)),
1.58,
rubens_new_suit_completion(target) & len(target, 3..) & points(13..15),
);
}
rules = rules
.rule(
Bid::new(4, Strain::from(Suit::Hearts)),
1.6,
rubens_new_suit_completion(Suit::Hearts) & len(Suit::Hearts, 3..) & points(15..),
)
.rule(
Bid::new(3, Strain::Notrump),
1.6,
rubens_new_suit_completion(Suit::Diamonds) & points(15..) & stopper_in_their_suits(),
);
for y in [Suit::Hearts, Suit::Spades] {
rules = rules.rule(
Bid::new(4, Strain::from(y)),
1.5,
rubens_raiser_rebids(y) & points(14..),
);
}
rules = rules.rule(
Bid::new(3, Strain::Notrump),
1.5,
rubens_raiser_rebids(Suit::Diamonds) & points(14..) & stopper_in_their_suits(),
);
rules = rules.rule(
Bid::new(4, Strain::from(Suit::Hearts)),
1.52,
rubens_transferee_rebids(Suit::Hearts) & len(Suit::Hearts, 6..) & points(14..),
);
for target in [Suit::Diamonds, Suit::Hearts] {
rules = rules
.rule(
Bid::new(3, Strain::Notrump),
1.5,
rubens_transferee_rebids(target) & points(14..) & stopper_in_their_suits(),
)
.rule(
Bid::new(3, Strain::from(target)),
1.5,
rubens_transferee_rebids(target) & len(target, 6..) & points(12..14),
);
}
for y in [Suit::Clubs, Suit::Diamonds, Suit::Hearts] {
rules = rules.rule(Bid::new(3, Strain::from(y)), 1.5, rubens_cue_answers(y));
}
rules = rules.rule(
Bid::new(4, Strain::from(Suit::Hearts)),
1.55,
rubens_cue_answers(Suit::Hearts) & points(14..),
);
for y in [Suit::Clubs, Suit::Diamonds] {
rules = rules.rule(
Bid::new(3, Strain::Notrump),
1.55,
rubens_cue_answers(y) & points(14..) & stopper_in_their_suits(),
);
}
for target in [Suit::Diamonds, Suit::Hearts] {
let target_strain = Strain::from(target);
rules = rules.rule(
Bid::new(2, target_strain),
1.35,
natural_new_suit_advance(target)
& len(target, 5..)
& points(10..)
& min_level_is(2, target_strain),
);
}
if competitive_rebid_enabled() {
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
let strain = Strain::from(suit);
rules = rules.rule(
Bid::new(2, strain),
1.0,
their_live_bid_at_most(3)
& i_bid_suit(suit)
& min_level_is(2, strain)
& len(suit, 6..)
& !they_bid(strain)
& not_penalty_latched(),
);
rules = rules.rule(
Bid::new(3, strain),
1.0,
their_live_bid_at_most(3)
& i_bid_suit(suit)
& min_level_is(3, strain)
& len(suit, 6..)
& (len(suit, 7..) | top_honors(suit, 2..))
& !they_bid(strain)
& not_penalty_latched(),
);
}
}
rules
.rule(
Call::Double,
0.9,
their_live_bid_at_most(3)
& short_in_their_suits()
& hcp(12..)
& not_penalty_latched()
& takeout_double_shape_ok(),
)
.rule(
Call::Double,
0.8,
their_live_bid_at_most(3) & points(17..) & not_penalty_latched(),
)
.rule(
Call::Double,
1.6,
their_live_bid_at_most(3)
& penalty_latched_c()
& latch_penalty_c()
& doubled_suit_stack(),
)
.rule(
Call::Double,
1.6,
their_live_bid_at_most(3)
& penalty_latched_c()
& latch_optional_c()
& doubled_suit_len(2..=3)
& hcp(6..),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bidding::trie::Classifier;
use contract_bridge::auction::RelativeVulnerability;
const fn call(level: u8, strain: Strain) -> Call {
Call::Bid(Bid::new(level, strain))
}
fn best(auction: &[Call], hand: &str) -> Call {
let hand: Hand = hand.parse().expect("valid test hand");
let context = Context::new(RelativeVulnerability::NONE, auction);
let logits = instinct().classify(hand, &context);
(&logits.0)
.into_iter()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("logits are never NaN"))
.map(|(call, _)| call)
.expect("array is never empty")
}
fn american_floored(auction: &[Call], hand: &str) -> (Call, bool) {
use crate::bidding::Family;
use crate::bidding::american::american;
let hand: Hand = hand.parse().expect("valid test hand");
let (logits, provenance) = american()
.against(Family::NATURAL)
.classify_with_provenance(hand, RelativeVulnerability::NONE, auction)
.expect("a legal auction classifies");
let call = (&logits.0)
.into_iter()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("logits are never NaN"))
.map(|(call, _)| call)
.expect("array is never empty");
(call, provenance.depth == 0 && provenance.fallback.is_some())
}
#[test]
fn advancing_a_double_advances_a_bust_but_defends_with_length() {
let auction = [call(3, Strain::Clubs), Call::Double, Call::Pass];
assert_eq!(best(&auction, "96432.J85.9742.2"), call(3, Strain::Spades));
assert_eq!(best(&auction, "964.J85.974.9632"), Call::Pass);
}
#[test]
fn trump_stack_converts_to_penalties() {
let auction = [call(2, Strain::Spades), Call::Double, Call::Pass];
assert_eq!(best(&auction, "KQ92.A532.J42.96"), Call::Pass);
}
#[test]
fn competitive_rebid_shows_the_long_suit() {
let raised = [
call(1, Strain::Diamonds),
call(1, Strain::Hearts),
Call::Pass,
call(2, Strain::Hearts),
];
let one_suiter = "765.A.AKJT984.63";
set_competitive_rebid(false);
assert_eq!(best(&raised, one_suiter), Call::Double);
set_competitive_rebid(true);
assert_eq!(best(&raised, one_suiter), call(3, Strain::Diamonds));
assert_eq!(
american_floored(&raised, one_suiter),
(call(3, Strain::Diamonds), true)
);
let balancing = [
call(1, Strain::Diamonds),
call(1, Strain::Hearts),
Call::Pass,
Call::Pass,
];
assert_eq!(best(&balancing, one_suiter), call(2, Strain::Diamonds));
let major = [
call(1, Strain::Spades),
call(2, Strain::Hearts),
Call::Pass,
call(3, Strain::Hearts),
];
assert_eq!(best(&major, "AKJ982.K5.A54.63"), call(3, Strain::Spades));
assert_eq!(best(&raised, "765.A.AKJT9.6432"), Call::Double);
assert_eq!(best(&raised, "765.A.AKJ864.632"), call(3, Strain::Diamonds));
assert_ne!(best(&raised, "KQ5.A.T98764.632"), call(3, Strain::Diamonds));
assert_eq!(
best(&balancing, "KQ5.A.T98764.632"),
call(2, Strain::Diamonds)
);
let over_three = [
call(1, Strain::Diamonds),
call(3, Strain::Hearts),
Call::Pass,
Call::Pass,
];
assert_ne!(
best(&over_three, "K5.54.AQJ982.J43"),
call(4, Strain::Diamonds)
);
let transfer = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Hearts),
call(3, Strain::Clubs),
];
assert_ne!(best(&transfer, "K5.AQJ982.A5.K43"), call(3, Strain::Hearts));
let overcall = [
call(1, Strain::Hearts),
call(2, Strain::Diamonds),
call(2, Strain::Hearts),
Call::Pass,
Call::Pass,
];
assert_eq!(
best(&overcall, "K5.A54.AKJT98.63"),
call(3, Strain::Diamonds)
);
set_competitive_rebid(false);
}
#[test]
fn penalty_latch_doubles_the_runout_for_penalty() {
let auction = [
call(1, Strain::Notrump),
Call::Double,
call(2, Strain::Diamonds),
];
set_penalty_latch(false);
assert_eq!(best(&auction, "T98.964.AKQ7.853"), Call::Pass);
set_penalty_latch(true);
assert_eq!(best(&auction, "T98.964.AKQ7.853"), Call::Double);
let takeout = [call(2, Strain::Clubs)];
assert_eq!(best(&takeout, "AQ95.KJ73.K842.6"), Call::Double);
}
#[test]
fn penalty_latch_leaves_partner_s_double_in() {
let auction = [
call(1, Strain::Notrump),
Call::Double,
call(2, Strain::Diamonds),
Call::Double,
Call::Pass,
];
set_penalty_latch(false);
assert_eq!(best(&auction, "AQ74.AQ5.82.A632"), call(4, Strain::Spades));
set_penalty_latch(true);
assert_eq!(best(&auction, "AQ74.AQ5.82.A632"), Call::Pass);
}
#[test]
fn advancer_runs_from_redoubled_penalty_double() {
let auction = [call(1, Strain::Notrump), Call::Double, Call::Redouble];
assert_eq!(best(&auction, "J9763.852.764.43"), call(2, Strain::Spades));
assert_eq!(best(&auction, "82.43.765.QJ8765"), call(2, Strain::Clubs));
assert_eq!(best(&auction, "KQ7.K83.J642.643"), Call::Pass);
set_advancer_xx_runout(false);
assert_eq!(best(&auction, "J9763.852.764.43"), Call::Pass);
set_advancer_xx_runout(true);
}
#[test]
fn doubler_runs_from_redoubled_penalty_double() {
let auction = [
call(1, Strain::Notrump),
Call::Double,
Call::Redouble,
Call::Pass,
Call::Pass,
];
assert_eq!(best(&auction, "AQ765.KQ4.A82.K3"), call(2, Strain::Spades));
assert_eq!(best(&auction, "AQ4.K82.K3.AQ765"), call(2, Strain::Clubs));
assert_eq!(best(&auction, "AQ74.KQ32.A82.K3"), Call::Pass);
set_doubler_xx_runout(false);
assert_eq!(best(&auction, "AQ765.KQ4.A82.K3"), Call::Pass);
set_doubler_xx_runout(true);
}
#[test]
fn optional_latch_doubles_short_and_partner_cooperates() {
let runout = [
call(1, Strain::Notrump),
Call::Double,
call(2, Strain::Diamonds),
];
let cooperative = "KQ5.KQ5.642.QJ93";
set_latch_style(LatchStyle::Penalty);
assert_ne!(best(&runout, cooperative), Call::Double);
set_latch_style(LatchStyle::Optional);
assert_eq!(best(&runout, cooperative), Call::Double);
let advance = [
call(1, Strain::Notrump),
Call::Double,
call(2, Strain::Diamonds),
Call::Double,
Call::Pass,
];
set_latch_style(LatchStyle::Penalty);
assert_eq!(best(&advance, "AQ74.AQ5.82.A632"), Call::Pass);
set_latch_style(LatchStyle::Optional);
assert_eq!(best(&advance, "AQ74.AQ5.82.A632"), call(4, Strain::Spades));
set_latch_style(LatchStyle::default());
}
#[test]
fn advancing_a_double_bids_game_with_values() {
let auction = [call(2, Strain::Spades), Call::Double, Call::Pass];
assert_eq!(best(&auction, "AQ3.K65.J64.QJ96"), call(3, Strain::Notrump));
assert_eq!(best(&auction, "92.AQ53.KQ42.962"), call(4, Strain::Hearts));
}
#[test]
fn unforced_raise_with_fit() {
let auction = [call(1, Strain::Spades), call(2, Strain::Hearts)];
assert_eq!(best(&auction, "Q32.953.A964.Q92"), call(2, Strain::Spades));
}
#[test]
fn unforced_takeout_double_on_shape() {
let auction = [call(3, Strain::Diamonds)];
assert_eq!(best(&auction, "KQ32.AJ53.2.A942"), Call::Double);
}
#[test]
fn unforced_pass_without_values() {
let auction = [call(3, Strain::Diamonds)];
assert_eq!(best(&auction, "Q5432.J53.942.92"), Call::Pass);
}
#[test]
fn doubles_only_their_live_bids() {
let auction = [
call(1, Strain::Hearts),
call(1, Strain::Spades),
Call::Pass,
call(2, Strain::Spades),
Call::Pass,
];
let hand: Hand = "92.K53.AQJ42.962".parse().expect("valid test hand");
let context = Context::new(RelativeVulnerability::NONE, &auction);
let logits = instinct().classify(hand, &context);
assert_eq!(*logits.0.get(Call::Double), f32::NEG_INFINITY);
}
#[test]
fn settle_floor_defends_with_length_behind_their_suit() {
let auction = [call(3, Strain::Spades), Call::Double, Call::Pass];
let weak_with_defense = "9543.74.K2.QJ876";
set_settle_floor(false);
assert_eq!(best(&auction, weak_with_defense), call(4, Strain::Clubs));
set_settle_floor(true);
assert_eq!(best(&auction, weak_with_defense), Call::Pass);
let strong = "2.853.K42.AKQ876";
assert_eq!(best(&auction, strong), call(4, Strain::Clubs));
set_settle_floor(true); }
#[test]
fn completes_partners_transfer_over_notrump() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Diamonds),
Call::Pass,
];
assert_eq!(best(&auction, "AQ32.KJ5.KQ4.Q92"), call(2, Strain::Hearts));
}
#[test]
fn forced_to_game_opposite_strong_notrump() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Diamonds),
Call::Pass,
call(2, Strain::Notrump),
Call::Pass,
];
assert_eq!(best(&auction, "KQ52.AQ984.J6.32"), call(3, Strain::Notrump));
}
#[test]
fn forced_to_game_picks_the_known_major_fit() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(3, Strain::Hearts),
Call::Pass,
];
assert_eq!(best(&auction, "AQ52.K53.KQ4.32"), call(4, Strain::Hearts));
}
#[test]
fn transfer_invite_reaches_the_floor_over_a_possible_five_two() {
crate::bidding::american::set_sixcard_invite_floor(14);
let invite = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Diamonds),
Call::Pass,
call(2, Strain::Hearts),
Call::Pass,
call(3, Strain::Hearts),
Call::Pass,
];
let (bid, from_floor) = american_floored(&invite, "AKQ2.J5.AQ52.K42");
assert!(
from_floor,
"the transfer invite is off-book, the floor decides"
);
assert_eq!(bid, call(3, Strain::Notrump));
crate::bidding::american::set_sixcard_invite_floor(13); }
#[test]
fn floor_asks_keycards_with_slam_values_and_a_known_fit() {
let auction = [
call(1, Strain::Spades),
Call::Pass,
call(3, Strain::Spades),
Call::Pass,
];
assert_eq!(
best(&auction, "AKQJ7.AKQ.A32.32"),
call(4, Strain::Notrump),
"slam values + known fit → ask keycards, not 6♠ direct"
);
}
#[test]
fn floor_answers_keycards_1430() {
let auction = [
call(1, Strain::Spades),
Call::Pass,
call(3, Strain::Spades),
Call::Pass,
call(4, Strain::Notrump),
Call::Pass,
];
assert_eq!(
best(&auction, "KQ732.K53.Q42.92"),
call(5, Strain::Clubs),
"1 keycard → 5♣"
);
assert_eq!(
best(&auction, "QJ732.K53.Q42.Q2"),
call(5, Strain::Diamonds),
"0 keycards → 5♦"
);
assert_eq!(
best(&auction, "AK732.A53.842.92"),
call(5, Strain::Diamonds),
"3 keycards → 5♦"
);
assert_eq!(
best(&auction, "AQ732.A53.842.92"),
call(5, Strain::Spades),
"2 keycards + trump Q → 5♠"
);
assert_eq!(
best(&auction, "A8732.A53.842.92"),
call(5, Strain::Hearts),
"2 keycards, no trump Q → 5♥"
);
assert_eq!(
best(&auction, "AK732.A53.A42.A2"),
call(5, Strain::Clubs),
"5 keycards → 5♣"
);
}
#[test]
fn floor_asker_continues_after_the_answer() {
let auction = [
call(1, Strain::Spades),
Call::Pass,
call(3, Strain::Spades),
Call::Pass,
call(4, Strain::Notrump),
Call::Pass,
call(5, Strain::Diamonds),
Call::Pass,
];
assert_eq!(
best(&auction, "AQ752.A76.72.A93"),
call(5, Strain::Spades),
"two keycards missing → sign off"
);
assert_eq!(
best(&auction, "AK752.A76.72.A93"),
call(6, Strain::Spades),
"one keycard missing → small slam"
);
}
#[test]
fn floor_answerer_respects_the_signoff() {
let auction = [
call(1, Strain::Spades),
Call::Pass,
call(3, Strain::Spades),
Call::Pass,
call(4, Strain::Notrump),
Call::Pass,
call(5, Strain::Clubs),
Call::Pass,
call(5, Strain::Spades),
Call::Pass,
];
assert_eq!(
best(&auction, "KQ543.KQJ.KQJ4.K"),
Call::Pass,
"the answerer never overrides the asker's signoff short a keycard"
);
}
#[test]
fn control_bid_is_never_passed_out() {
let auction = [
call(1, Strain::Diamonds),
Call::Pass,
call(1, Strain::Spades),
Call::Pass,
call(2, Strain::Diamonds),
Call::Pass,
call(4, Strain::Hearts),
Call::Pass,
];
let (bid, from_floor) = american_floored(&auction, "A4.K85.KQJ62.Q75");
assert!(from_floor, "the 4♥ jump is off-book");
assert_eq!(
bid,
call(5, Strain::Diamonds),
"return to the agreed suit over partner's control bid"
);
}
#[test]
fn transfer_jump_to_game_reaches_the_floor_and_passes() {
let game = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Diamonds),
Call::Pass,
call(2, Strain::Hearts),
Call::Pass,
call(4, Strain::Hearts),
Call::Pass,
];
let (bid, from_floor) = american_floored(&game, "AKQ2.J5.AQ52.K42");
assert!(from_floor, "the 4♥ jump is off-book, the floor decides");
assert_eq!(bid, Call::Pass);
}
#[test]
fn nine_count_five_card_major_forces_game_after_a_transfer() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Hearts),
Call::Pass,
call(2, Strain::Spades),
Call::Pass,
];
let (bid, from_floor) = american_floored(&auction, "AK543.82.Q76.542");
assert!(from_floor, "the game force is off-book, the floor decides");
assert_eq!(bid, call(3, Strain::Notrump));
}
#[test]
fn opener_corrects_choice_of_games_3nt_to_the_known_major_fit() {
set_correct_3nt_to_major(true);
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Hearts),
Call::Pass,
call(2, Strain::Spades),
Call::Pass,
call(3, Strain::Notrump),
Call::Pass,
];
let (fit, _) = american_floored(&auction, "AQ4.K8.KJ72.Q832");
assert_eq!(
fit,
call(4, Strain::Spades),
"3-card support with a doubleton corrects to 4♠"
);
let (flat, _) = american_floored(&auction, "AQ4.K83.KJ72.Q83");
assert_eq!(
flat,
Call::Pass,
"flat 4333 has no ruff — 3NT is the better game"
);
let (two, _) = american_floored(&auction, "AQ.K842.KJ73.Q82");
assert_eq!(two, Call::Pass, "no eight-card fit leaves it in 3NT");
set_correct_3nt_to_major(true); }
#[test]
fn strong_balanced_redoubles_a_double_of_our_1nt_not_3nt() {
let auction = [call(1, Strain::Notrump), Call::Double];
let (bid, from_floor) = american_floored(&auction, "KQ4.KJ43.AQ62.Q5");
assert!(from_floor, "the response is off-book, the floor decides");
assert_eq!(bid, Call::Redouble);
}
#[test]
fn keeps_passing_with_a_weak_responder() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Diamonds),
Call::Pass,
call(2, Strain::Notrump),
Call::Pass,
];
assert_eq!(best(&auction, "8632.J9842.96.42"), Call::Pass);
}
#[test]
fn forced_to_game_after_strong_two_clubs() {
let auction = [
call(2, Strain::Clubs),
Call::Pass,
call(2, Strain::Diamonds),
Call::Pass,
call(2, Strain::Notrump),
Call::Pass,
];
assert_eq!(best(&auction, "QJ52.K43.T62.J32"), call(3, Strain::Notrump));
}
#[test]
fn forced_two_clubs_bids_major_game() {
let auction = [
call(2, Strain::Clubs),
Call::Pass,
call(2, Strain::Diamonds),
Call::Pass,
call(2, Strain::Notrump),
Call::Pass,
];
assert_eq!(best(&auction, "3.QJ9854.K32.J32"), call(4, Strain::Hearts));
}
#[test]
fn double_negative_two_clubs_may_pass() {
let auction = [
call(2, Strain::Clubs),
Call::Pass,
call(2, Strain::Hearts),
Call::Pass,
call(2, Strain::Notrump),
Call::Pass,
];
assert_eq!(best(&auction, "8632.J9842.96.42"), Call::Pass);
}
#[test]
fn forced_game_steps_aside_when_penalizing() {
let auction = [
call(2, Strain::Clubs),
Call::Pass,
call(2, Strain::Diamonds),
Call::Pass,
call(2, Strain::Notrump),
call(3, Strain::Diamonds),
Call::Double,
Call::Pass,
];
assert_eq!(best(&auction, "K3.KQ4.65.QJ8765"), call(4, Strain::Clubs));
}
#[test]
fn milestone_game_opposite_a_limited_rebid() {
let auction = [
call(1, Strain::Diamonds),
Call::Pass,
call(1, Strain::Hearts),
Call::Pass,
call(1, Strain::Notrump),
Call::Pass,
];
assert_eq!(best(&auction, "J9.AKJ7.K94.A852"), call(3, Strain::Notrump));
assert_eq!(best(&auction, "KJ9.QJ73.K94.852"), Call::Pass);
}
#[test]
fn milestone_slam_opposite_a_strong_rebid() {
let auction = [
call(1, Strain::Diamonds),
Call::Pass,
call(1, Strain::Hearts),
Call::Pass,
call(2, Strain::Notrump),
Call::Pass,
];
assert_eq!(best(&auction, "KQ.AKJ7.K94.8542"), call(6, Strain::Notrump));
}
#[test]
fn milestone_game_opposite_a_competitive_overcall() {
let auction = [
call(3, Strain::Diamonds),
call(3, Strain::Spades),
Call::Pass,
];
assert_eq!(best(&auction, "K32.AKJ.AQ4.KJ32"), call(4, Strain::Spades));
assert_eq!(best(&auction, "K32.KJ4.KQ4.5432"), Call::Pass);
}
#[test]
fn milestone_notrump_game_needs_a_stopper_in_competition() {
let auction = [
call(3, Strain::Clubs),
call(3, Strain::Diamonds),
Call::Pass,
];
assert_eq!(best(&auction, "AKQ.AQJ.32.K432"), call(3, Strain::Notrump));
assert_eq!(best(&auction, "AKQ4.AKQ4.32.432"), Call::Pass);
}
#[test]
fn rubens_new_suit_transfer() {
let auction = [call(1, Strain::Clubs), call(1, Strain::Spades), Call::Pass];
assert_eq!(best(&auction, "2.K32.KQT54.J432"), call(2, Strain::Clubs));
assert_eq!(best(&auction, "2.Q32.KQT54.J432"), Call::Pass);
}
#[test]
fn rubens_limit_raise_transfer() {
let auction = [call(1, Strain::Clubs), call(1, Strain::Spades), Call::Pass];
assert_eq!(best(&auction, "K54.K32.K43.Q432"), call(2, Strain::Hearts));
}
#[test]
fn rubens_completion_is_mechanical() {
let auction = [
call(1, Strain::Clubs),
call(1, Strain::Spades),
Call::Pass,
call(2, Strain::Clubs),
Call::Pass,
];
assert_eq!(
best(&auction, "AKJ52.K3.952.J32"),
call(2, Strain::Diamonds)
);
}
#[test]
fn rubens_two_level_cue_raise() {
let auction = [call(1, Strain::Spades), call(2, Strain::Clubs), Call::Pass];
assert_eq!(best(&auction, "432.K32.K2.KQJ54"), call(2, Strain::Spades));
}
#[test]
fn rubens_skips_jump_overcalls() {
let auction = [call(1, Strain::Clubs), call(2, Strain::Spades), Call::Pass];
assert_eq!(best(&auction, "K54.K32.K43.Q432"), call(3, Strain::Spades));
}
#[test]
fn rubens_completes_through_the_double() {
let auction = [
call(1, Strain::Clubs),
call(1, Strain::Spades),
Call::Pass,
call(2, Strain::Clubs),
Call::Double,
];
assert_eq!(
best(&auction, "AKJ52.K3.952.J32"),
call(2, Strain::Diamonds)
);
}
#[test]
fn rubens_max_breaks_the_completion_to_game() {
let auction = [
call(1, Strain::Clubs),
call(1, Strain::Spades),
Call::Pass,
call(2, Strain::Hearts),
Call::Pass,
];
assert_eq!(best(&auction, "AKJ52.K3.K52.J32"), call(4, Strain::Spades));
assert_eq!(best(&auction, "AKJ52.K3.Q52.432"), call(3, Strain::Spades));
assert_eq!(best(&auction, "AKJ52.K3.952.J32"), call(2, Strain::Spades));
let minor = [
call(1, Strain::Clubs),
call(1, Strain::Diamonds),
Call::Pass,
call(2, Strain::Clubs),
Call::Pass,
];
assert_eq!(best(&minor, "A32.K32.AQJ54.K2"), call(3, Strain::Notrump));
assert_eq!(best(&minor, "AQ2.K32.AQJ54.32"), call(2, Strain::Diamonds));
}
#[test]
fn rubens_new_suit_break_bids_what_it_would_over_natural() {
let auction = [
call(1, Strain::Clubs),
call(1, Strain::Spades),
Call::Pass,
call(2, Strain::Diamonds),
Call::Pass,
];
assert_eq!(best(&auction, "AKJ52.Q32.K52.32"), call(3, Strain::Hearts));
assert_eq!(best(&auction, "AKJ52.Q32.K52.A2"), call(4, Strain::Hearts));
assert_eq!(best(&auction, "AKJ52.32.Q952.J2"), call(2, Strain::Hearts));
}
#[test]
fn rubens_transferee_rebid_survives_an_out_of_band_two_spades() {
let auction = [
call(1, Strain::Clubs),
call(1, Strain::Diamonds),
Call::Pass,
call(2, Strain::Spades),
Call::Pass,
call(3, Strain::Clubs),
Call::Pass,
];
let _ = best(&auction, "K54.A32.KQ32.Q43");
}
#[test]
fn rubens_transferee_clarifies_with_extras() {
let hearts = [
call(1, Strain::Clubs),
call(1, Strain::Spades),
Call::Pass,
call(2, Strain::Diamonds),
Call::Pass,
call(2, Strain::Hearts),
Call::Pass,
];
assert_eq!(best(&hearts, "2.AKQT54.K32.A32"), call(4, Strain::Hearts));
assert_eq!(best(&hearts, "2.AKJT54.Q32.Q32"), call(3, Strain::Hearts));
let diamonds = [
call(1, Strain::Clubs),
call(1, Strain::Spades),
Call::Pass,
call(2, Strain::Clubs),
Call::Pass,
call(2, Strain::Diamonds),
Call::Pass,
];
assert_eq!(
best(&diamonds, "2.K32.AKQT54.A32"),
call(3, Strain::Notrump)
);
}
#[test]
fn rubens_raiser_moves_with_extras_over_the_completion() {
let auction = [
call(1, Strain::Clubs),
call(1, Strain::Spades),
Call::Pass,
call(2, Strain::Hearts),
Call::Pass,
call(2, Strain::Spades),
Call::Pass,
];
assert_eq!(best(&auction, "K54.A32.KQ32.Q43"), call(4, Strain::Spades));
assert_ne!(best(&auction, "K54.K32.K43.Q432"), call(4, Strain::Spades));
}
#[test]
fn rubens_cue_answer_places_the_contract() {
let auction = [
call(1, Strain::Spades),
call(2, Strain::Clubs),
Call::Pass,
call(2, Strain::Spades),
Call::Pass,
];
assert_eq!(best(&auction, "32.K32.Q32.AQJT54"), call(3, Strain::Clubs));
assert_eq!(best(&auction, "A2.K32.Q2.AKQJ54"), call(3, Strain::Notrump));
let hearts = [
call(1, Strain::Spades),
call(2, Strain::Hearts),
Call::Pass,
call(2, Strain::Spades),
Call::Pass,
];
assert_eq!(best(&hearts, "2.AKQJ54.K32.Q32"), call(4, Strain::Hearts));
assert_eq!(best(&hearts, "Q2.AQJT54.432.32"), call(3, Strain::Hearts));
}
#[test]
fn rubens_cue_answer_fires_through_the_system() {
let auction = [
call(1, Strain::Spades),
call(2, Strain::Clubs),
Call::Pass,
call(2, Strain::Spades),
Call::Pass,
];
let (call_made, floored) = american_floored(&auction, "32.K32.Q32.AQJT54");
assert!(floored, "the overcaller's cue answer is floor territory");
assert_eq!(call_made, call(3, Strain::Clubs));
}
#[test]
fn rubens_skips_advances_of_a_double() {
let auction = [
call(1, Strain::Hearts),
Call::Double,
Call::Pass,
call(1, Strain::Spades),
];
assert_eq!(overcall_shape(&auction), None);
}
#[test]
fn rubens_disabled_reverts_to_natural_advances() {
set_rubens_advances(false);
let auction = [call(1, Strain::Clubs), call(1, Strain::Spades), Call::Pass];
assert_eq!(best(&auction, "K54.K32.K43.Q432"), call(2, Strain::Spades));
assert_eq!(
best(&auction, "2.K32.KQT54.J432"),
call(2, Strain::Diamonds)
);
let after = [
call(1, Strain::Clubs),
call(1, Strain::Spades),
Call::Pass,
call(2, Strain::Clubs),
Call::Pass,
];
assert_ne!(best(&after, "AKJ52.K3.952.J32"), call(2, Strain::Diamonds));
set_rubens_advances(true);
}
#[test]
fn one_nt_runout_disabled_passes() {
set_one_nt_runout(false);
let doubled = [call(1, Strain::Notrump), Call::Double];
assert_eq!(best(&doubled, "32.QJ763.9742.83"), Call::Pass);
set_one_nt_runout(true);
}
#[test]
fn one_nt_runout_escapes_to_the_long_suit() {
set_one_nt_runout(true);
let doubled = [call(1, Strain::Notrump), Call::Double];
assert_eq!(best(&doubled, "32.QJ763.9742.83"), call(2, Strain::Hearts));
assert_eq!(best(&doubled, "T9842.3.7.QJ9632"), call(2, Strain::Clubs));
assert_eq!(best(&doubled, "432.J85.K74.9632"), Call::Pass);
set_one_nt_runout(true);
}
#[test]
fn one_nt_runout_redoubles_with_values() {
set_one_nt_runout(true);
set_runout_xx_min(8);
let doubled = [call(1, Strain::Notrump), Call::Double];
assert_eq!(best(&doubled, "K43.KQ5.8642.972"), Call::Redouble);
assert_eq!(best(&doubled, "3.QJ763.97642.83"), call(2, Strain::Hearts));
set_runout_xx_min(7);
set_one_nt_runout(true);
}
#[test]
fn gambling_3nt_over_double_routes_long_minors() {
set_one_nt_runout(true);
set_gambling_3nt_over_double(true);
set_gambling_3nt_top_honors(2);
set_gambling_3nt_require_ace(true);
let doubled = [call(1, Strain::Notrump), Call::Double];
assert_eq!(best(&doubled, "32.43.654.AKJ987"), call(3, Strain::Notrump));
assert_eq!(best(&doubled, "32.43.AKJ987.654"), call(3, Strain::Notrump));
assert_eq!(best(&doubled, "KQ4.KJ43.AQ62.Q5"), Call::Redouble);
assert_eq!(best(&doubled, "32.43.654.KQJ987"), call(2, Strain::Clubs));
set_gambling_3nt_require_ace(false);
assert_eq!(best(&doubled, "32.43.654.KQJ987"), call(3, Strain::Notrump));
set_gambling_3nt_require_ace(true);
assert_eq!(best(&doubled, "32.43.654.AJ9876"), call(2, Strain::Clubs));
set_gambling_3nt_top_honors(0);
assert_eq!(best(&doubled, "32.43.654.AJ9876"), call(3, Strain::Notrump));
set_gambling_3nt_top_honors(2);
set_gambling_3nt_over_double(false);
set_one_nt_runout(true);
}
#[test]
fn preempt_4m_over_double_jumps_the_long_major() {
set_one_nt_runout(true);
set_preempt_4m_over_double(true);
set_preempt_4m_top_honors(2);
set_preempt_4m_require_ace(true);
let doubled = [call(1, Strain::Notrump), Call::Double];
assert_eq!(best(&doubled, "432.AKJ987.65.32"), call(4, Strain::Hearts));
assert_eq!(best(&doubled, "AKJ987.432.65.32"), call(4, Strain::Spades));
assert_eq!(best(&doubled, "432.KQJ987.65.32"), call(2, Strain::Hearts));
set_preempt_4m_require_ace(false);
assert_eq!(best(&doubled, "432.KQJ987.65.32"), call(4, Strain::Hearts));
set_preempt_4m_require_ace(true);
assert_eq!(best(&doubled, "432.AJ9876.65.32"), call(2, Strain::Hearts));
set_preempt_4m_top_honors(0);
assert_eq!(best(&doubled, "432.AJ9876.65.32"), call(4, Strain::Hearts));
set_preempt_4m_top_honors(2);
set_preempt_4m_over_double(false);
set_one_nt_runout(true);
}
#[test]
fn one_nt_runout_2nt_scrambles_the_minors() {
set_one_nt_runout(true);
set_unusual_2nt(Unusual2nt::FourFour);
let doubled = [call(1, Strain::Notrump), Call::Double];
assert_eq!(best(&doubled, "K3.842.Q642.J642"), call(2, Strain::Notrump));
let after = [
call(1, Strain::Notrump),
Call::Double,
call(2, Strain::Notrump),
Call::Pass,
];
assert_eq!(best(&after, "AQ5.KQ4.32.AK842"), call(3, Strain::Clubs));
assert_eq!(best(&after, "AQ5.KQ4.AK842.32"), call(3, Strain::Diamonds));
set_unusual_2nt(Unusual2nt::Direct);
set_one_nt_runout(true);
}
#[test]
fn one_nt_runout_2nt_shape_modes() {
set_one_nt_runout(true);
let doubled = [call(1, Strain::Notrump), Call::Double];
set_unusual_2nt(Unusual2nt::FourFour);
assert_ne!(best(&doubled, "3.42.KQ876.J8765"), call(2, Strain::Notrump));
set_unusual_2nt(Unusual2nt::FiveFiveAdd);
assert_eq!(best(&doubled, "3.42.KQ876.J8765"), call(2, Strain::Notrump));
set_unusual_2nt(Unusual2nt::Direct);
assert_eq!(
best(&doubled, "K3.842.Q642.J642"),
call(2, Strain::Diamonds)
);
set_unusual_2nt(Unusual2nt::Direct);
set_one_nt_runout(true);
}
#[test]
fn one_nt_runout_penalizes_escape_on_stack() {
set_one_nt_runout(true);
set_penalize_escape_values(false);
let run = [
call(1, Strain::Notrump),
Call::Double,
Call::Redouble,
call(2, Strain::Clubs),
];
set_penalize_escape_stack(true);
assert_eq!(best(&run, "Q52.K43.Q43.AKJ4"), Call::Double);
set_penalize_escape_stack(false);
assert_ne!(best(&run, "Q52.K43.Q43.AKJ4"), Call::Double);
set_penalize_escape_stack(true);
set_penalize_escape_values(true);
set_one_nt_runout(true);
}
#[test]
fn one_nt_runout_leaves_in_escape_penalty() {
set_one_nt_runout(true);
set_penalize_escape_stack(true);
let doubled_run = [
call(1, Strain::Notrump),
Call::Double,
Call::Redouble,
call(2, Strain::Clubs),
Call::Double,
Call::Pass,
];
assert_eq!(best(&doubled_run, "KQ3.K54.J632.987"), Call::Pass);
set_penalize_escape_stack(true);
set_one_nt_runout(true);
}
#[test]
fn one_nt_runout_penalizes_escape_on_values() {
set_one_nt_runout(true);
set_penalize_escape_stack(false);
set_penalize_escape_values(true);
let run = [
call(1, Strain::Notrump),
Call::Double,
Call::Redouble,
call(2, Strain::Clubs),
];
assert_eq!(best(&run, "AQ5.KQ43.K3.6432"), Call::Double);
let again = [
call(1, Strain::Notrump),
Call::Double,
Call::Redouble,
call(2, Strain::Clubs),
Call::Double,
call(2, Strain::Diamonds),
];
assert_eq!(best(&again, "KQ3.K54.J632.987"), Call::Double);
let sos = [
call(1, Strain::Notrump),
Call::Double,
Call::Pass,
Call::Pass,
Call::Redouble,
call(2, Strain::Clubs),
];
assert_ne!(best(&sos, "J32.Q54.J632.987"), Call::Double);
set_penalize_escape_stack(true);
set_penalize_escape_values(true);
set_one_nt_runout(true);
}
#[test]
fn one_nt_runout_universal_opener_escapes_and_sos() {
set_one_nt_runout(true);
set_one_nt_runout_universal(true);
let balancing = [
call(1, Strain::Notrump),
Call::Double,
Call::Pass,
Call::Pass,
];
assert_eq!(
best(&balancing, "AQ542.KJ.K43.Q32"),
call(2, Strain::Spades)
);
assert_eq!(best(&balancing, "AQ4.KJ2.K432.Q32"), Call::Redouble);
let after_sos = [
call(1, Strain::Notrump),
Call::Double,
Call::Pass,
Call::Pass,
Call::Redouble,
Call::Pass,
];
assert_eq!(
best(&after_sos, "QJ32.842.642.J32"),
call(2, Strain::Spades)
);
set_one_nt_runout_universal(true);
set_one_nt_runout(true);
}
#[test]
fn one_nt_runout_opener_passes_not_completes_phantom_transfer() {
set_one_nt_runout(true);
let after_runout = [
call(1, Strain::Notrump),
Call::Double,
call(2, Strain::Hearts),
Call::Pass,
];
assert_eq!(best(&after_runout, "AQ4.KJ3.KQ52.432"), Call::Pass);
set_one_nt_runout(true);
}
}